[React - 그래서] key prop은 렌더링에 어떻게 쓰이는가 (Fiber / 재조정 reconciliation)
Code/그래서

[React - 그래서] key prop은 렌더링에 어떻게 쓰이는가 (Fiber / 재조정 reconciliation)

반응형

`${stack} - 그래서` 카테고리 탄생 배경

새로운 카테고리를 개설했습니다.
기술 공부를 할 때 그래서..? 이게 뭐..?라는 느낌이 들며 지식 간 연결이 끊어져있는 느낌이 들 때가 잦습니다.

그럴 때 다른 분들은 '왜?'를 떠올리던데 저는 대부분 그것보다 이전 단계에서 이해를 못하는 편이라 '그래서? 이게 무슨 상관인데?'로 인덱싱 문제를 해결하곤 합니다. why? 보다 then.....what? 느낌이랄까요

노력형 개발자가 저뿐만은 아니겠죠, 계속 성장 하고 있는 걸 보면 사실 큰 문제는 아닌 것 같습니다. 
하나씩 모으다 보면 어느 순간에 뭔가 연결되는 느낌이 드는데 그때 적지않은 도파민이 나오기에 그 쾌감이 퍽 재밌기도 합니다.


개인적으로는 새로운 개념을 받아들일 때 얕게 우선 정의부터 대략적으로 받아들이고 시간을 갖고 조금씩 깊게 들어가야 이해하는 편이라 이번 글도 그러한 방식으로 편하게 작성해볼까 합니다.


반응형


아마 알고있을


React key prop

- 배열을 통해 JSX 요소를 렌더링할 때 불필요한 리렌더링 방지를 위해 주로 사용된다.
- key 프롭을 설정하지 않으면, <Component key={index} /> 와 같이 index를 사용한 것처럼 작동되며 이건 최악이다!
  :  근데 Index가 괜찮은 경우가 있기도 하다?
- key prop을 안 넣으면 콘솔에 에러가 뜬다.
- key prop에는 유니크한 id를 넣어야 한다. 하지만 그렇다고 Math.random() 같은 걸 넣으면 안된다.

react의 key prop에 대해 찾아본 적이 있다면 아마 얕게라도 위의 내용에 대해 인지하고 있을 것 같습니다.


여기서 베이스를 조금씩 확장시켜 보겠습니다.


React의 컴포넌트는 props와 state가 변경되면 리렌더링됩니다.

virtual dom을 통해 마지막으로 렌더링 된 vdom(current Tree)와 props와 state가 변경되어 바뀔 vdom (Work In Progress Tree)을 비교하고, 변경된 부분만 실제 리얼 dom을 업데이트하여 수정 후 렌더링합니다. (vdom은 js 객체 형태로 메모리에 저장)

이러한 과정을 리액트에선 재조정, reconciliation이라 합니다.

두 virtual dom 트리를 비교하는 데에는 리액트에서 구현한 diffing 알고리즘을 통해 진행됩니다.

diffing 알고리즘에선 조정자를 사용해 현재 트리와 진행 중인 작업 트리간의 차이점을 찾아 계산된 변경 사항을 렌더러에 보냅니다.

조정자

  • React 16 이전 : Stack Reconciler
  • React 16 이후 : Fiber Reconciler 


이 비교 알고리즘에 사용되는 모델이 16이전에는 stack, 이후에는 Fiber로 변경되었습니다.
Stack Reconciler의 경우 렌더링을 진행할 때 재귀 호출 방식 + 모두 동기적인 방식으로 작동되었습니다.

요즘 장치는 대부분 60FPS로 환경을 재생합니다. 계산해보면 16ms마다 새 프레임이 나타나야하는 것인데
Stack Reconciler의 경우 모든 업데이트에 대해 재귀 호출 방식으로 모든 virtual dom 노드에 대해 탐색한 뒤 렌더링까지 진행되기에 UI의 덜커덕거림이나 뒤틀림이 발생할 수 있습니다.



그래서..진짜 Fiber Reconciler여야 했는가?

Fiber는 동시성 렌더링을 위해 좀 더 효율적인 방법으로 실제 dom에 React element를 반영하기 위해 도입되었습니다.

우선순위 선정 / 재사용 

저는 크게 두 가지 키워드로 Fiber의 역할을 이해할 수 있었습니다.
Fiber 이전까지는 재귀 호출 방식으로 탐색을 해내가야 했고, render phase -> commit phase가 진행되는 도중에 작업을 중지할 수 없었습니다. 

