React의 Context API는 전역 상태 관리를 쉽게 만들어 주는 도구입니다. 하지만 상태가 변경될 때마다 구독하는 모든 컴포넌트가 리렌더링된다는 특성 때문에, 불필요한 리렌더링이 발생하는 문제를 겪을 수 있습니다.
개인 프로젝트를 진행하던 중, Context로부터 값을 받는 컴포넌트들을 React.memo
로 감싸면 리렌더링을 줄일 수 있지 않을까라는 호기심이 생겼고, 두 기술에 대해 공부한 것을 짧게 정리하고자 합니다.
Context API에서는 상태가 업데이트되면 해당 Context를 구독하는 모든 컴포넌트가 리렌더링됩니다. 이는 Context 값을 사용하는 모든 하위 컴포넌트가 Context 값이 변경될 때마다 자동으로 재렌더링되기 때문입니다.
React.memo
는 컴포넌트 성능 최적화를 위한 도구로, 전달받은 props
가 변경되지 않으면 리렌더링이 발생하지 않습니다.
구체적으로 React.memo
는 얕은 비교(shallow comparison)를 사용하여 props
가 변경되었는지 확인합니다. 얕은 비교란 props
가 객체일 경우, 그 참조값(reference)을 비교하는 방식을 말합니다. 즉, 내부의 속성을 비교하는 것이 아니라 객체 자체가 동일한 참조를 가리키고 있는지를 확인합니다.
이러한 얕은 비교 방식은 리엑트에서 대부분의 성능 개선에 효과적이지만, 상태관리와 Context API에 관련해서는 리렌더링을 유발할 수 있습니다. 예를들어, Context에서 관리하는 상태가 업데이트 될 때 새로운 객체가 생성되어 전달되면 그 참조값이 변경됩니다. 그렇기 때문에 React.memo
는 얕은 비교를 통해 이전에 저장된 참조값과 달라졌다고 판단하여 props
가 변경된 것으로 인식하고 리렌더링을 발생시킵니다.
Context API에서 컴포넌트의 불필요한 리렌더링 문제를 해결하기 위해서는 더 세밀하게 상태를 관리하거나 상태 업데이트의 영향을 최소화하는 방법이 있습니다.
1. Context 분할 (Context Splitting)
상태를 더 세분화하여 Context로 나누어 관리하면, 특정 Context 값이 업데이트될 때 그 Context를 구독하는 컴포넌트만 리렌더링됩니다. 이렇게 상태를 분리하면 불필요한 리렌더링을 줄일 수 있습니다. Context API를 전역적인 상태관리 도구로 사용하기 보다는 부모-자식 컴포넌트 간의 상태 주입을 위한 용도로 사용하는 것이 적절하다고 판단됩니다.
2. Jotai, Zustand, Recoil과 같은 상태 관리 라이브러리 사용
Jotai와 같은 상태 관리 라이브러리를 사용하면 atom 단위로 상태를 관리하여 필요한 컴포넌트만 리렌더링되도록 최적화할 수 있습니다. 이러한 상태 관리 라이브러리들은 Context API와 달리 더 세밀한 컴포넌트 단위에서 리렌더링을 제어할 수 있어, 전역 상태 관리뿐만 아니라 세부 상태 관리에서도 유리합니다. 이러한 이유로 필자는 Jotai로 상태 관리를 전환했습니다.
Context API는 편리하지만, 전역 상태 관리를 최적화하기 위한 도구로는 한계가 있습니다. 하지만 compound component pattern과 같이 여러 하위 컴포넌트가 상위 컴포넌트의 상태를 공유하고 동작하는 경우에는 Context API는 적절한 선택이 될 수 있습니다. 이 패턴에서는 상위 컴포넌트에서 관리하는 상태를 각 하위 컴포넌트에 자연스럽게 전달할 수 있어 props drilling을 피하고, 구조의 유연성을 유지할 수 있습니다. 보다 세밀한 전역적인 상태관리가 필요하거나 리렌더링 성능 최적화가 중요한 경우에는 Jotai와 같은 상태 관리 라이브러리를 도입하는 것이 더 나은 선택이 될 수 있다고 생각합니다.