thumbnail

리액트 재조정과 올바른 key!

생성일2023. 5. 15.
태그
작성자지한솔

안녕하세요! 오늘은 리액트 재조정 (react-reconciliation)에 대해 포스팅을 작성하려고 합니다.
사실 우리 모두 이미 알고있는 사실일겁니다. 하지만 용어적 풀이, 기능적 설명에 대해 많이 부족할 거라 생각합니다. 저 또한 그랬으니까요.
리액트 재조정이란 무엇일까요? 공식문서에서는
  • React는 선언적 API를 제공하기 때문에 갱신이 될 때마다 매번 무엇이 바뀌었는지를 걱정할 필요가 없습니다.
  • React 내부에서 어떤 일이 일어나고 있는지는 명확히 눈에 보이지 않습니다.
  • diffing(비교) 알고리즘을 만들때 어떤 선택을 했는지, 이 알고리즘 덕분에 컴포넌트의 갱식이 예측 가능해지면서 고성능앱이 될 수 있습니다.
라고 설명하고 있습니다. 재조정에 대한 설명보단 재조정이 어떤 효과를 불러일으키고, 어떤 알고리즘을 사용했는지에 대해 설명해주고 있습니다. 재조정은 공식문서에서 말한것처럼 “매번 무엇이 바뀌었는지를 걱정할 필요가 없습니다.” 가 전부입니다.
이미 모두가 알고 있을 수 있다는거죠, 명확히 눈에 보이지 않고, 무엇이 바뀌는지 걱정할 필요가 없었으니 말이죠 하지만 그게 어떻게 동작하고, 어떻게 활용해야하는지에 대해선 알아야 할 필요가 있습니다.

비교 알고리즘 (Diffing Algorithm)

자 그럼 그렇게 말하는 비교 알고리즘은 어떤걸까요?
React는 두 개의 트리를 비교할 때, 두 엘리먼트의 루트(root) 엘리먼트 부터 비교를 진행합니다. 이후의 동작은 이 루트 엘리먼트의 타입에 따라 달리지게 됩니다.

엘리먼트의 타입이 다른 경우

우선 React는 트리를 버릴 때 이전의 DOM 노드들을 모두 파괴시킵니다. 따라서 이전 트리와 연관된 모든 state는 사라지게 됩니다.
그럼 트리를 버리게되는 경우는 어떠한 경우일까요? 바로 React에서 두 엘리먼트가 다른 상태입니다. 예를들어 <span>이 <a> 태그로 변경되고나, <Article>이 <Section>으로 바뀌는 경우가 그에 해당됩니다. 이렇게 엘리먼트가 바뀌면 React는 이전 트리와 state를 버리고 새로 트리를 구축합니다.
// div 태그 <div> <Count /> </div> // span 태그 <span> <Count /> </span>
위 처럼 div에서 span으로 상위 엘리먼트가 변경됨에 따라 count는 자동으로 버려지고 새로 구축됩니다. 하위에 있는 모든 state는 지워지기 때문입니다.
만약 Count컴포넌트에 local state값이 10일때 div → span 으로 바뀌면 local state는 다시 초기값을 띄게 될 것입니다.

엘리먼트의 타입이 같은 경우

엘리먼트의 타입이 같은 경우 ㄲeact는 노드를 파괴하지 않고 두 엘리먼트의 속성을 확인하여 동일한 내용은 유지, 변경된 속성만 갱신을 진행합니다.
// color: black <div style={{color: 'black', fontWeight: 'bold'}}> <Count /> </div> // color: white <div style={{color: 'white', fontWeight: 'bold'}}> <Count /> </div>
위 처럼 div 태그의 style 속성만 color: black에서 white 로 변경되었습니다. React는 동일한 속성인 fontWeight는 수정하지 않고 color 속성만 수정하게 됩니다.
DOM 노드의 처리가 끝나면 React는 해당 노드의 자식들을 재귀적으로 처리할 것입니다.

자식 노드 재귀 방법

