Skip to main content

리액트에서 key에 index를 넣으면 안 되는 ‘진짜’ 이유

About 2 minNode.jsReact.jsArticle(s)blogyozm.wishket.comnodenodejsnode-jsreactreactjsreact-js

리액트에서 key에 index를 넣으면 안 되는 ‘진짜’ 이유 관련

React.js > Article(s)

Article(s)

리액트에서 key에 index를 넣으면 안 되는 ‘진짜’ 이유 (1) | 요즘IT
리액트에서 key에 index를 넣으면 성능 저하가 발생할 수 있으므로 사용하지 않는 것이 좋다고 합니다. 즉석에서 값을 생성해 부여하는 것 역시 안 된다고 하네요. index가 안 되는 건 그렇다 하더라도, 즉석에서 값을 생성하는 것은 왜 안 되는 걸까요? 애초에 key 프로퍼티의 역할은 무엇일까요? 단순히 해결법만 알아 보고 끝내는 것이 아닌, 어떤 리액트 동작 원리에 따라, 무슨 이유로 key에 index를 넣으면 안 되는지, 이를 더 자세히 알아보고자 합니다.
<출처: 리액트와 열쇠 이미지, <FontIcon icon="fas fa-globe"/>maan-icons>
<출처: 리액트와 열쇠 이미지, maan-iconsopen in new window>

리액트(React)로 개발할 때, 배열 형태 데이터를 반복시켜 리스트와 같은 컴포넌트를 만드는 것은 흔한 일입니다. 이 경우 API 요청으로 리스트 형식의 데이터를 불러와 map 등 반복문 함수를 사용해 컴포넌트로 반환하고는 하는데요. 만약 eslint를 설정해 두었다면, 이 과정에서 아래 경고를 맞닥뜨릴 수 있습니다.

‘Missing “key” prop for element in iterator’, 반복적인 작업에서 엘리먼트에 key 프로퍼티를 전달하지 않았다는 의미입니다. 리액트 개발을 시작한 지 얼마 안 된 주니어 개발자라면, 이때 가장 비슷해 보이는 배열 index 값을 key에 부여할 수도 있겠습니다. 하지만 그럼 또 다른 경고가 나타날 겁니다.

이번 경고는 ‘Do not use Array index in keys’, 배열의 index 값을 key에 넣지 말라는 내용입니다. 왜 이런 경고 문구가 뜨는 걸까요? key에 index 값을 부여하면 무슨 문제가 생기는 걸까요?


key에 index를 부여하면 안 되는 이유

key에 index 값을 부여하면 안 되는 이유는 검색으로 쉽게 찾을 수 있습니다. 대다수가 이를 리액트의 성능과 관련된 문제라고 설명합니다. key에 index 값을 부여하면 배열 데이터가 변경(삽입, 삭제, 재정렬)됐을 때 성능이 떨어질 수 있다는 것이죠.

<출처: <FontIcon icon="fa-brands fa-react"/>react.dev>
<출처: react.devopen in new window>

위에서 보았던 eslint 경고 이외, 리액트 공식 문서에서도 key에 index 값을 넣지 않는 것을 권장하고 있습니다. 그만큼 중요한 부분으로 보입니다. 문서에서 언급한 내용을 간단히 정리해 보면 아래와 같습니다.

  • 항목을 삽입하거나 삭제, 재정렬 시 렌더링 순서가 시간이 지남에 따라 달라집니다.
  • index 값 이외에 즉석에서 값을 생성하는 경우도 비슷합니다. (예. key={랜덤 값 반환 함수})
  • key는 실제로 컴포넌트에 전달되지 않고 리액트에서만 사용합니다.

이유를 알았으니 이제 앞으로는 key에 index 값을 넣지 않으면 되겠네요! 궁금증을 모두 해소했습니다.


끝인가요?

정말 모든 궁금증이 다 풀렸나요?

