리액트 key porp은 왜 사용할까?

2023.07.10

Table of Contents

리액트를 어느정도 학습한 분들이라면 key prop은 리스트 렌더링 할때만 사용하는 값이라고 생각할 수 있다. key prop은 조금 더 의미를 가지고 있다. 이번 글은 리액트 공식문서 스터디를 진행하며 스터디원들과 이야기했던 key에 대해 작성하려고 한다.

map과 함께 사용하는 key props

React에서는 list를 렌더링하기 위해 map 메서드와 함께 사용한다. 리턴하는 가장 바깥쪽 JSX props로 key 값을 입력하지 않으면 다음과 같은 에러가 발생한다.
Warning: Each child in a list should have a unique “key” prop.
그런데 에러가 발생하여도 우리는 화면에 렌더링된 내용을 확인할 수 있다. 이는 React는 개발자가 key를 작성하지 않으면 index를 key로 활용하기 때문이다.

리스트 렌더링에서의 key의 목적

각 컴포넌트가 어떤 배열 항목에 해당하는지 React가 식별할 수 있도록 한다. 배열의 요소 삽입, 삭제, 정렬로 인한 이동시에 React는 key값을 통해 변화를 추론하고 DOM 트리를 업데이트 하는데 도움을 된다.

index를 값으로 활용할 때의 문제점

index를 key로 활용하고 배열에 변경이 일어날 때 생기는 문제를 todo list를 통해 살펴보자
id 값이 있지만 이를 무시하고 Index로 배열을 렌더링하였다. 새로운 todo를 추가하면 배열의 맨앞에 값이 입력된다.
key - index
key - index
key - index
key - index
unique key
unique key
3번을 추가하자 input의 내용이 실제 컴포넌트를 따라가는것이 아닌 index에 따라 표시되고 있다. 배열에 요소를 삽입하여 리렌더링으로 index를 다시 매핑하게 된다. 유니크한 key값을 설정하여 예상하지 못하게 동작되는 부분을 해결할 수 있다.

index는 무조건 나쁜 것인가?

React에서는 key를 index로 입력한다고 에러를 발생시키진 않는다. 앞서 설명한 내용과 같은 문제가 발생할 수 있으나 배열의 변경 없이 렌더링만 필요한 경우에는 index로 화면을 구성해도 문제는 없다.
오히려 다른 값을 적용해야한다는 압박감에 Math.random()으로 key를 생성하는 경우가 있다. 이는 렌더링 간의 key 불일치가 발생하여 모든 컴포넌트와 DOM이 매번 다시 생성될 수 있다. 이는 성능적인 문제와 입력값 손실을 야기한다.

key로 어떤걸 사용해야 할까?

  • 데이터베이스의 데이터: 데이터 베이스에서 데이터를 가져오는 경우 포함된 id, key를 사용하자
  • 로컬에서 생성된 데이터: 데이터가 로컬에서 생성되고 유지되는 경우 crypto.randomUUID() 또는 uuid와 같은 패키지를 사용하자
지켜야할 규칙으로는
  • key는 형제간(특정 배열 렌더링을 위한 컴포넌트들) 고유해야 한다. 하지만 다른 배열과 JSX 노드에 동일한 key를 사용해도 된다.
  • key는 변경되어서는 안된다.

key를 활용한 state 초기화/보존

이 내용을 정리하기 위해 글을 작성하기 시작하였는데 서론이 길어졌다. React는 어떤 방식으로 컴포넌트의 변경을 식별하고 state 초기화와 보존과 연관되어있는지가 이 글의 핵심이 될 것 같다.

State 보존과 초기화

React는 UI 트리에서의 위치를 통해 State가 어떤 컴포넌트에 속하는지 추적한다. 다음 그림을 통해 React가 컴포넌트들을 통해 UI 트리를 만들고 React DOM을 통해 브라우저의 DOM과 일치하도록 업데이트합니다.
notion image
우리는 각 컴포넌트에 정의된 state가 컴포넌트마다 정확하게 매핑되어 있다고 생각할 수 있지만 state 또한 React 내부에 존재한다. React는 컴포넌트의 UI 트리 위치를 이용해 각 state를 매핑한다.
위 내용을 토대로 코드를 살펴보자
import { useState } from 'react'; export default function App() { const [isMe, setIsMe] = useState(true) return ( <div> {isMe ? <Counter name='me' /> : <Counter name='someone' />} <button onClick={() => setIsMe(prev => !prev)}>바꾸기</button> </div> ); } function Counter({ name }) { const [count, setCount] = useState(0) return ( <div> <h1>{count}</h1> <button onClick={() => setCount(count + 1)}> +1 </button> </div> ) }
버튼을 누르면 count가 1씩 증가하는 컴포넌트와 me와 someone을 통해 다른 컴포넌트로 교체되도록 구현하였다. 실제로 작동하면 아래와 같은 문제가 발생하게 된다.
notion image
notion image
분명 내용은 Someone 컴포넌트로 교체되었는데 count state는 그대로 유지되고 있다. 우리는 컴포넌트가 교체되었다고 생각하고 이는 틀린 내용은 아니다. 하지만 UI 트리에서 React는 변화가 없다고 판단하여 상태값을 보존하고 있다.
우리가 생각하는 방식으로 state를 초기화시켜 각 사람의 count를 따로 다루고 싶다면 UI 트리에 변동이 일어나야 한다. 아래 3가지 방법이 있다.
... //방법1 - 같은 위치에 다른 컴포넌트 렌더링 <div> {isMe ? ( <div> <Counter name="me" /> </div> ) : ( <section> <Counter name="someone" /> </section> )} <button onClick={() => setIsMe((prev) => !prev)}></button> </div> //방법2 - 다른 위치에 컴포넌트 렌더링 <div> {isMe && <Counter name="me" /> } {!isMe && <Counter person="someone" /> } <button onClick={() => setIsMe((prev) => !prev)}></button> </div> //방법3 - key를 이용해 state 초기화 <div> {isMe ? <Counter key='me' name='me' /> : <Counter key='someone' name='someone' />} <button onClick={() => setIsMe(prev => !prev)}>바꾸기</button> </div>
이러한 UI 트리를 잘 활용하여 상태값을 보존하는 장점으로 활용할 수 도 있다. 실제로 검색바의 state가 초기화 되는 문제를 해결하기 위해 같은 위치에 검색바를 렌더링하여 문제를 해결한 적이 있다.

마치며

두가지를 나눠서 설명하였지만 "key는 React가 각 컴포넌트의 변화를 추적할 수 있도록 한다.”로 정리할 수 있겠다. 리액트 공식문서 스터디를 진행하며 이야기를 나눴던 실제로 경험한 문제를 주로 정리하려고 하였으나 공식 문서 정리가 된 것 같은 느낌이 든다.😅 한국어로 번역이 활발하게 되고 있으니 공식문서를 천천히 읽어보는 것도 추천드리는 바이다.

Prev
웹 사이트 성능 측정 지표
Next
브라우저 렌더링 과정 (리플로우와 리페인트)