키워드를 조금 더 해석해보자면,

1. 우선순위 선정

- 컴포넌트를 렌더링하고 있는데 유저의 클릭 이벤트가 발생되었다. 유저는 전자보다 후자의 응답이 빠르게 올 것을 기대할 것이다.
   : 더 우선 순위가 높은 작업이 있다면, 작업을 잠시 멈추고 나중에 다시 작업한다.

동시성 참고 링크 : https://goidle.github.io/react/in-depth-react18-concurrent_render/

 

2. 재사용
- 재활용 할 수 있는 걸 굳이 다시 만들지 말자.
  : 불필요한 virtual dom 탐색 / 추가 / 삭제를 줄일 수 있다.



그래서.. Fiber는 대체 뭔데?

Fiber는 하나의 작업 단위이자, React element와 대응하는 인스턴스입니다.


우선 하나의 작업 단위라는 뜻은 '우선순위'와 함께 생각하면 좋을 것 같습니다.

자바스크립트는 한 번에 한 가지 일만 실행할 수 있는 싱글스레드 언어입니다.

자바스크립트 코드를 실행시키면 유효 범위를 갖는 코드들에 대해 실행 컨텍스트를 생성하고, 이를 콜 스택에 푸시하여 작업을 진행합니다.
Promise, 비동기 요청, 이벤트 처리 등은 콜스택이 아닌 마이크로 태스크큐, 태스크 큐에 보관됩니다.

콜스택이 비어 있어야만 브라우저의 이벤트 루프가 이를 감지하여 큐에 보관되어 있는 작업을 콜스택으로 푸시해주며, 싱글스레드 언어인 자바스크립트를 보완해주고 있습니다.

Fiber는 이 콜스택을 재구현한 것입니다.
더 높은 우선순위의 작업이 들어왔다면 작업을 멈추고 (포인터로 기억) 다른 작업을 먼저 할 수 있도록 콜스택을 조정합니다.


그럼 React element와 대응하는 인스턴스라는 말에 대해 조금 더 설명해 보겠습니다.

React element의 확장 -> Fiber 노드
리액트는 재조정 과정에 앞서 render 과정을 먼저 진행합니다. 

리액트 재조정에서의 render 의미는 dom이 페인트 되는 것을 뜻하는 것이 아닌 '준비'정도로 받아들이면 쉬울 것 같습니다.

 

재조정 - Render 단계

이 과정에선 JSX를 바벨을 통해 React element로 변환하는 작업만을 진행합니다.

React.createElement( 
  type, // 태그 이름 문자열 'div' | 개발자 정의 컴포넌트함수 Custom .. | 리액트 호스트컴포넌트 Suspense..
  [props], // 컴포넌트 프롭스
  [...children] 
);

위와 같이 React.createElement()를 호출하는 js코드로 변환합니다.

재조정 - Reconcile 단계

이후 Reconcile 단계에서는 

React element를 Fiber 노드로 변환하는 작업을 진행하고, 실제 Dom 트리와 새 React 엘리먼트를 비교하여 변경점을 적용합니다.
Fiber 노드에는 엘리먼트와 관련된 정보들을 포함하고 있으며, 재조정은 깊이 우선 탐색으로 작동됩니다.


key를 설명하는 데에 가장 필요할 것으로 보이는 Fiber 구현체의 key를 몇 개 뽑아 보았습니다.

- sibling : 다음 형제 Fiber 
- child : 자식 FIber
- return : 부모 Fiber

각 노드들은 single linked list로 연결되어 있고, 객체로 존재하고 있는 child 요소들에 대해서도 Fiber 노드로 변환하는 작업을 거칩니다.



그럼 이제 간단한 예제 코드와 함께 진짜 key에 대해서 파악을 해보겠습니다.

첫 마운트가 끝나, React element들에 대한 Fiber노드가 존재하는 상황이라 가정
const Test = () => {
return (
      <ul>
        <li>안</li>
        <li>녕</li>
        <li>하</li>
        <li>쇼</li>
      </ul>
  )
}

Fiber (객체 일부)

- type : 태그 이름 문자열 'div' | 개발자 정의 컴포넌트함수 Custom .. | 리액트 호스트컴포넌트 Suspense..
- key : element의 key 프롭
- sibling : 다음 형제 Fiber 
- child : 자식 FIber
- return : 부모 Fiber

Fiber 해체

