글 작성 시 퍼포먼스 테스트를 위해 사용한 Node 버전 → v18.15.0
해당 Node.js의 v8 엔진 버전 → 10.2.154.26
⚽️ 목표
immutable한 값 변화가 좋다는데… performance 차이는 어떨까?
•
리액트는 immutable 한 값 변화를 활용해 퍼포먼스를 최적화했다는데… 대체 어떤 면에서 그렇다는건데?로 시작한 생각들이다.
•
“무적권 빠른거 쓰세요”를 말하는 글이 절대 아니다. 화내기 전에 글읽는 5분만 참아달라. 제발..
TL;DR
immutable한 값 변화는 mutable한 값 변화 보다 느리다
•
하지만 걱정할 필요없다구!
비교해야할 대상이 복잡할 수록(nested object, long array) shallow comparison이 deep comparison보다 빠르다
•
복잡한 구조의 객체, 배열을 일수록 shallow comparison이 더 빠르다.
1. Immutability?
객체나 값이 생성된 후 변경될 수 없는 특성을 가리키는 프로그래밍 개념
const arr1 = new Array(1000000).fill(0);
const arr2 = new Array(1000000).fill(0);
// mutable 값 변경 코드
arr1[0] = 1;
// immutable 값 변경 코드
const newArr = [...arr2];
newArr[0] = 1;
TypeScript
복사
•
muatable한 변화 → 원본 객체 또는 값이 변경될 경우 해당 값을 참조하는 다른 코드에서 side effect가 생길 가능성이 존재한다. → (아! 식상해… 다른 사람들 다 알고있는 사실이잖아?)
◦
해당 객체를 참조하는 다른 함수나 코드에서 예기치 않은 결과 초래한다.
◦
예기치 않은 결과 → 모든 레퍼런스에서 나오는 제일 중요한 키워드
•
immutable한 값 변화를 사용함에 대한 개발적(DX) 장점은 확실하다 → 퍼포먼스적으로 차이점은 무엇일까?를 알아보자.
2. performance 비교 테스트 하기 전에
•
퍼포먼스 비교는 총 두개로 나뉘어진다.
1.
immutable한 값 변화의 퍼포먼스
2.
변경된 값 확인 여부를 위한 shallow comaprision 과 deep comparision 비교
•
간단한 비교를위해 평균 속도 측정시 소수점 두자리 아래는 모두 버린다.
•
명확한 테스트 결과를 확인을 위해 극단적인 양의 비교를 수행했다. 알고있다, 실제로 이렇게 많은 비교는 아주 극히 일부 케이스라는걸..
비교 1. immutable한 값 변화와 direct 값 변화 퍼포먼스 비교
테스트 코드
결과
mutable한 값 변화가 훨씬 더 빠름
1회 | 2회 | 3회 | 평균 | |
23.77ms | 27.63ms | 27.99ms | 26.46ms | |
8.87ms | 9.92ms | 9.51ms | 9.43ms |
•
immutable 하게 바꾸는게 더 많은 메모리 사용과 느린 실행 속도를 유발한다.→ 대부분의 use case에서
•
값 변경 시, 직접 해당 값을
1. 객체(배열) 복사
2. 값 증가 후 새로운 객체(배열 생성)
라는 프로세스가 존재하기 때문에 직접 해당 주소의 값을 바꾸는게 훨씬 더 빠르고 메모리 소모가 적다.
비교 2. Deep comparison, Shallow Comparison의 퍼포먼스 비교
테스트 코드
결과
비교하는 대상이 복잡해지면 복잡해질 수록 Shallow Comparison이 훨씬 더 빠름
1회 | 2회 | 3회 | 평균 | |
0.32ms | 0.33ms | 0.36ms | 0.33ms | |
4.45ms | 4.15ms | 4.47ms | 4.35ms |
•
Shallow Comparison은 해당 데이터의 주소값만 가져와 비교하므로 비교 대상이 복잡해지면 복잡해질 수록 Deep Comparison에 비해 빠른 속도를 가진다.
•
하나의 값을 비교할 때는 오히려 Deep Comparison이 빠르다.
◦
해당 값을 가지고 있는 주소값을 가져오는 것은 직접 값을 참조하여 비교하는 것 보다 퍼포먼스 오버헤드가 존재하기 때문이다.
결론
리액트에선 왜 mutation 하지말라는거야?
개발 시 리액트를 사용하며 흔하게 state를 mutating 하는 상황이 발생할 수 있지만 이런 접근 방식은 리액트의 보이는 데이터 처리 접근방식에 반하는것→ state 변화가 리렌더링이다 를 기조로 리액트 팀은 기능 개발 및 최적화를 하고있다. - 리액트 공식문서 -
•
한마디로 state mutation 자체는 모터사이클 윌리와 비교할 수 있다. → 하란대로 하지 왜 이상하게 타는거야?
윌리 사진
◦
모터사이클 제조사들은 모터사이클 앉아서 두손 잡고 타기 위해 수많은 테스트를 했고 이 테스트 들을 기반으로 제품을 디자인하고 퍼포먼스를 튜닝한다.
◦
모터사이클 제조사들은 ABS와 TCS와 같은 안전을 위한 전자장비들이 적용되며 ECU 펌웨어 업데이트 또한 그에맞게 수행된다.
◦
두바퀴 땅바닥에 닿아있고 정상적인 자세로 운전을 위한 업데이트와 기능 추가가 이루어진다.
◦
근데 앞 바퀴 두개 다들고 시트가 아닌 엔진탱크위에 앉아서 주행을 한다면??? → 한마디로 “그렇게 타라고 만든게 아닌데 왜 그렇게 타냐ㅋㅋ?”
◦
이런 위험 주행 시 사고가난다면 그 누구도 제조사를 탓하지 않는다. 당연히 라이더를 탓하지.. → “쟨 왜그렇게 탄거래?ㅋㅋ”
◦
결론은 “제조사가 하지말란건 하지마라 좀.. 그러라고 만든거 아니라고..”
immutable한 값 변화
•
비교 1 테스트는 백만개의 값 변화 시 속도 차이에 대한 테스트이다. → “어떤게 더 빠른가!” 에대한 차이를 극명하게 보기 위한 극단적 테스트 케이스
•
실제 프로덕트 개발 시 setState로 값 변화는 아무리 많아야 10개 이하 → state 값 변화에 대한 퍼포먼스 걱정 보다는 바뀐 state를 찾는 퍼포먼스 concern이 훨씬 더 크고 해당되는 case도 많다.
shallow comparion
•
state가 변경을 감지하는데 deep comaprison으로 리렌더링 되어야 할 컴포넌트 감지 시 시간 복잡도가 늘어나 문제가 생긴다.
◦
state가 변경됨 → 변경된 값을 감지 해야함 → 모든 객체, 배열 내부의 property의 값을 비교하면 시간이 더 오래 걸린다.
•
그에 반해 shallow comparison으로 값을 비교할 경우 객체 내부의 모든 값 들의 변경 감지하는 대신 주소값 비교이므로 더 나은 퍼포먼스 제공한다.
◦
새로 렌더링 되어야할 값들을 감지하는데 걸리는 시간단축