최근 작업 중에 재밌는 인사이트를 얻어서 정리해두려고 한다.
key={index}
가 성능상 더 좋은 경우를 실제로 마주쳤고, 그 배경과 이유를 남겨본다.
예시: 리더보드 UI
{currentLeaderboard.top_ranks.map((rank, index) => (
<LeaderboardRow
key={index} // or rank.user_id
rank={rank.rank}
avatar={rank.avatar as AvatarInterface}
mastery={rank.score}
nickname={rank.nickname}
isOwn={rank.user_id === currentLeaderboard.my_rank.user_id}
/>
))}
원래대로라면 key
는 고유 식별자(user_id 같은)를 써야 한다.
그런데 특정 컴포넌트에서 이게 오히려 성능 저하로 이어졌고, index가 더 나았다.
리렌더링 vs 마운트
리액트는 리스트의 key를 기반으로 다음을 판단한다:
- key가 다르면: 이전 컴포넌트를 언마운트하고 새로 마운트
- key가 같으면: 기존 컴포넌트 유지, prop만 바뀌면 리렌더링
일반적으로는 리렌더링과 마운트의 연산 비용은 크게 차이가 없다.
문제의 포인트: Rive + Wasm 컴포넌트
<LeaderboardRow />
안에서는 Rive를 이용한 애니메이션 컴포넌트가 있는데,
Rive는 마운트될 때 Wasm 컴파일이 일어나는 무거운 연산이 발생한다.
useEffect(() => {
if (!rive) {
const r = new Rive({ ...riveParams, canvas: canvasElem });
r.on(EventType.Load, () => setRive(r));
}
return () => {
if (!isLoaded) r?.cleanup();
};
}, [canvasElem, isParamsLoaded, rive]);
즉, 컴포넌트가 새로 마운트되면 Wasm이 다시 컴파일된다.
이게 느려지는 원인이다.
key 비교 결과
key = user_id (매번 새로 마운트됨)
→ 탭 전환 시 약간의 딜레이 발생
→ Wasm 다시 컴파일
key = index (마운트 유지됨)
→ 바로 전환됨
→ Wasm 재사용 가능
결론
보통은 key={index}
를 쓰면 안 된다.
하지만 렌더링 비용이 마운트에 집중되는 구조라면,
index key로 리렌더링만 유도하는 게 더 나은 선택일 수 있다.
이번 경험으로 key가 단순히 "경고 없애기용"이 아니라
렌더링 전략을 직접 제어할 수 있는 수단이라는 걸 체감했다.