안녕하세요! 오늘은 React를 완전히 까보는 시간을 가져보려고 합니다. 평소에 우리가 당연하게 써왔던 React가 내부적으로 어떻게 동작하는지, Virtual DOM부터 Fiber Architecture까지 30분 동안 함께 알아보겠습니다!
오늘은 크게 5가지 주제를 다룰 건데요. 각 주제가 서로 연결되어 있어서 순서대로 따라오시면 전체 그림이 보이실 겁니다. 목표는 '아 그래서 React가 이렇게 동작하는구나'를 느끼는 거예요!
여러분 처음 JSX 봤을 때 어땠나요? 저는 '어? JavaScript에서 HTML을 쓴다고?'라고 생각했어요. 근데 이거, 사실은 JavaScript가 아닙니다!
JSX는 Babel이 JavaScript로 변환해줍니다. 보시면 React.createElement라는 함수 호출로 바뀌죠. 그럼 이 함수가 뭘 반환할까요?
결국 JavaScript 객체를 만드는 거예요! 이 객체가 바로 Virtual DOM을 구성하는 노드입니다. HTML처럼 보이지만 사실은 전부 JavaScript 객체인 거죠.
Virtual DOM 이야기를 하기 전에, 왜 필요했는지부터 보죠. jQuery 시대에는 데이터가 바뀔 때마다 DOM을 일일이 찾아서 업데이트했어요. 코드는 복잡해지고 버그는 양산되죠.
Virtual DOM의 핵심 아이디어는 간단합니다. 상태가 바뀌면 새로운 Virtual DOM을 만들고, 이전 것과 비교해서 바뀐 부분만 실제 DOM에 반영하는 거예요. 그런데 여기서 중요한 오해가 하나 있습니다...
많은 분들이 'Virtual DOM이 빠르니까 쓴다'고 생각하는데, 사실 목적은 성능이 아니라 개발 경험이에요. 우리가 What만 신경 쓰면 How는 React가 알아서 해주는 거죠!
두 개의 트리를 비교하는 건 원래 엄청 복잡한 문제예요. O(n³) 복잡도라서 노드가 1000개만 되어도 10억 번 연산을 해야 합니다. 60fps를 유지하려면 16ms 안에 끝내야 하는데, 이건 불가능하죠. React는 어떻게 해결했을까요?
React는 두 가지 과감한 가정으로 O(n)을 달성했습니다. 첫째, 타입이 다르면 완전히 다른 UI라고 가정하고 비교하지 않고 새로 만듭니다. 둘째, key를 통해 요소를 식별합니다. 이 두 가정 덕분에 엄청난 성능 향상을 이뤄낸 거죠!
이게 바로 우리가 늘 듣는 'key warning'의 이유입니다. index를 key로 쓰면 React가 요소를 제대로 식별하지 못해요. 새 아이템을 추가했는데 React는 기존 아이템이 바뀐 줄 알고 불필요하게 전부 리렌더링하죠. 그래서 반드시 고유한 ID를 key로 사용해야 합니다!
Diffing 알고리즘의 핵심은 3가지입니다. 같은 레벨끼리만 비교하고, 타입이 같으면 props만 업데이트하고, 컴포넌트가 같으면 state를 유지합니다. 이 마지막 포인트가 리렌더링 시에도 state가 사라지지 않는 이유예요!
React는 상태가 바뀌었는지 확인할 때 얕은 비교만 합니다. 참조만 비교하는 거죠. 깊은 비교는 중첩된 모든 객체를 순회해야 해서 느립니다. 대신 우리가 불변성을 지켜야 하죠! 그리고 여기서 중요한 포인트 하나! React는 ===가 아닌 Object.is를 사용합니다. 왜냐하면 === 는 NaN과 NaN을 비교하면 false가 나오고, +0과 -0을 비교하면 true가 나오거든요. Object.is는 이런 엣지 케이스를 더 정확하게 처리합니다. useEffect나 useMemo의 의존성 배열을 비교할 때 바로 이 Object.is를 사용하는 겁니다!
이게 바로 우리가 항상 스프레드 연산자를 쓰는 이유입니다. 직접 수정하면 참조가 같아서 React가 변화를 감지하지 못해요. 새 객체를 만들어야 참조가 바뀌면서 리렌더링이 일어나죠. 복잡하면 Immer 같은 라이브러리를 쓰면 됩니다!
Vue나 Angular는 자동으로 변화를 감지해줍니다. 편하죠. 하지만 React는 '명시적'을 선택했어요. 더 번거롭지만, 코드를 읽었을 때 정확히 무슨 일이 일어나는지 알 수 있다는 장점이 있습니다!
React 16 이전에는 Stack Reconciler를 썼는데요. 재귀적으로 모든 작업을 한 번에 처리했습니다. 문제는 중단할 수 없다는 거예요. 대규모 리스트를 렌더링할 때 입력이 먹통되는 경험, 다들 해보셨죠? 바로 이 때문입니다!
Fiber는 작업을 작은 단위로 쪼개서, 중요한 것부터 먼저 처리하고, 필요하면 중간에 멈추고 다시 시작할 수 있습니다. 사용자 입력은 즉시 처리하고, 검색 결과 업데이트 같은 건 뒤로 미룰 수 있는 거죠!
Fiber는 작업에 우선순위를 매길 수 있습니다. 클릭이나 입력 같은 사용자 인터랙션은 최우선으로 처리하고, 데이터 페칭이나 로깅 같은 건 나중에 처리하죠. useTransition이나 Suspense 같은 기능도 모두 Fiber 덕분에 가능한 겁니다!
Fiber는 렌더링을 중단하고 다시 시작할 수 있기 때문에, 컴포넌트 함수가 여러 번 호출될 수 있습니다. 그래서 반드시 순수 함수로 작성해야 해요. API 호출 같은 부작용은 useEffect에서 처리해야 합니다!
오늘 배운 내용을 정리해볼게요. JSX가 JavaScript 객체로 변환되고, 이게 Virtual DOM을 구성하고, Reconciliation으로 효율적으로 비교하고, 얕은 비교로 빠르게 감지하고, Fiber로 우선순위를 관리합니다. 모두 연결되어 있죠!
이제 여러분은 React를 더 깊이 이해하게 되었습니다. key warning이 왜 뜨는지, 왜 불변성을 지켜야 하는지, 언제 최적화를 해야 하는지 이해할 수 있죠. 이 지식은 실무에서 성능 문제를 디버깅하고 더 나은 코드를 작성하는 데 큰 도움이 될 거예요!
더 깊이 공부하고 싶으시면 React 공식 문서와 Lin Clark의 Fiber 설명 영상을 추천드립니다. 여유가 되시면 React 소스코드를 직접 읽어보거나, 간단한 React를 직접 구현해보는 것도 큰 도움이 될 거예요!
발표는 여기까지입니다. 궁금하신 점이나 더 자세히 알고 싶은 부분 있으시면 질문해주세요!
끝까지 들어주셔서 감사합니다. 혹시 나중에 더 궁금하신 게 있으시면 블로그나 GitHub로 연락주세요!