DOM을 리렌더링 하는 것은 굉장히 비싼 비용을 치러야 하는 작업이다. 따라서, 리액트에선 불필요한 리렌더링을 줄이는 것만으로도 웹 애플리케이션의 성능을 엄청나게 개선할 수 있다.
리액트 훅과 리액트 내장 메서드를 사용해서 불필요한 리렌더링을 방지하기 위한 여러 방법이 있다.
useMemo 훅과 리액트 내장 메서드인 memo는 메모이제이션 기법을 사용해 리렌더링을 최소화한다.
메모이제이션이란, 컴퓨터 과학에서 동일한 연산을 반복하지 않도록 연산 결과를 메모해 두고(메모리에 저장해 두고), 해당 연산이 다시 호출됐을 때 메모된 연산이라면 결과를 바로 가져오도록 하는 기술이다.
메모이제이션을 사용하면 불필요한 연산을 줄일 수 있으며, 리액트에서는 말 그대로 '불필요한 연산을 줄여줄 수 있다'.
useMemo와 memo 모두 다 특정한 상태가 변경돼서 컴포넌트가 리렌더링 될 때 굳이 다시 리렌더링 되지 않아도 되는 걸 렌더링 배치에서 제외하는 식으로 동작한다.
useMemo는 콜백 함수의 의존성 배열을 인자로 받으며, 의존성 배열에 있는 값이 변경이 된 경우에만 콜백 함수의 값을 다시 연산해서 반환해 준다. 만약에 의존성 배열에 있는 상태가 변경되지 않았다면 한번 연산해 둔 결과를 메모해 둔 후에 필요할 때 반환해 준다.
memo는 부모 컴포넌트에 정의된 상태가 변경돼서 하위 컴포넌트들을 리렌더링 할 때 경우에 따라서는 굳이 리렌더링 되지 않아도 되는 컴포넌트들도 존재할 수 있다(두 개 이상의 컴포넌트에서 공통으로 사용하는 상태는 상위 부모 컴포넌트에서 정의되는데, 꼭 모든 부모 요소의 상태가 모든 자식 요소에게 필요한 건 아닐 수 있다).
그런 경우에 export 해주는 컴포넌트명을 memo로 감싸주면 해당 컴포넌트가 꼭 렌더링 돼야 하는 상태 변화가 있을 시에만 렌더링을 해주게 된다.
memo는 props로 받는 모든 요소를 비교해 변경 여부를 파악하는데, 이때 참조형 데이터인 props는 내부의 값이 전부 같더라도 참조 주소가 다르면 변경이 있는 것으로 해석하고 리렌더링을 하게 된다. 경우에 따라서는 참조 주소는 바뀌더라도 내부에서 관리하는 값들이 모두 일치하면(또는 일부 몇 개의 값이 일치하면) 굳이 리렌더링을 안 시키고 싶을 수 있다. 이런 경우엔 memo의 두 번째 인자로 콜백 함수를 전달하고, 해당 콜백 함수가 true를 반환하면 리렌더링이 되지 않고, false를 반환하면 리렌더링 된다.
memo는 고차 함수(HOC; High-Order Components)이다. 고차 함수는 컴포넌트를 인자로 받아 컴포넌트를 리턴하는 함수이다. 여러 컴포넌트에 동일한 로직을 추가해주고 싶을 때 고차 함수를 사용할 수 있다(고차 함수에 대해 추가로 공부해 보기).
만약에 특정 로직 처리 함수가 함수 본문 내용은 동일한데 리렌더링 시 참조가 변경되어서 해당 함수를 props로 받는 특정 컴포넌트가 상태의 변경으로 인지되는 경우가 있다. 이런 경우를 방지하기 위해선, 함수가 리렌더링 될 때 다시 생성되지 않고 한번 생성한 함수를 고정하고 싶을 수 있다. 이럴 때 useCallback이라는 훅을 사용한다.
useCallback은 첫 번째 인자로 콜백 함수를 전달하고, 두 번째 인자로 의존성 배열을 전달한다. 의존성 배열 안에 특정 상태가 추가되면 해당 상태가 변경될 때 첫 번째 인자의 콜백 함수를 재선언해주고, 그렇지 않을 땐 함수가 여러 번 리렌더링 되더라도 재선언 되지 않도록 막아준다. 의존성 배열을 빈 배열로 전달하면 컴포넌트가 처음 마운트 될 때에만 함수를 재선언하고 그 이후엔 고정해서 사용한다.
모든 상황에서 최적화 방법을 적용하는 게 항상 좋은 건 아니다. 로직이 복잡하지 않은 간단한 컴포넌트는 차라리 리렌더링을 시키는 게 useMemo, memo, useCallback 등의 리렌더링 방지 훅-내장 메서드의 연산 비용보다 저렴할 수 있다.
최적화는 언제나 기능 구현이 완료된 다음에 하자. 최적화로 인해 기능 구현의 동작이 예상 가능하게 작동하지 않아 개발 과정에 혼란이 생길 수 있다.
리액트 createRoot 공부
npm create vite@latest로 리액트 프로젝트를 빌드하면 main.jsx에 createRoot(document.getElementById('root')).render() 라는 메서드들이 체이닝 해서 실행되는 것이 확인된다. 여기서 createRoot는 리액트 18 버전에 추가된 New root api이다.
기존에 root를 사용자가 특정할 방법이 없었다면(DOM의 요소를 통해서만 root인 요소에 접근이 가능했다), createRoot를 사용하면 특정 HTMLElement 요소를 리액트 프로젝트의 최상단 노드를 가리키는 포인터인 root로 정의 내릴 수 있다.
createRoot().render()는 리액트로만 만들어진 웹 애플리케이션에선 처음 웹 애플리케이션이 마운트 될 때 한 번만 실행되고, 이후부턴 리액트의 가상 돔(Virtual DOM)을 통해 UI(상태)의 변화가 발생한 컴포넌트만 파악해서 DOM의 필요한 부분만 리렌더링 한다.
createRoot의 등장은 또한 SSR 기능을 구현하는 데 있어 필요한 hydrate 동작을 수월하게 해 주기 위한 것이라고 추측된다.
리액트의 여러 컨셉이나 개념이 아직 머릿속에 잘 그려지지가 않아서 공부하는 데 시간도 많이 걸리고 어려움이 많이 있다. 내일은 JSX 문법이랑 Virtual DOM에 대해 공부해보려고 한다. 자바스크립트 처음 공부할 때에도 꽤나 막막했는데, 지금은 살짝은 익숙해진 것처럼, 리액트도 하나씩 차근차근 공부하면 이해도가 많이 높아질 수 있을 것이라고 생각한다. 파이팅 해봐야겠다.