ul
// -> type : 'ul' / 형제 sibling : null / return 부모 Fiber / child : li Fiber (첫 번째 자식 요소 -> <li>안</li))

li ('안')
// -> type : 'li' / sibling : li Fiber (<li>녕</>) / return ulFiber / child null

li ('녕')
// -> type : 'li' / sibling : liFiber (<li>하</>) / return ulFiber / child null

...


li ('쇼')
// -> type : 'li' / sibling : null / return ulFiber / child null

예시코드는 현재 key에 대해 지정을 하지 않아, 이 경우 fiber의 key는 null을 갖지만 리액트 내부에서 이를 인덱스로 가정하여 처리합니다. 

리액트 Fiber는 재사용할 수 있는 것은 최대한 재사용할 수 있도록 구현되어 있습니다.
이 포인트에서 key의 존재 목적을 다시 생각할 수 있습니다.

Fiber 노드에서는 부모의 자식 관계를 sibling을 통해 효율적으로 관리합니다.
상태나 props의 변경으로 인해 업데이트가 진행되는 단계에서 Reconciler는 Fiber를 재사용할지, 교체 삭제 또는 생성을 할지 판단하는데 이때 엘리먼트의 type이나 key가 이전과 다르다면 해당 Fiber에 대한 작업을 다시 진행하게 됩니다.

React에서는 key prop에 유니크한 값을 넣도록 강조하고 있는데 그 이유를 여기서 찾을 수 있습니다.

- 배열의 index를 사용하지 마라

단순히 배열의 마지막에 요소가 추가되는 것이라면 큰 문제가 없을 수도 있습니다. 하지만 같은 type을 갖고 Fiber에서 index를 키로 갖고 있는 상태일 경우, 배열 사이사이 요소가 재정렬 되는 로직이 추가된다면 예기치 못한 문제가 발생할 수 있습니다. 

- key에 유니크한 값을 지정해야 한다. 그렇다고 Math.random()등을 사용해선 안된다.

key의 존재 목적은 '효율성'에 있기에 변경이 필요한 부분에 대해서만 작업하기 위함이며,

key는 부모의 자식 요소간 관계도를 나타낸다는 것을 알 수 있습니다.
렌더 부분에 Math.random()을 사용한다면 렌더링 될 때마다 key가 변경될테니 효용이 없을 것입니다.
- (key가 다르다면 새로운 Fiber노드를 생성하니 하위 노드도 모두 다시 생성하게 됩니다)
형제 요소 간 동일한 key를 사용하는 것 또한 목적과 동작을 생각해 보았을 때 key를 제대로 활용하지 못하게 됩니다.



서두에 정의했던 일반적인 key prop 내용에 대해 다시 한 번 살펴보겠습니다.

- 배열을 통해 JSX 요소를 렌더링할 때 불필요한 리렌더링 방지를 위해 주로 사용된다.
- key 프롭을 설정하지 않으면, key에 index를 사용한 것처럼 작동되며 이건 최악이다!
  :  근데 Index가 괜찮은 경우가 있기도 하다?
- key prop을 안 넣으면 콘솔에 에러가 뜬다.
- key prop에는 유니크한 id를 넣어야 한다. 하지만 그렇다고 Math.random() 같은 걸 넣으면 안된다.

 


조금 더 이해할 수 있도록 간략히 실무에서 종종 발생할 수 있는 버그를 공유드리면 좋을 것 같습니다.

실무에선 예제와 달리 복잡한 비즈니스 로직을 거쳐 특정 데이터만 변경되는 요소들이 존재합니다.
그로인해 단순히 unique한 값만 부여하는 것이 아닌 세부 값을 추가해주어야 할 때가 있습니다.
배열 데이터에서 분명 데이터 자체의 id를 사용해 key를 설정했으나 하위 컴포넌트의 일부가 이전 값으로 보이는 경우가 종종 있는데 이 경우는 변경을 시도했으나 이전 key와 현재 key가 같고 형제 요소 간 Fiber타입이 같을 때 발생할 수 있어 key에 보다 명확한 유니크 값을 설정할 필요가 있습니다. 

이런 문제를 방지하기 위해 고유 id 값으로만 판별이 어렵다 판단되는 아이템에 대해서는 id는 고유하되 변경될 수 있고 렌더링에 반영되어야 하는 특정 값에 대해서도 추가로 결합해주어 사용하고 있습니다.


