[React] key 값을 쓰는 이유(2)

yongkini ·2023년 2월 24일
0

React

목록 보기
17/19
post-custom-banner

Key 값을 쓰는 이유(1)에 이어서 의문점 추가 정리

: 위의 현상을 다시 살펴보고자 글을 써본다.

상황

    <>
      {/* 추가 버튼과 삭제 버튼*/}
      <input type="button" value="추가" onClick={addItem} />
      <input type="button" value="삭제" onClick={delItem} />

      <h2> Show Problem Example</h2>
      {list.map((v, index) => (
        /*  div 태그의 key로 배열의 index 사용*/
        <div key={index}>
          <span>
            <input type="number" />
          </span>
          {v.name}, idx: {index} <input type="text" />{' '}
        </div>
      ))}
    </>

: 위의 JSX 를 렌더링하고 있는건데, 포인트는 div 태그의 key값에 map 메서드의 index를 준 상황이다. 물론 아예 key값을 주지 않아도 위의 상황과 똑같을 것이다. key값을 index(map의)로 줬을 때 위에 gif 플로우처럼 맨앞에 정국이라는 이름의 데이터를 추가하면 그 데이터만 dom update를 해서 리렌더링하는게 아니라 정국 데이터를 필두로 모든 데이터가 리렌더링된다. 추가로 input tag의 value로 써놨던 값들이 하나씩 앞으로 밀려서 본래 철수에 1, 영희에 2라는 값이 들어가있었다면, 정국이 맨앞에 오게됨으로써 정국이 1, 철수가 2, 영희가 3(본래 민수의 데이터)이 된다. 이건 예를 들어, input을 써놓고 수정할 수 있도록 만든 todolist를 만들었다 했을 때도 큰문제가 되는 상황이고, 어쨌든 의도대로 코드가 동작하지 않는다는 본질적인 측면에서 문제가 되는 상황이다.

왜 저런 현상이 일어날까?

: 위의 gif는 input tag를 빼고 똑같은 방식으로 실행해본 결과다. 결론적으로, 본래 의도가 정국 데이터를 추가하는거라고 했을 때 정상적으로 철수, 영희, 민수 데이터 앞에 정국 데이터가 들어갔으므로 문제가 되지 않아보인다. 하지만, 일단 앞서 말한 것처럼 정국 데이터를 포함한 DOM만 update하면 되는 상황에 뒤의 모든 Element를 업데이트하는(리렌더링) 것 자체가 일단 비효율적이다. 이에 더해, 이를 통해 알 수 있는 또한가지는 React가 key값을 통해 이전의 Element를 현재의 Element에 맞추는 방향으로 동작함을 알 수 있다. 왜냐하면, 현재 변한건

[{name: '철수'}, {name: '영희'}, {name: '민수'}]

위와 같은 데이터에서 맨앞에 unshift 로직으로

{name: '정국'}

데이터가 추가된 것이다. 그래서 변한 데이터에 맞게 리액트는 리렌더링을 실행했고, 그에 따라 map을 다시 실행했을 것이다. 이 때 새로 렌더링할 때 동작 방식이 key, name(데이터=state), 이를 렌더링하는 하나의 DOM 객체 혹은 노드를 테이블로 표현해보면 먼저, 리렌더링이 발생하기 이전(정국 데이터 추가 이전)을 표현해보면,

key name Node
0 철수 고유의 Node id가 있다면 철수라고 해보자
1 영희 고유의 Node id가 있다면 영희라고 해보자
2 민수 고유의 Node id가 있다면 민수라고 해보자

위와 같다. 여기서 고유의 Node id라는 표현은 브라우저가 HTML을 파싱할 때 만들어놓는 Node가 있을 때, 그 Node에 각각의 데이터가 담기고, 이를 렌더링한 상태에서 각각의 Element의 고유 주소값 혹은 id값 등이 있다고 가정해보는거다(설명을 위해). 그럼, 정국의 데이터가 추가된 다음엔 이렇게 된다.

key name Node
0 정국 고유의 Node id가 있다면 철수라고 해보자
1 철수 고유의 Node id가 있다면 영희라고 해보자
2 영희 고유의 Node id가 있다면 민수라고 해보자
2 민수 고유의 Node id가 있다면 정국 데이터 추가를 위해 생긴 것 이라고 해보자

