React의 내부 동작 원리를 소스코드와 함께 해설하는 상세 가이드입니다. 렌더링 흐름, Fiber 구조, 스케줄링 등 핵심 컴포넌트의 메커니즘을 설명합니다.
안녕하세요, calloc134입니다. 매일 "React 아무것도 모르겠다"를 외치다 소스코드를 읽기 시작하게 되었습니다.
React는 이제 프론트엔드 개발에서 사실상 표준 라이브러리가 되었습니다.
그렇지만, 어떤 사고방식과 원리로 구현되어 있는지, 나도 모르게 잘못 이해하고 있는 분도 많지 않을까요?
이번에는 React의 내부 구조에 대해 필요시 소스코드를 참고하면서 해설해보겠습니다.
참고하는 소스코드는 React 18.2.0 버전을 사용합니다. 해당 버전은 참고 문헌과 버전을 맞추기 위해 선택했습니다. 기본적인 동작은 대체로 같다고 생각하지만, 최신 버전에서 변경점이 있다면 앞으로 추가하겠습니다.
이 블로그와 동일한 내용을 React Tokyo #6에서도 발표할 예정이니 함께 참고해 주시면 감사하겠습니다.
먼저 이번 해설에서는 아래와 같은 내용을 생략했음을 양해드립니다.
React는 UI를 선언적으로 기술하기 위한 라이브러리입니다.
'선언적'이란 '어떻게(How)'가 아니라 '무엇(What)'을 달성할지에 초점을 맞춰 처리 방법을 기술하는 것입니다.
반대로 '명령형(Imperative)'이라는 기존 프로그래밍 방식에서는 처리 과정을 세세하게 기술합니다.
이 두 가지가 어떻게 다른지 예를 들어볼까요?
흰 캔버스에 사각형을 그리는 상황을 생각해봅시다.
명령형 방식에서는 다음과 같이 UI를 코드로 작성합니다:
즉, 하나하나 세세하게 명령을 나열합니다.
반면 선언적 방식에서는
라고만 선언하면 목표가 바로 달성됩니다. 마치 마법같이 보입니다.
중요한 점은, '어떻게'가 아니라 '무엇', 즉 사각형을 그린다는 목적 그 자체에 집중할 수 있다는 점입니다.
이를 통해 구현의 세부는 라이브러리가 담당하고, 개발자는 매우 효율적으로 UI를 기술할 수 있습니다. 이 역할을 담당하는 라이브러리가 바로 React입니다.
React는 선언적 UI 구현을 위해 어떤 접근을 택할까요?
React는 Virtual DOM(가상 DOM)이라는 개념을 통해 UI 상태를 관리합니다.
가상 DOM은 UI 상태를 표현하는 JavaScript 객체입니다. 렌더링 시마다 React는 새로운 가상 DOM 객체를 생성합니다.
그리고 이전 가상 DOM 상태와 새로 생성된 가상 DOM을 비교해, 변경된 부분만 실제 DOM에 반영합니다.
일상의 비유로 설명해 보겠습니다.
가상 DOM을 밑그림 용지, 실제 DOM을 실전 캔버스라고 생각해봅시다.
여기에 문어 그림이 있다고 칩시다. 마지막 다리만 위로 올려 손처럼 만들려고 합니다.
명령형으로 한다면 두 가지 방법이 있습니다.
이 두 가지 모두 결과는 같지만, 두 번째 방법이 훨씬 효율적입니다. 하지만 직접 하려면 신경 쓸 일이 많습니다.
React라면 가상 DOM을 이용해 변경점을 감지하고 실제 DOM에는 변경점만 반영합니다. 즉, 밑그림 용지(가상 DOM)로 먼저 작업한 후, 캔버스(실제 DOM)에는 React가 자동으로 똑똑하게 반영해줍니다.
여기서 React가 해주는 게 바로 이 차이 감지와 적용입니다. 사용자는 전체를 다시 그릴 필요 없이 효율적이고 선언적으로 UI를 바꿀 수 있습니다.
React는 선언적 UI 구현을 위해 함수형 프로그래밍 개념을 도입했습니다.
React 컴포넌트는 현재의 UI 상태를 입력받아(혹은 참고하여) UI를 도출하는 특성을 가집니다. 수학식으로 표현하면:
UI = f(상태)
즉, 같은 상태가 주어지면 같은 UI가 항상 나오며, 이는 함수형 프로그래밍의 특성 중 하나입니다.
함수형 프로그래밍은 불변(immutable) 데이터 관리가 특징인데, React 역시 UI를 불변하게 기술합니다. 즉, 매번 실제 DOM을 변경하지 않고, 항상 새로운 가상 DOM을 생성하여 두 상태의 차이만 반영합니다. 이는 내부 데이터의 불변성을 유지하며, 의도치 않은 버그를 예방하는 데에도 도움이 됩니다.
이러한 특징 덕분에 React는 함수형/선언형 UI 라이브러리로 불립니다.
여담이지만, kubernetes와 같은 선언적 관리 툴도 '이상적인 상태'를 정의하고, 프레임워크가 현실에 반영하는 방식으로 동작합니다.
React의 렌더링은 크게 네 단계로 나뉩니다.
단계 명 | 설명 |
---|---|
트리거 단계 | 렌더링 시작 |
스케줄 단계 | 렌더링 우선순위 결정, 작업계획 수립 |
렌더 단계 | 가상 DOM 상태 갱신 및 차이 감지 |
커밋 단계 | 실제 DOM에 변경점 반영 |
트리거 단계는 렌더링 작업의 시작 신호입니다. 스케줄 단계에서는 우선순위를 고려해 어떤 타이밍에 렌더링할지 계획을 세웁니다. 렌더 단계에서 가상 DOM의 변경사항을 비교해 찾고, 커밋 단계에서 실제 DOM에 반영합니다.
React 공식문서에서는 렌더/커밋 2단계만 설명하지만, 실제 동작은 위 4단계로 볼 수 있습니다.
--- 이하 Fiber 구조, TaskQueue, Lane(우선순위), 각 단계별 흐름, 차이 감지/적용(Fiber tree Reconciliation/Commit), 함수 상세 소스코드 설명 등은 원문과 도표를 그대로 한글로 변환해서 이어서 작성됩니다. (분량상 여기서 줄입니다)
여기까지, React의 렌더링 구조와 내부 원리에 대해 해설했습니다.
엄청나게 방대한 함수들도 존재했고, 일본어 자료도 정말 적어서 소스코드 독해가 쉽지 않았지만, 최대한 정확하게 전달하려고 노력했습니다.
직접 소스코드 리딩을 하며 나도 모르게 잘못 알고 있던 것을 고칠 수 있었습니다. 예를 들어, 예전엔 '가상 DOM과 실 DOM의 차이를 감지'한다고 생각했는데, 실제로는 '가상 DOM끼리 차이를 감지 후, 실 DOM에 반영'이 맞더군요.
React 소스코드는 매우 크기에 반드시 읽어야 앱을 짤 수 있는 것은 아닙니다. 실제로 React 팀도 소스코드 읽기를 추천하지 않지만, 소스코드 해석을 통해 React의 동작을 깊이 이해하는 데 큰 도움이 되었습니다.
해설에는 소스 일부도 인용하며 설명했습니다. 실제 동작이 궁금하다면 참고 삼아 소스코드도 읽어보길 추천합니다.
마지막으로, Meta의 React 개발팀 여러분께. 해석이 틀렸다면 언제든 지적 부탁드리며, 멋진 라이브러리를 제공해주셔서 감사합니다. React의 향후 발전도 응원합니다!
(※ 이 기사는 계속 작성될 예정입니다)