다시 한번 내용을 되새겨 보겠습니다. key에 index를 넣으면 성능 저하가 발생할 수 있으므로 사용하지 않는 것이 좋다고 합니다. 즉석에서 값을 생성해 부여하는 것 역시 안 된다고 하네요. 따라서 고유한 값을 미리 생성해 부여하거나, 데이터 자체의 고유한 값(id 값 등)을 key에 넣으면 되겠습니다.

여기까지 알아보자 이런 궁금증이 생겼습니다. index가 안 되는 건 그렇다 하더라도, 즉석에서 값을 생성하는 것은 왜 안 되는 걸까요? 이 두 방법은 어떤 공통점이 있어 문제를 발생시키는 걸까요? 애초에 key 프로퍼티의 역할은 무엇일까요?

따라서 이번 글에서는 단순히 해결법만 알아 보고 끝내는 것이 아닌, 어떤 리액트 동작 원리에 따라, 무슨 이유로 key에 index를 넣으면 안 되는지, 이를 더 자세히 알아보고자 합니다.


리액트는 어떻게 동작할까요?

이 주제에 대해 살펴보려면 우선 리액트가 어떻게 동작하는지부터 알아야 합니다. 리액트는 우리의 생각보다 훨씬 복잡하고 방대합니다. 이를 전부 다룰 수는 없으니 이번 주제를 이해하기 위한 내용만 짚고 넘어가 보겠습니다.

1. 가상 돔(Virtual DOM)

리액트는 렌더링할 때 메모리상의 Virtual DOM(이하: 가상 돔)을 활용합니다. 데이터의 변경이 발생했을 때 바로 실제 돔을 조작하는 것이 아닌, 가상 돔으로 변경 사항을 먼저 확인하는 작업을 거치는 거죠.

실제 돔을 바로 조작하지 않는 이유는 무엇일까요? 성능 최적화 때문입니다. 일반적으로 실제 돔을 조작하는 것은 큰 비용을 일으킵니다. 돔이 변경되면 브라우저는 리플로우(Reflow) 및 리페인트(Repaint)를 포함한 일련의 과정을 진행하는데요. 데이터가 변경될 때마다 전체 돔에 대해 이 과정을 거치려면 큰 비용이 들 것입니다.

따라서 가상 돔으로 변경 사항을 먼저 확인하고, 실제 변경이 필요한 부분만 이를 진행하며 불필요한 비용이 들어가지 않도록 한 것입니다. 방금 설명한 과정을 리액트에서는 재조정(Reconciliation)이라고 부릅니다.

2. 재조정(Reconciliation)

앞서 말한 재조정 과정을 예시 코드로 자세히 알아보겠습니다. 아래는 간단한 숫자 카운트 코드입니다.

예제 코드 <출처: 작가>
예제 코드 <출처: 작가>
구현 결과 <출처: 작가>
구현 결과 <출처: 작가>

처음 리액트가 구동될 때는 실제 돔을 기반으로 가상 돔 객체를 만들어냅니다. 그림으로 보면 아래와 같습니다.

이때 가상 돔은 current와 workInProgress라는 이름의 더블 버퍼링(Double Buffering) 형태로 구성됩니다. 간단히 말하면 current는 현재까지의 가상 돔, workInProgress는 데이터 변경이 발생했을 때, 이를 반영한 가상 돔이라고 볼 수 있습니다.

만약 앞서 예제에서 1 더하기 버튼을 누르면, 가상 돔은 1을 더하기 전의 current 가상 돔 객체와 더한 후의 workInProgress 가상 돔 객체로 구성됩니다.

workInProgress에 모든 변경 사항이 반영되면, current와 workInProgress을 비교하여 실제 돔에 반영할 리스트인 이펙트 리스트(Effect List)를 수집할 수 있습니다. 곧 이 리스트의 요소만 실제 돔에 반영해 우리 화면에 변화가 나타납니다. current를 참조하는 트리는 workInProgress로 바뀝니다.