DOM 노드의 자식들을 재귀처리할때 React는 두 리스트를 순회하고 차이점이 있으면 변경을 진행합니다. 예로 비교하는 리스트 엘리먼트 끝에 새로운 엘리먼트를 추가한다면 두 트리의 변경은 무리없이 동작할 것입니다.
<ul> <li>사자</li> <li>당나귀</li> <ul> <ul> <li>사자</li> <li>당나귀</li> <li>토끼</li> <ul>
재귀적 처리로 ‘사자’, ‘당나귀’가 일치하는 것을 확인하고, 마지막 ‘토끼’를 트리에 추가할 것입니다. 단순한 구조입니다. 하지만 리스트의 맨 앞에 추가하는 경우는 이야기가 다를 수 있습니다.
사자 위에 ‘토끼’를 넣게 된다면 형편없이 동작할 것입니다. ‘사자’와 ‘토끼’는 일치하지 않으며 자식을 전부 새로 그릴 것입니다.
이러한 문제를 해결하기 위해 Key값을 사용할 수 있습니다. ‘사자’, ‘당나귀’, ‘토끼’에 모두 key값이 있다면 ‘토끼’가 맨 위에 추가된다 해도 동일한 key를 가진 엘리먼트는 이동만 하게 됩니다.

올바른 key값이란?

올바른 key로 우리는 유니크한 값을 추가하여야 합니다. 많은 개발자들이 key값에 index를 사용하는 경우가 많은데, 대부분의 상황에서 이상함을 못느낄 수는 있지만, 특이케이스에선 문제가 생기는 경우가 있습니다.
예를 들어, 이번 회사 프로젝트로 실시간 댓글을 남기는 화면이 추거되었습니다. 요구 사항은 다음과 같았습니다.
  • 시간순으로 리스트가 보여야한다. (맨 위가 제일 최신)
  • 리스트 기준 새로 추가하는 댓글은 맨 위에 추가된다.
이러한 요구사항을 토대로 같은 팀원이 개발을 진행했는데, 이상한 케이스가 생겼습니다. 수정을 진행하다가 댓글을 삭제했는데 내가 수정하는 내용이 다른 댓글에 담겨져 있는 것이였습니다. 결국 원인을 파악하지 못하셔서 저한테 도움을 요청하셨고 확인해보니 key값을 for 의 index로 설정한것이 보였습니다.
index로 설정하면 어떻게 되는지 아래와 같은 예제 코드를 통해 확인해보겠습니다.
function Home({}: Props) { const [list, setList] = useState([{ name: "사자" }, { name: "당나귀" }, { name: "토끼" }]); return ( <> <div className="mt-4 px-4"> {list.map((name, index) => ( <div className="flex gap-2 mb-4" key={index}> <div>{name.name}</div> <input type={"text"} className='border flex-1' /> </div> ))} <button className="w-full text-center rounded-xl bg-slate-400 py-4 text-white" onClick={() => { setList([{ name: "개구리" }, ...list]); }} > 추가 </button> <button className="w-full text-center rounded-xl bg-slate-400 py-4 text-white mt-2" onClick={() => { setList(list.filter((_, index) => index !== 0)); }} > 삭제 </button> </div> </> ); }
예제에서는 list의 auto index를 통해 key 값을 부여하고 있습니다.
notion image
우리는 리스트 맨 위에 ‘개구리’ 라는 동물을 추가하고 싶습니다. 근데, 수정하기 전에 ‘당나귀’ 옆 input 박스에 ‘당나귀’에 대한 설명을 아래처럼 추가하였습니다.
notion image
여기서 auto index로 설정된 리스트 맨 위에 ‘개구리’를 추가해보겠습니다.
notion image
우리가 ‘당나귀’설명을 했는데, ‘사자’에 남아있는 모습을 볼 수 있습니다. 이렇게 올바른 키값을 사용하지 않으면 엉망이 될 수 있습니다.
삭제도 똑같이 동작할 것입니다. 그럼 어떻게 해야할까요?
우리는 key 값으로 auto index가 아닌 유니크한 값을 사용하면 됩니다. 주로 유니크한 id 값이나, 위 예제에선 중복되지 않는단 전제하에 name이 될 수 있습니다. 아래의 예제는 name 을 key값으로 변경하였을때 화면입니다.
notion image
정상동작하는 것을 확인할 수 있습니다! 앞으로는 auto index가 아닌 올바른 key값을 기입할 수 있도록 React 재조정, 자식 노드 재귀 방법에 대해 익혀둘 필요가 있을 것 같습니다.!
감사합니다!