리액트를 어느 정도 학습한 분들이라면 key prop에 대해 들어보았을 것이고 이것이 리스트 렌더링 할 때만 사용하는 값이라고 생각할 수 있다. key prop은 조금 더 의미를 가지고 있다. 이번 글은 리액트 공식문서 스터디를 진행하며 스터디원들과 이야기했던 key에 대해 작성하려고 한다.
map과 함께 사용하는 key prop
React에서는 list를 렌더링하기 위해 map 메서드와 함께 사용한다. 리턴하는 가장 바깥쪽 JSX props로 key 값을 입력하지 않으면 다음과 같은 에러가 발생한다.
Warning: Each child in a list should have a unique “key” prop.
그런데 에러가 발생하여도 우리는 화면에 렌더링된 내용을 확인할 수 있다. 이는 React는 개발자가 key를 작성하지 않으면 index를 key로 활용하기 때문이다.
리스트 렌더링에서의 key의 목적
key는 React가 어떤 항목을 변경, 추가 또는 삭제할지 식별하는 것을 돕는다. 배열의 요소 삽입, 삭제, 정렬로 인한 이동시에 React는 key값을 통해 변화를 추론하고 DOM 트리를 업데이트하는데 도움이 된다.
index를 값으로 활용할 때의 문제점
index를 key로 활용하고 배열에 변경이 일어날 때 생기는 문제를 todo list를 통해 살펴보자, id 값이 있지만 이를 무시하고 Index로 배열을 렌더링하였다. 새로운 todo를 추가하면 배열의 맨 앞에 값이 입력된다.
3번을 추가하자 input의 내용이 실제 컴포넌트를 따라가는것이 아닌 index에 따라 표시되고 있다. 배열에 요소를 삽입하여 리렌더링으로 index를 다시 매핑하게 된다. 유니크한 key값을 설정하여 예상하지 못하게 동작되는 부분을 해결할 수 있다.
다른 또 한가지 문제는 정확한 요소에 접근하고 작업이 이뤄지지 않고 자바스크립트에서 배열을 처리하는 것과 같이 업데이트가 일어나면 배열을 밀고 당기듯 불필요한 리렌더링이 일어난다는 점이다.
index key는 무조건 나쁜 것인가?
React에서는 key를 index로 입력한다고 에러를 발생시키진 않는다. 앞서 설명한 내용과 같은 문제가 발생할 수 있으나 배열의 변경 없이 렌더링만 필요한 경우에는 index로 화면을 구성해도 문제는 없다.
오히려 다른 값을 적용해야 한다는 압박감에 Math.random()으로 key를 생성하는 경우가 있다. 이는 렌더링 간의 key 불일치가 발생하여 모든 컴포넌트와 DOM이 매번 다시 생성될 수 있다. 이는 성능적인 문제와 입력값 손실을 야기한다.
key로 어떤걸 사용해야 할까?
- 데이터베이스의 데이터: 데이터 베이스에서 데이터를 가져오는 경우 포함된 id, key를 사용하자
- 로컬에서 생성된 데이터: 데이터가 로컬에서 생성되고 유지되는 경우 crypto.randomUUID() 또는 uuid와 같은 패키지를 사용하자
지켜야 할 규칙으로는
- key는 형제간(특정 배열 렌더링을 위한 컴포넌트들) 고유해야 한다. 하지만 다른 배열과 JSX 노드에 동일한 key를 사용해도 된다.
- key는 변경되어서는 안 된다.
key를 활용한 state 보존/초기화
이 내용을 정리하기 위해 글을 작성하기 시작하였는데 서론이 길어졌다. React는 어떤 방식으로 컴포넌트의 변경을 식별하고 state 초기화와 보존과 연관되어 있는지가 이 글의 핵심이 될 것 같다.
React의 State 보존과 초기화 방법
React는 UI 트리에서의 위치를 통해 State가 어떤 컴포넌트에 속하는지 추적한다. 다음 그림을 통해 React가 컴포넌트들을 통해 UI 트리를 만들고 React DOM을 통해 브라우저의 DOM과 일치하도록 업데이트합니다.
우리는 각 컴포넌트에 정의된 state가 컴포넌트마다 정확하게 매핑되어 있다고 생각할 수 있지만 state 또한 React 내부에 존재한다.
즉 React는 컴포넌트 UI 트리 위치를 이용해 각 state를 매핑한다.
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>
<h3>{name}</h3>
<button onClick={() => setCount(count + 1)}>
+1
</button>
</div>
)
}
버튼을 누르면 count가 1씩 증가하는 컴포넌트와 me와 someone을 통해 다른 컴포넌트로 교체되도록 구현하였다. 실제로 작동하면 아래와 같은 문제가 발생하게 된다.
분명 내용은 Someone 컴포넌트로 교체되었는데 count state는 그대로 유지되고 있다. 우리는 컴포넌트가 교체되었다고 생각하고 있고 이는 틀린 내용은 아니다. 하지만 React가 변화를 추적하는 과정에서 UI 트리에서 변화가 없다고 판단하여 상태값을 보존하고 있다.
해결 방법
우리가 생각하는 방식으로 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가 초기화되는 문제를 해결하기 위해 같은 위치에 검색바를 렌더링하여 문제를 해결한 적이 있다.
기존 각 페이지마다 헤더 컴포넌트를 작성한 경우에는 UI 트리의 변경으로 검색어와 옵션이 초기화되었다. 페이지 이동이 일어나더라도 헤더의 UI 트리는 변화하지 않도록 하기 위해 React-router v6의 Nested Routes를 활용해 쉽게 리팩토링을 진행할 수 있었다.
마치며
두 가지를 나눠서 설명하였지만 "key는 React가 각 컴포넌트의 변화를 추적할 수 있도록 한다.”로 정리할 수 있겠다. 또한 key 값으로 Index 보다는 기능 추가와 유지보수를 위해 unique한 key 값을 활용하는 것이 좋다. 리액트 공식문서 스터디를 진행하며 이야기를 나눴던 실제로 state가 초기화되거나 보존되는 직접 경험한 문제를 주로 정리하려고 하였으나 공식 문서 정리가 된 것 같은 느낌이 든다.😅
참조
'React' 카테고리의 다른 글
리액트 input 태그 상태 관리법 - React (0) | 2022.08.02 |
---|---|
동적인 값 관리 useState - React (0) | 2022.07.01 |
리액트의 값 전달 방식 Props - React (0) | 2022.06.20 |
리액트 코드 작성 방식 JSX - React (0) | 2022.06.01 |
리액트란, 작동 원리, 개발 환경 구축 - React (0) | 2022.05.09 |