이때는 가상 돔을 활용해 변경된 부분만 확인하여 렌더링을 진행하기 때문에, 만약 다른 컴포넌트가 있더라도 이는 다시 렌더링하지 않습니다.


중간 정리

지금까지 내용을 정리하면 아래와 같습니다.

  • 리액트는 메모리상의 가상 돔을 사용하여 렌더링을 진행합니다.
  • 현재 버전과 바뀐 버전의 가상 돔을 비교하여 어떤 것을 렌더링할지 결정하는 방식(재조정[1])입니다.
  • 변경된 곳을 제외한 부분은 다시 렌더링하지 않으며 성능을 최적화합니다.

key는 어떤 역할을 할까요?

1. 가상 돔의 key prop

이제 처음 의문을 가졌던 key에 대해서 알아보겠습니다. 리액트는 가상 돔 객체를 메모리상에 생성한다고 했습니다. 리액트 16 이상 버전을 기준으로 Fiber Node라고 불리는 이 객체는 아래와 같은 구조를 가집니다. 편의상 주제와 관련된 정보만 정리했습니다.

<출처: React>
<출처: Reactopen in new window>

2. 재조정 과정에서 key의 역할

가상 돔 객체는 트리 구조를 가지고 있습니다. 위에서 본 FiberNode가 해당 트리의 개별 노드입니다. 리액트 공식 문서에 따르면 두 개 트리를 비교할 때, 최첨단 알고리즘open in new window을 사용하더라도 n개의 엘리먼트에 대해 O(n3)\text{O}\left(n^3\right)의 복잡도를 가진다고 합니다. 1000개의 엘리먼트를 그리기 위해서는 10억 번의 비교 연산을 진행해야 한다는 뜻이죠. 이는 굉장히 비싼 연산입니다. 따라서 다른 해결법이 필요했고, 리액트는 두 가지 가정을 도입합니다.

  1. 타입이 다른 두 엘리먼트는 서로 다른 트리를 만들어 낸다.
  2. 개발자가 key prop을 전달해 어떤 자식 엘리먼트가 변경되지 말아야 할지 표시해 준다.

1번 가정에서 말하는 타입은 엘리먼트의 타입을 의미합니다. 이를테면 <div><span>으로 바뀌었거나, <Calculator><ToDoList>로 달라진 경우 해당 트리 전체를 비교한다는 것을 의미합니다. 만약 타입이 바뀌지 않는다면 변경된 속성(property)만 갱신합니다.

다음 2번 가정에서는 key의 역할을 설명하고 있습니다. 개발자가 key prop을 전달하여 자식 엘리먼트를 리렌더링하지 않도록 만든다는 것인데요. 다르게 말하면, key prop이 바뀌면 실제로 데이터가 변경되지 않았더라도 이를 변경으로 간주하고 리렌더링을 진행한다는 말입니다. 예시를 보며 더 자세히 알아보겠습니다.

위와 같은 구조의 리스트가 있다고 가정해 봅시다. 만약 이 리스트의 마지막에 <li>홍길동</li>을 추가하면 어떻게 될까요? 다행히도 <li>황주현</li><li>himprover</li>는 바뀌지 않고, <li>홍길동</li>만 추가될 겁니다.

그렇다면 가장 상단에 <li>홍길동</li>이 생기면 어떨까요? 실제로 <li>황주현</li><li>himprover</li>는 값이 바뀌지 않았지만, 해당 컴포넌트 모두를 다시 연산해야 합니다. 이미 연산한 값을 이동만 하면 되는데도 말이죠.

리액트는 이런 상황을 개발자가 방지해 주기 바랐습니다. key를 사용함으로써 말이죠. 그래서 2번 가정에 따라 key 값이 변하지 않는다면 연산을 다시 진행하지 않고, 이미 있는 값을 재활용하도록 했습니다. 이런 방식으로 불필요한 연산을 방지하며 성능 최적화를 끌어낸 것이죠. 예시에서는 간단한 <li>였지만, 실제로 많은 정보를 가지는 컴포넌트라면 이 방법이 성능에 큰 영향을 줄 수 있습니다.


