리액트에서 `key={index}`가 더 빠른 경우가 실제로 존재할까?

March 12, 2025

최근 작업 중에 재밌는 인사이트를 얻어서 정리해두려고 한다.
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 (매번 새로 마운트됨)

user_id

→ 탭 전환 시 약간의 딜레이 발생
→ Wasm 다시 컴파일


key = index (마운트 유지됨)

index

→ 바로 전환됨
→ Wasm 재사용 가능


결론

보통은 key={index}를 쓰면 안 된다.
하지만 렌더링 비용이 마운트에 집중되는 구조라면,
index key로 리렌더링만 유도하는 게 더 나은 선택일 수 있다.

이번 경험으로 key가 단순히 "경고 없애기용"이 아니라
렌더링 전략을 직접 제어할 수 있는 수단이라는 걸 체감했다.