이 글이 key prop의 그래서? + 왜?에 대해 얕게라도 조금 더 이해에 도움이 되는 글이었으면 합니다.

혹 틀린 내용이 있다면 언제든 말씀 해주시면 정말 감사드리겠습니다. !

감사합니다.


 

 

 


글을 마무리 지으려다가 간만에 주니어 개발자의 이런 저런 사견을 조금 적어볼까 합니다.

요즘에는 기술에 대해 깊게 파고들기 위해 노력하고 있는데 여간 어려운 게 아닙니다.
와!!! 하다가도 음.....하고, 아하! 하다가도 엄...? 하고, 왜 이걸 지금 알았지 성찰도 하고

더닝 크루거 효과

제가 좋아하는 더닝크루거 학습곡선입니다.

어느덧 경력 만 3년이 되기까지 한 달이란 시간이 남아있네요.

취업 직전까지 우매함의 봉우리, 멍청이산의 정상에 있다가 취업과 동시에 절망의 계곡으로 떨어지고 있었는데 사실 일 년 전에도 이 년 전에도 저는 절망의 계곡이었단 말이죠

이때쯤이면 계곡에서 수영 정도는 할 수 있지 않을까 했는데 수심이 생각보다 깊네요. 그래서 물놀이할 땐 항상 계곡을 조심해야 합니다.

 

비록 절망의 계곡에 있지만 예전과 달라진 부분이 하나 있다면 기술 공부가 부쩍 재밌어졌습니다.

예전엔 대부분의 공부가 실무로 주를 이루었습니다. 해결해야만 하는 버그, 기능이 있고 그걸 하려면 어떻게 해야 하는가에 초점이 맞춰져 있던 것 같습니다. 그래서 더 일에만 매진한 것 같네요.

구현 능력, 문제 해결 능력을 키우는 데에 많은 도움이 되었기에 후회한다는 말은 아닙니다.

다만 방식을 바꾸어 개인적으로 좀 더 넓은 범위의 공부를 하다보니 시야가 더 빨리 확장되며 실무 능력 또한 같이 상승하고 있다는 느낌을 받고 있는 것 같습니다. 

예로 들자면 https를 공부하다가 암호화를 알게 되고, 또 대칭키 비대칭키를 이해하려고 머리 쥐어 뜯으며 그림 그려가며 이해하고 그러다가 실무에서 SSO 기능 구현을 위해 url 서치 파라미터로 token등과 같은 정보를 주고 받으며 통신을 해야 했는데 공부를 하고 나니 token을 생으로 전달하는 건 안되겠다 싶어 암복호화 플로우를 설계해서 공유하고 

또 플로우 설계를 하면서 비대칭키 rsa로 jwt 전달하려니 바이트 초과로 사용을 못해서 res 대칭키 방식과 혼합해서 사용하는 방식으로도 생각을 해보고..

실제 암 복호화는 프론트가 아닌 서버 api를 통해 해결하긴 했지만, 만약 https 공부를 따로 안 했다면 아이디어를 떠올리지도 못했을텐데 하는 생각과 함께 보다 넓은 범위의 개인 공부에 대한 중요성을 크게 느낀 재밌는 경험이지않았나

 

요즘엔 또 좋은 코드가 무엇인지 고민이 많습니다. 

추상화를 어디까지 해야 선언적인 코드인가를 고민하다가 의견을 찾아보니 기존 컨벤션에 맞추면 된다는데 그 컨벤션을 맞춘 사람이 내 자신이라면......? 커스텀 훅으로 묶을 때도 co-locate 패턴도 좋은데 ui랑 분리하겠다고 비즈니스 로직을 묶는 게 진짜 괜찮은가?에 대한 뭔가 근본적인 물음을 계속 던지고 있는 것 같습니다.

기존 방식을 벗어나 사고를 확장하려고 노력 중인데 뭐 과도기라고 생각합니다.

열심히 계속 하다보면 언젠간 깨달음의 비탈까지 가지않을까..해서 계속 정진해보려고 합니다.

그 언젠가에도 더닝크루거 이미지를 넣으며 모든 주니어를 응원하는 문장으로 마무리했던 적이 있는 것 같은데 데자뷰인가 봅니다.

 

그러니

절망의 계곡에 있는 개발자들 모두 화이팅!!!!!!!

반응형

'Code > 그래서' 카테고리의 다른 글

[HTML-그래서] 브라우저 동작 원리와 함께 보는 HTML5  (0) 2024.02.13