react의 코드를 본다는 것
예전에는 리액트를 공부하면서도 실제 리액트의 내부 코드를 직접 들여다본 적은 없었다. 대부분 공식 문서나 블로그 글을 통해 개념적으로만 이해했을 뿐이었다. 그런데 시간이 지날수록 이런 접근이 약간 불안하게 느껴지기 시작했다.
최적화를 하거나, 컴포넌트 간의 관계를 깊이 이해해야 할 때, 리액트가 내부적으로 어떻게 동작하는지를 내가 직접 본 적이 없다는 사실이 마음에 걸렸던 것이다. 어쩌면 그때의 막연한 불안감은 당연한 것이었는지도 모르겠다.
오픈소스는 학부 시절부터 많이 접해왔다. 그때는 재미있는 주제를 중심으로 블로그 글도 자주 썼는데, 예를 들면 Python의 GC를 직접 까보거나, js, java, python의 해시 테이블 구현을 비교하며 실제 내부 코드를 분석해보는 식이었다. 이런 경험을 통해 자연스럽게 오픈소스 코드를 읽고 탐색하는 습관을 키울 수 있었다. 하지만 리액트는 그 자체로 워낙 큰 규모의 프로젝트다 보니, 어디서부터 접근해야 할지 쉽게 감이 오지 않아 조금 망설였던 것 같다.
react craft에서 생각한 것
이전까지는 React Hook을 실제 코드나 내부 구현을 참고하지 않고, 단순히 겉으로 보이는 동작만 보고 직접 구현해보려 했다. 하지만 나중에 실제 리액트의 코드를 살펴보니, 내가 구현했던 방식과는 많은 차이가 있었고, 결국은 '비슷하게 흉내만 낸' 수준이었다는 걸 깨달았다. 물론 뒤늦게 코드를 참고해 다시 적용하긴 했지만, 오히려 비효율적인 학습 방식이었다는 생각이 들었다.
처음에는 React Craft를 통해 다음과 같은 주제를 중심으로 학습할 계획이었다.
- React Hook 구현
- React Element로 바뀌는 과정 구현
- 관련 패키지 직접 구현
하지만 Hook 구현 이후에는, 바로 다음 단계로 넘어가기보다는 전체적인 맥락을 먼저 더 공부한 뒤에 진행하는 것이 맞겠다고 느꼈다. 그래서 예전부터 많이 도움을 받았던 BOAZ님의 ‘React 코드 살펴보기’ 시리즈를 모두 들어보며, react craft 프로젝트를 다시 진행하게 되었다.
실제 강의는 리액트 코드의 흐름을 이론적으로 설명해주는 데에 초점이 맞춰져 있어서, 별도의 구현은 포함되어 있지 않다. 이 기회에 그냥 강의 내용을 바탕으로 실제 리액트의 동작을 약식으로라도 직접 구현해보며 학습해볼 계획이다.
react의 element, 그리고 실제 코드
react element는 ui정보를 담은 객체이다. 이것을 통해 리액트는 ui를 최종적으로 그리는데 중요한 정보인데, 크게 두가지 종류로 정의할 수 있을 것 같다.
- 일반 태그를 담은 정보
- 일반 태그는 맞지만 컴포넌트 정보
여기서 컴포넌트 정보의 경우 속한 다른 컴포넌트를 찾아가서, 일반 태그가 나올 때까지 계속 들어간다. 또한 모두 찾았을 때, element를 일괄 처리하는 것은 아니고, 중간중간 element는 적절한 시기에 따로 처리가 된다. 이는 패키지와 관련된 부분이기 때문에 여기까지 하겠다. 즉, 위의 element의 종류를 다시 시각화 해보면 아래와 같다.
그래서 실제 코드를 보면 다음과 같이 구현이 되어있다.
react element를 만다는 과정은 createElement라는 함수를 보면 되는데, 먼저 전체적인 흐름은 다음과 같다.
- 요소 식별하는 key와 dom 접근 ref를 별로 처리
- JSX에서
<Button color="blue" size="lg" />
처럼 props를 넘기면 내부에선 이걸{ color: 'blue', size: 'lg' }
객체로 만드는 작업을 하기 위한 props객체 구성 - 여러 children을 가질 경우에 대한 처리
- 어떤 props가 빠졌더라도, 오류 없이 동작하게 하려는 처리
1. key, ref 처리
- React에서 컴포넌트 구분이나 참조를 위해 예약된 prop를 따로 관리한다.
2. 나머지 props 처리
- key, ref를 제외한 나머지 props를 구성한다.
3. children처리
- JSX에서
<div>hello</div>
나<div><span /></div>
처럼 안에 들어가는 children들을 처리
4. defaultProps 처리
- type이 컴포넌트일 경우, 그 컴포넌트의 defaultProps가 있으면 빠진 props를 default 값으로 채워준다. 그리고 마지막으로 element를 return 한다. 만약 jsx가 있다고 하면 각 단계에 대해서 다음과 같다.
유사하게 만들어본 동작과정
organization 주소 : react craft
function createElement(type, config, ...children) {
let props = {};
let key = null,
ref = null;
if (config != null) {
if (config.key !== undefined) key = "" + config.key;
if (config.ref !== undefined) ref = config.ref;
for (let prop in config) {
if (!RESERVED_PROPS.hasOwnProperty(prop)) {
props[prop] = config[prop];
}
}
}
if (children.length === 1) {
props.children = children[0];
} else if (children.length > 1) {
props.children = children;
}
if (type && type.defaultProps) {
for (let prop in type.defaultProps) {
if (props[prop] === undefined)
props[prop] = type.defaultProps[prop];
}
}
return ReactElement(type, key, ref, props);
}
이렇게 react craft에서 createElement함수를 적용해봤다. 따로 key, ref를 다른 props와 별개로 관리를 했고, children 처리 및 defaultProps 병합, 최종적으로 ReactElement 구조를 생성했다. 여기서 jsx를 따라 만든 약식? 코드도 있는데,
function ComponentButton1(props) {
return createElement(
"button",
{ color: "blue" },
createElement("p", null, "hello"),
createElement(ComponentButton2, { color: "blue" }, "no"),
createElement(ComponentButton3, { color: "blue" }, "no")
);
}
function ComponentButton2(props) {
return createElement("button", { color: props.color }, props.children);
}
function ComponentButton3(props) {
return createElement("button", { color: props.color }, props.children);
}
const element = createElement(
"div",
{ id: "delete-account" },
createElement("p", null, "Elements"),
createElement(ComponentButton1)
);
실제로 jsx는 createElement 호출로 변환되어 처리되지만, 이번에는 순수 JavaScript로 그 과정을 구현해보았다. 이후 컴포넌트를 중첩해서 구성한 뒤, resolveTree()
함수를 통해 컴포넌트 트리를 재귀적으로 순회하면서 최종 구조를 포함 관계로 시각화도 했다.
React의 렌더링 과정중 elements 생성 부분을 실제 코드를 따라가며 구조를 분석할 수 있는 좋은 경험이었고, 전처럼 동작만 유사하게 짐작해서 해보는 학습보다는 확실하게 더 도움이 되는 것 같다.