2024-02-17
React key와 memo, re-render
들어가며
흔히 "React에서 List에서 key는 unique한 값을 넣어야 한다." 라고 하는데 왤까?
궁금해서 React 공식문서에서는 아래와 같이 설명한다.
데스크톱의 파일에 이름이 없다고 상상해 봅시다. 파일 이름 대신 첫 번째 파일, 두 번째 파일 등의 순서로 파일을 참조할 것입니다. 물론 익숙해질 수도 있지만, 파일을 삭제하면 혼란스러워질 수도 있습니다. 두 번째 파일이 첫 번째 파일이 되고, 세 번째 파일이 두 번째 파일이 되는 식으로 말이죠._
폴더의 파일 이름과 배열의 JSX key는 비슷한 역할을 합니다. key를 사용하면 형제 항목 사이에서 특정 항목을 고유하게 식별할 수 있습니다. 잘 선택한 key는 배열 내 위치보다 더 많은 정보를 제공합니다. 만약 재정렬로 인해 어떤 항목의 위치가 변경되더라도, 해당 항목이 사라지지 않는 한, React는
key
를 통해 그 항목을 식별할 수 있습니다._
그런데 나는 텍스트만으로는 와닿지 않아서 직접 확인해보려 한다.
왜 key를 써야하는지 직접 확인해보기
Todo App을 간단하게 만들어서 key에 unique한 값을 써보는 것(1)과 index를 사용해보는 것(2)로 테스트를 해보았다.
1let id = 1 2 3export default function UseIndexForKey() { 4 const [todos, setTodos] = useState([]) 5 6 const handleAdd = (text, id) => { 7 setTodos([...todos, { text, id }]) 8 } 9 10 const handleDel = useCallback((targetId) => { 11 setTodos((prev) => { 12 return prev.filter(({ id }) => id !== targetId) 13 }) 14 }, []) 15 16 const shuffle = () => { 17 const newTodos = todos.slice() 18 for (let i = newTodos.length - 1; i > 0; i--) { 19 const j = Math.floor(Math.random() * (i + 1)) 20 ;[newTodos[i], newTodos[j]] = [newTodos[j], newTodos[i]] 21 } 22 setTodos(newTodos) 23 } 24 25 return ( 26 <> 27 <ul> 28 {todos.map((todo, idx) => ( 29 <Item key={idx} todo={todo} del={handleDel} /> 30 ))} 31 </ul> 32 <AddForm add={handleAdd} /> 33 <button onClick={() => shuffle()}>shuffle</button> 34 </> 35 ) 36} 37 38function AddForm({ add }) { 39 const [text, setText] = useState('') 40 return ( 41 <form> 42 <input 43 type="text" 44 value={text} 45 onChange={(e) => setText(e.target.value)} 46 /> 47 <button 48 onClick={(e) => { 49 e.preventDefault() 50 add(text, id++) 51 setText('') 52 }} 53 > 54 Add 55 </button> 56 </form> 57 ) 58} 59 60const Item = memo( 61 ({ todo, del }) => { 62 const [text, setText] = useState(todo.text) 63 console.log(`${todo.text} rendered`) 64 return ( 65 <li> 66 <span>{todo.text}</span> 67 <input 68 type="text" 69 value={text} 70 onChange={(e) => setText(e.target.value)} 71 /> 72 <button onClick={() => del(todo.id)}>Delete</button> 73 </li> 74 ) 75 }, 76 (a, b) => { 77 console.log(a, b) 78 return Object.is(a, b) 79 } 80)
위 코드를 실행해보면 대략 아래와 같은 앱이 구동된다.
간단히 앱을 설명하자면
- Todo Item을 추가했을 때 입력된 값을 라벨 및 Input의 초기 데이터로 가진다.
- 각 Item은 삭제 가능하다
shuffle
버튼을 클릭 시 순서를 뒤섞는다
1. key에 id 사용
가운데에 있는 라인을 삭제 하거나 shuffle
클릭 시 어떻게 될까?
정상적으로 라벨과 Input의 값이 일치하는 걸 볼 수 있다.
그러면 key에 index를 사용한다면 어떻게 될까?
2. key에 index 사용
key에 index를 넣고 두번째에 있는 카카오를 삭제해보자.
shuffle
을 클릭해보자
그러면 라벨과 Input의 데이터가 꼬인 것을 볼 수 있다.
왜그럴까?
확인해보고자 Item 컴포넌트에 memo
를 사용해봤다. memo
를 사용하면 리-렌더링을 하기전에 이전 props와 다음 props를 확인해볼 수 있다.
위 사진을 보면 라인
만이 리-렌더링이 된 것을 볼 수 있다.
idx: 0(카카오) → idx: 0(카카오) prop이 같으니 리-렌더링 패스
idx: 1(네이버) → idx: 1(라인) 어 얘는 prop이 바뀌었네? 리렌더링하자!
prop이 바뀌고 나서 prop에서 꺼내온 값들(라인)은 잘 바뀌었는데
input에 있는 데이터는 안바뀌었다. 왜냐 State는 리-렌더링이 되어도 바뀌지 않기 때문이다.
이런 이유로 key에 index를 사용하고나서, 중간에 아이템을 삭제하거나, 리-오더를 해버리면 이슈가 생긴다.
번외: key에 Math.random()을 넣으면…?
그래도 key에 index를 사용하고 memo
를 사용하면 Todo
를 추가 할 때 이미 생성된 Todo
의 리-렌더링이 발생하지 않는다.
그런데 key
에 Math.random()
을 사용하면? 이미 추가된 Todo
도 리-렌더링이 된다.(OMG)
또 신기했던게 memo
를 사용해서 이전, 다음 props
를 확인해보려했으나 아예 콜백이 수행되지도 않았다. 그렇다면 memo에 도달하기 전에 key
값으로 검사하는 것 같다!
- 리-렌더링 시
key
가 같음 →memo
사용했으면 props 비교해보고 리-렌더링 결정해줄게~ - 리-렌더링 시
key
가 다름 →memo
고 뭐고 너는 바로 리-렌더링이야
요약
key
는 유니크한 값으로 넣자!key
가 다르면 아예memo
최적화도 못해요!