어떻게 된건지 설명해보면, 본래 철수라는 data를 담은 노드, 영희라는 data를 담은 노드 등이 있었는데, 정국의 데이터를 맨앞에 추가하면서, 리렌더링이 발생한다. 이 때, 본래 고유한 key값을 제대로 썼다면 나머지 노드들은 그대로 있고, 정국의 data를 담은(고유의 node id가 있다면 정국이라고 할 수 있는) DOM Element만 추가되는게 정상적인 플로우다. 하지만, 위에서는 map의 index값을 썼기에 앞서 말한대로 되지 않고 본래 철수 data를 담던 node에 정국의 데이터를 덮어씌우고, 본래 영희의 데이터를 담던 node에 철수를 덮어씌우는 식으로 된거다. 그리고 마지막에 새로운 node를 추가해 거기에 민수의 data를 담는식으로 진행된거다. 사실은 정국의 data를 담는 새로운 node가 필요했고, 그것만 만들면 됐는데, 하나씩 데이터를 앞으로 덮어씌우고, 마지막에 본래 정국의 data를 담을 새로운 node가 하나 생기고 거기에 민수의 데이터가 쓰여진거다. 이러한 방식으로 리렌더링이 일어나기 떄문에 정국의 데이터만 추가했지만, 앞에서부터 모든 DOM객체(앞서 말한 노드)에서 리렌더링이 일어난거다.

정상적인 플로우라면?

: 정상적인 플로우, 즉, key값에 map의 index가 아닌 데이터 각각이 품고 있는 예를 들어,

[{userId: "CZ1", name: '철수'}, {userId: "CZ2",name: '영희'}, {userId: "CZ3",name: '민수'}]

위와 같은 데이터에서 맨앞에 unshift 로직으로

{userId: "CZ0",name: '정국'}

이런식으로 추가가 됐고, key값에 위의 userId를 썼다면? 데이터가 추가되도, 데이터 각각이 갖는 userId는 고정적인 고유값이므로 결과는 이런식으로 작동한다.

정국 데이터 추가전

key name Node
CZ1 철수 고유의 Node id가 있다면 철수라고 해보자
CZ2 영희 고유의 Node id가 있다면 영희라고 해보자
CZ3 민수 고유의 Node id가 있다면 민수라고 해보자

정국 데이터 추가후

key name Node
CZ0 정국 고유의 Node id가 있다면 정국이라고 해보자
CZ1 철수 고유의 Node id가 있다면 철수라고 해보자
CZ2 영희 고유의 Node id가 있다면 영희라고 해보자
CZ3 민수 고유의 Node id가 있다면 민수라고 해보자

위와 같이 정상적으로 동작했다면 단지 맨앞에 정국의 데이터를 품은 DOM 노드를 새로 생성해 넣고 나머지는 그대로 두는 방식으로 최소한의 리렌더링으로 동작할 것이다.

다시 돌아와서 input value는 왜그렇게 됐을까?

: 다시 처음 gif 상황으로 와보자. 앞서 말한 예시로 보면, data(state)가 업데이트 되면 그 데이터를 기준으로 리렌더링은 잘수행되는 것을 알 수 있었다. 정국 데이터가 맨앞에 오고, 나머지는 하나씩 밀려나는 형태라고 해도 일단 목적은 달성한게 된 것이므로 그렇다. 하지만, input 태그에 value를 추가해서 같은 플로우로 돌려보면 철수에 썼던 input value가 정국의 input value로 오고 영희, 민수도 똑같이 앞으로 해당 input value를 밀어내는걸 볼 수 있었다. 이건 위의 상황에서 고유 node id가 있을 때 특정 id가 그대로있고, 맨 뒤에 새로운 node를 만들어서 거기에 앞에서부터 하나씩 밀어낸 데이터를 넣는 것을 생각해보면 실마리를 찾을 수 있다. 위의 상황에서 input tag를 포함시켜보자

key name Node input Node
0 철수 고유의 Node id가 있다면 철수라고 해보자 고유의 Node id가 있다면 철수 input이라고 해보자
1 영희 고유의 Node id가 있다면 영희라고 해보자 고유의 Node id가 있다면 영희 input이라고 해보자
2 민수 고유의 Node id가 있다면 민수라고 해보자 고유의 Node id가 있다면 민수 input이라고 해보자

여기서 고유 key값을 제대로 설정 안한채로 정국 데이터를 추가했다면

