프론트엔드 개발을 더 쉽게 만들어주는 library이다.
리액트가 해결하려는 문제
- 컴포넌트별로 구분하여 유지보수를 용이하게 함
- Virtual-DOM을 사용하여 사용자의 액션에 따른 수정사항들을 적은 비용으로 적용
Virtual DOM 이란?
→ 실제 DOM의 구조와 비슷한 React 객체의 트리
→ 개발자가 Virtual DOM을 제어하면 React에서 적절한 과정을 통해 Virtual DOM을 DOM에 반영한다.
장점
- DOM을 직접 조작하지 않아도 된다
- 수개의 DOM을 직접 관리, 조작하는 과정은 복잡하고 실수할 가능성, 연산비용 등 고려할 게 많은데 Virtual DOM이 이러한 과정들을 자동화, 추상화 해준다
- DOM 의 update를 batch 작업(실시간 처리가 아닌, 일괄적으로 모아서 처리하는 작업)을 통해 연산을 최소화, 연산을 묶어서 한 번에 처리
Real DOM의 경우에는 업데이트 할 때마다 수정되는 DOM과 관련있는 모든 자식 노드, 부모 노드를 업데이트를 해야한다. 반복해서 업데이트하거나 DOM트리가 클 경우 성능에 나쁜 영향을 끼친다. React는 Virtual DOM을 사용하여 트리의 변경되는 부분만 업데이트하는 방식(diffing algorithm)으로 DOM을 업데이트 하기 때문에 성능면에서 우월하다.
- Virtual DOM 트리를 메모리에 새로 생성
- 이전 Virtual DOM 트리와 O(n)의 diffing algorithm을 사용하여 차이점을 파악한다.
- 차이점들을 하나로 모아서 실제 DOM에 전달
→ 이로 인해서 실제 DOM의 리렌더링 연산(Reflow, Repaint) 이 단 한 번만 일어나게 되어 큰 성능의 이득을 얻게 된다.
→ 관리하는 state가 많을 수록 변경사항을 추적하는 것보다 새로 생성하여 비교하는 것이 더욱 효율적!
전체 DOM트리를 탐색하고 비교하는 일반적인 알고리즘은 O(n^3)의 시간복잡도를 갖는다. 그래서 React는 두 가지 가정 아래에서 복잡도를 O(n)에 근사하도록 구현했다.
가정
- 서로 다른 타입의 두 엘리먼트는 서로 다른 트리를 만든다.
- key prop을 통해 어떤 자식 엘리먼트가 변경되지 않아야 할 지 표시해 줄 수 있다. (동일한 key를 갖는 자식만 비교한다) (key는 sibling 사이에서만 유일하면 된다.)
- 함수형 컴포넌트 사용시 메모리에 이점이 있다.
- 클래스형 컴포넌트의 경우에는 생명주기 메소드를 사용하여 특정 시점에 코드가 실행되도록 하지만, 함수형 컴포넌트에서는 Hook을 사용하여 생명주기에 원하는 동작을 실행한다.
근본적으로 다른 것은 아니다. 함수형이 조금 더 편할 뿐!
→ 함수형 컴포넌트를 통해 함수형 프로그래밍으로 코드를 작성할 수 있다.
기존 class 컴포넌트의 문제점
- 컴포넌트가 복잡해지면 이해하기 어렵다.
- 생명주기 메소드에 관련없는 로직이 쉽게 섞이기에, 버그가 쉽게 발생하고 무결성이 해쳐진다
- class 컴포넌트는 JS의 this키워드 동작방식을 제대로 알아야만 했다
- 문법제안의 도움이 없을 때 코드를 매우 장황하게 만든다.
위의 문제를 해결하기 위해 Hook 도입
Hook은 함수 컴포넌트에서 React state와 생명주기 메소드를 연동할 수 있게 해주는 함수이다.
- 함수 범위 내에 Hook을 둠으로써 JS의 클로저를 활용하여 문제를 해결
- 계층의 변화 없이 상태관련 로직을 재사용할 수 있도록 한다.
리액트 컴포넌트를 매개변수로 받아서 새로운 리액트 컴포넌트를 리턴하는 함수이다.
리렌더링 조건을 제대로 활용하자
리렌더링의 경우,
- 부모에서 전달받은 props 가 변경될 때
- 부모 컴포넌트가 리렌더링 될 때
- 자신의 state가 변경될 때
발생한다.
- useMemo
컴포넌트 내의 어떤 함수가 값을 리턴하는데(연산에) 많은 시간을 소요한다면, 이 컴포넌트가 리렌더링 될 때마다 함수가 호출되면서 많은 시간을 소요하게 될 것이고, 함수의 반환값을 활용하는 하위 컴포넌트가 있다면 그 하위컴포넌트는 매 함수호출마다 새로운 값으로 리렌더링 될 것이다.
useMemo를 사용하면 두번째 parameter로 dependency를 전달할 수 있는데 해당 dependency 값이 변경될 때에만 연산을 수행함으로써 최적화를 할 수 있다.
→ 연산량이 많은 함수의 반환값을 memoization
- React.memo (함수형 컴포넌트)
react.memo() 로 래핑할 경우 리액트는 컴포넌트를 렌더링한 뒤 그 결과를 기억하고 있게되어 새롭게 렌더링 하고 이전과 비교하는 과정을 생략할 수 있다. props가 같을 경우에만 적용된다. 가상 DOM과 바뀐 부분 비교생략!
→ props 를 비교할 때는 shallow compare 로써 원시값 타입은 같은 값을 갖는지, 객체나 배열 등은 같은 참조값을 갖는지 확인한다.
→ 컴포넌트를 렌더링 한뒤 그 결과를 memoization
→ ( class형 컴포넌트에서는 React.PureComponent를 사용하자 (똑같이 얕은 비교를 통해서 state나 props의 변경을 인지한다, shouldComponentUpdate 에 얕은비교가 이미 적용되어 있음)
- useCallback
인라인 함수를 props로 전달할 경우 re-rendering마다 함수가 메모리에 새로 할당되므로 해당 인라인 함수를 사용하는 컴포넌트가 다시 렌더링 된다.
컴포넌트 내부의 함수들은 리렌더링 시 마다 새로
→ 특정 함수를 새로 만들지 않고 재사용
- 자식 컴포넌트의 props로 객체를 넘겨줄 때 데이터 가공을 하위컴포넌트에 위임하기
props의 값으로 객체를 넘겨주는 경우에는 컴포넌트가 리렌더링 될 때마다 새로운 객체가 생성되어 자식컴포넌트로 전달된다. 새로 생성된 객체는 이전 객체와 다른 참조 주소를 가지기 때문에 자식 컴포넌트는 메모이제이션이 되지 않는다.
→ 데이터가공은 하위컴포넌트에서 해서 상위 컴포넌트가 리렌더링 될 때 불필요하게 리렌더되는 상황을 방지하자
- key 값으로 index 사용하지 않기
React에서는 key를 사용하여 React element를 식별하고 비교하여 차이점을 새로 그린다. 고유한 id가 아닌 index를 key로 사용한다면 새로운 아이템이 맨 앞에 들어올 경우 index가 하나씩 밀려서 key가 들어간 형제노드들을 전부 update해야한다.
- onChange 이벤트처럼 타이핑을 할 때마다 렌더링되는 경우
React 18에서 추가된 Transition 언급!