이전 글에서 클라이언트 상태와 서버 상태를 분리하는 과정에서
Context API 대신 Recoil을 도입하게 된 이유를 정리한 바 있습니다.
이번 글에서는 해당 구조가 실제 프로젝트에서 어떻게 적용되었는지,
그리고 Recoil을 사용함으로써 성능 측면에서 어떤 개선 효과가 있었는지를 자세히 공유하려고 합니다.
왜 Recoil이 필요했는가
이전 글에서 언급했듯이 처음에는 Context API만으로도 충분해 보였습니다. 작은 단위의 상태들을 중앙에서 관리하고, 빠르게 구조화할 수 있었기 때문이다.
하지만 프로젝트가 커지고 기능이 쌓이면서, Context를 사용하는 컴포넌트가 많아지고, 하나의 값 변경에도 여러 곳에서 렌더링이 발생하기 시작했다.
특히 실시간 화상 연결 기능이 포함된 비대면 영상 연결 프로젝트에서는,
화상 연결 사용자 리스트인 remoteUsers
가 변경될 때마다 전체 UI가 리렌더링되는 문제가 있었다.
이건 단순히 구조상의 문제가 아니라 실제 사용자 경험에 영향을 주는 성능 이슈였다.
기존 구조 (Context API)
// provider.tsx
export default function Provider() {
const handleRemoteUser = (remoteUsers) => {
dispatch({
type: "SET_REMOTE_USER",
payload: remoteUsers,
});
};
}
// MediaContent.tsx
export default function MediaContent() {
const { remoteUsers } = useCall(); // useContext(CallContext)
const rtcUsers = remoteUsers.filter((user) => user.uid !== "screen");
return (
<>
{rtcUsers.map((rtcUser) => (
<>{rtcUser}</>
))}
</>
);
}
새로운 사용자가 화상 통화에 입장하면, 해당 사용자 리스트(remoteUsers)가 변경되고, 이 값을 참조하고 있는 MediaContent.tsx는 물론, 해당 Context를 구독하고 있는 다른 컴포넌트들까지 모두 리렌더링되었다.
이 상태에서 Profiler로 측정한 렌더링 시간은 다음과 같다.
Recoil로 구조 전환
이제 Recoil을 적용해 상태를 atom 단위로 쪼개고, 의존하는 컴포넌트에서만 렌더링이 발생하도록 구조를 바꿨다.
// provider.tsx
export const remoteUserState = atom({
key: "remoteUserState",
default: [],
});
// MediaContent.tsx
export default function MediaContent() {
const [remoteUsers] = useRecoilState(remoteUserState);
const rtcUsers = remoteUsers.filter((user) => user.uid !== "screen");
return (
<>
{rtcUsers.map((rtcUser) => (
<>{rtcUser}</>
))}
</>
);
}
성능 측정 결과
- 사용자 한 명이 새로 입장하면 해당 사용자의 비디오 컴포넌트를 렌더링하는 데 약 6.8ms가 소요되었다.
- 이 중 실제로 렌더링된 부분(
video
component)만 따지면 약 4.7ms 수준이었다. - Profiler 스크린샷을 비교해보면 렌더링이 시작된 최상단 컴포넌트 위치가 완전히 달라졌다는 것을 확인할 수 있었다.
- Recoil의 atom을 사용하면
MediaContent.tsx
처럼 해당 상태를 직접 구독하는 컴포넌트만 리렌더링된다. - 반면 Context API는 해당 값을 구독하고 있는 모든 컴포넌트에서 렌더링이 발생하기 때문에, 관련 없는 컴포넌트들도 리렌더링되는 문제가 있었다.
- 구조 자체가 바뀌었기 때문에, 이후에는 리렌더링 이슈를 추적하거나 성능 최적화 포인트를 찾을 때도 훨씬 수월했다.
결과적으로 Recoil을 사용하면서
렌더링 대상이 명확하게 분리되었고, 성능이 체감될 정도로 개선되었다.
마무리하며
Recoil을 도입한 이후, 렌더링 성능은 분명히 개선되었다.
특히 Context 기반 구조에서 발생하던 불필요한 리렌더링 문제 를 명확하게 줄일 수 있었고,
상태를 더 세분화해서 관리할 수 있다는 점은 유지보수나 협업 측면에서도 도움이 되었다.
다만, Recoil도 메모리 관련 이슈가 있는걸로 알고 있다…
- 상태가 쌓일수록 atom/selector 관리가 분산되고 복잡해지는 경향이 있었고,
- Recoil에서 내부적으로 사용하는 상태 트리 구조가 크거나 많아질 경우,
메모리 사용량이 증가 하거나 성능이 오히려 악화되는 사례도 일부 있었기 때문이다.
물론 이 프로젝트 규모에서는 체감할 만큼의 문제가 되진 않았지만,
다음 프로젝트에서는 이러한 부분을 고려해 요즘 많이 사용되는
보다 가볍고 명시적인 상태 관리 도구인 Zustand 도 공부해볼 방향이다..