index를 넣거나 즉석에서 값을 생성하면 어떻게 될까요?

드디어 key가 왜 탄생했고, 어떤 역할을 하는지 알게 되었습니다. 이제 우리는 여기에 index를 넣거나 즉석에서 값을 생성했을 때, 어떤 문제가 생길지 이해할 준비를 마쳤습니다.

nbsp;**1. key에 배열 index를 전달했을 때

자바스크립트의 배열에서 index 값은 말 그대로 각 요소의 순서 번호입니다. 따라서 배열의 끝이 아닌 중간이나 처음에 값을 더하거나 삭제하면, 모든 index 값이 달라집니다. 이는 곧 index가 바뀐 리스트로 다시 연산이 이뤄져야 함을 의미합니다.

2. key에 즉석에서 생성한 값을 전달했을 때

즉석에서 생성한 랜덤 값을 전달하는 것은 index를 사용하는 것보다 더욱 비효율적입니다. 사실상 매번 모든 연산을 다시 해야 하니까요. 따라서 key 값으로 즉석에서 생성한 값을 전달하는 것은 지양해야 합니다.


최종 정리

여기까지 같이 오느라 고생하셨습니다. 지금까지 알아본 내용을 정리하면 아래와 같습니다.

  • 리액트는 key prop으로 해당 리스트 아이템을 다시 연산할지 말지 결정합니다.
  • key prop에 index 값을 넣을 경우, 배열 순서가 바뀔 때마다 다시 연산하며 불필요한 비용이 발생합니다.
  • key prop에 즉석에서 생성한 랜덤 값을 넣을 경우, 매번 key prop이 달라지며 연산도 다시 이뤄집니다.
  • 따라서 key prop에는 해당 요소의 고유값(예: 회원 uuid 등)을 전달해 불필요한 연산을 방지해야 합니다.

사실 리액트는 개발자가 key prop을 전달하지 않으면, 자동으로 index 값을 부여합니다. 따라서 key prop에 아무 값을 전달하지 않으면 index 값을 전달하는 것과 같은 결과가 만들어집니다.

또한, 그렇다고 무조건 index 값을 사용하면 안 되는 것은 아닙니다. 배열의 요소가 추가/삭제되거나 순서가 달라지지 않는 고정된 데이터라면 index 값을 사용해도 무방합니다. 요소와 순서가 변하지 않는다는 것은 index 값이 고정된다는 의미를 지니기 때문입니다.


마치며

리액트에서 key에 index를 넣으면 안 되는 이유를 이해하기 위해 이렇게 많은 정보를 공부해야 했습니다. 물론 단순히 ‘key에 index를 넣으면 최적화가 안 된다’로 결론 내렸다면 이처럼 오랜 시간이 걸리지는 않았을 겁니다. 그러나 ‘최적화가 안 된다’라는 사실만 아는 사람과 ‘리액트의 동작 원리와 key 값의 역할에 기인하여, key에 index를 넣으면 성능 저하를 일으킬 수 있다’라는 사실을 이해하는 사람은 확실히 다르겠죠.

주니어 개발자일수록 어떤 문제에 봉착했을 때, 조급하게 그 문제를 해결하는 데에만 급급한 경향을 가지고는 합니다. 그러나 단순히 문제만 해결하고 끝내는 것이 아닌, 문제의 배경을 이해하고 원리를 알아보는 건 어떨까요? 인정받는 개발자가 되는 첫걸음으로 말입니다.



  1. 리액트의 재조정(Reconciliation) 과정에서는 지금까지 소개한 것보다 훨씬 많은 일이 벌어집니다. 관심이 있다면 React Reconciliation 키워드를 검색해 보세요! ↩︎