key name Node input Node
0 정국 고유의 Node id가 있다면 철수라고 해보자 고유의 Node id가 있다면 철수 input이라고 해보자
1 철수 고유의 Node id가 있다면 영희라고 해보자 고유의 Node id가 있다면 영희 input이라고 해보자
2 영희 고유의 Node id가 있다면 민수라고 해보자 고유의 Node id가 있다면 민수 input이라고 해보자
2 민수 고유의 Node id가 있다면 정국 데이터 추가를 위해 생긴 것 이라고 해보자 고유의 Node id가 있다면 정국 데이터 추가를 위해 생긴 것의 input 이라고 해보자

앞선 예시와 같이 input node도 앞으로 하나씩 밀어내졌다기보다는 node 자체는 그대로고, 거기에 정국 데이터를 새로 덮어씌웠고, input value는 정국의 데이터로 덮어씌워진게 아니라 이전 node에 있던 value가 그대로 남아있어서 마치 앞으로 한칸씩 이동한 것처럼 보이는거지 사실 node 자체는 표현해보면 거기 그대로 있는 것이라고 할 수 있다. 단지, 본래 민수의 데이터를 표현하던 node 뒤에 하나의 새로운 node가 생겼고, 거기에 민수의 데이터를 덮어씌웠고, input value도 새로 만들어진 input node의 value이므로 텅비어 있는 것이다.

결론

: 결론적으로, 리액트는 고유한 key값이 들어오지 않으면 배열을 map 표현식을 이용해 렌더링을 할 때 비효율적인 로직으로 동작하고, 원하는 결과물을 내지 않게 된다. 리액트는 최소한의 리렌더링을 하기위해서 기존의 상태와 현재의 상태를 key값을 통해서 비교한다. 이 때, 고유한 key값을 제대로 넣었다면 리액트의 계산상에서는 새로 생긴 key값만 넣어주면 되고, 실제로 그렇게 실행한다. 하지만, 고유한 key값이 제대로 들어가지 않았다면 리액트는 이전의 데이터는 그대로 있다고 해도 해석을 할 때 key값을 기준으로 하기 때문에 다 바뀐 것이라고 판단을 하게 되는거다. 그 플로우를 살펴보면(제대로된 key값을 넣지 않았을 때)

  • 정국의 데이터를 추가한다.
  • setState로 인해 리렌더링이 발생한다.
  • 리렌더링을 해야할지를 리액트가 판단하는데 이 때, key값을 참조한다.
  • 이전에는 index = 0(key값)에 철수의 데이터가 있었는데 이제는 정국의 데이터가 index = 0이 된다. 따라서 기존에 철수의 데이터를 담고 있던 객체는 리렌더링 해야한다. 따라서, 정국의 데이터를 기존 철수의 데이터 node에 업데이트하고 리렌더링한다.
  • 이전에는 index = 1에 영희의 데이터가 있었는데 지금은 철수의 데이터가 index=1이되므로 데이터가 변화했다고 판단하고 리렌더링한다.
  • 이런식으로 민수의 데이터도 바꼈다고 판단하고 리렌더링한다.
  • 마지막으로 index=4는 새로 생긴 데이터라고 판단되므로 당연히 새로 그리고 거기에 있는 민수 데이터를 이전에 있던거라고 판단할 참조가 없기에(고유한 key값이 없기에) 민수 데이터도 새 데이터로 판단하고 리렌더링한다.
  • 이에 따라 추가된 정국의 데이터를 기점으로 모두 리렌더링 된다.

하지만, 정상적인 플로우라면 이렇게 된다.

  • 정국의 데이터를 추가하고 setState가 발동하여 리렌더링이 이뤄진다.
  • 기존의 데이터를 현재의 데이터와 비교할 때 key값을 참조하는데 철수, 영희, 민수의 데이터의 키값은 그대로다(고유한 값을 넣었다는 전제). 이에 따라 3개의 객체는 리렌더링 하지 않는다. 하지만, 정국의 데이터가 새로운 key값을 가지고 추가됐다는 걸 알게돼 정국의 데이터를 담은 객체만 새로 리렌더링한다.

앞선 플로우와 쓴 내용, 리렌더링을 한 객체의 수 면에서 차이가 나고 훨씬 효율적임을 알 수 있다. 이러한 리액트 로직에 따라 key값에는 고유한 값을 써줘야함이 명확함을 알 수 있었다. 하지만, 예외적으로 이러한 state 변동 사항(순서의 수정, 삭제, 추가 등)이 일어나지 않는 static한 데이터라면 key값은 걱정하지 않아도 된다.

profile
완벽함 보다는 최선의 결과를 위해 끊임없이 노력하는 개발자
post-custom-banner

0개의 댓글