render 함수 구현하기 (요소 추가)
미니 리액트 구현하기
- Day1 : createRoot
- Day2 : JSX란?
- Day3 : JSX -> Js 객체 변환 함수 구현하기
- Day4 : render 함수 구현하기(추가)
지금까지 jsx와 이를 일반 자바스크립트 객체로 변환시키는 작업을 진행했다. 하지만, 아직 화면에 우리가 작성한 jsx를 표시해줄 수 없다.
화면에 표시하기 위해서는 렌더링 과정이 필요하다. 이를 구현하기 전에 React에서는 렌더링이 어떻게 일어나고 있는지 먼저 알아보고자 한다.
React에서의 랜더링
React에서 사용자에게 화면을 보여줄 때, React는 다음 세 가지 단계를 거친다.
- 렌더링 트리거
- 컴포넌트 렌더링
- DOM에 커밋
각각의 단계는 모두 초기 렌더링과 리렌더링에서의 동작으로 나눌 수 있다.
1단계: 렌더링 트리거
초기 렌더링
- 앱 시작 시 트리거 해야함
createRoot함수로 리액트의 진입점을 생성하고 생성된 root에render함수를 호출
import App from "./App";
import { createRoot } from "./lib/react-dom/client";
const root = createRoot(document.getElementById("root"));
root.render(<App />); // 주석 처리시 화면에 컴포넌트 표시 안됨
리렌더링
- 초기 렌더링 이후 상태 업데이트가 발생하면 렌더링이 트리거 됨
2단계: 컴포넌트 렌더링
렌더링 트리거가 되면 React는 컴포넌트를 호출해 화면에 노출할 내용을 파악한다.
React에서 렌더링은 React에서 컴포넌트를 호출하는 것이라고 할 수 있다.
초기 렌더링
- root 컴포넌트를 호출
리렌더링
- 리렌더링을 트리거한 컴포넌트를 호출
- React는 이전 렌더링 이후 변경된 내용을 계산(화면에 반영 x)
컴포넌트를 렌더링할 때, 컴포넌트가 중첩되어 반환값으로 다른 컴포넌트를 반환하는 경우 React는 반환되는 컴포넌트를 다음에 렌더링한다.
더이상 중첩되는 컴포넌트가 없고 React가 표시해야 할 컴포넌트에 대한 모든 정보를 알 때까지 계속해서 재귀적으로 렌더링이 발생한다.
3단계: DOM에 커밋
2단계 컴포넌트 렌더링에서 파악했던 화면에 노출할 내용을 바탕으로 DOM을 수정한다
초기 렌더링
appendChildAPI를 사용해 생성한 모든 DOM 노드를 화면에 표시
리렌더링
- 렌더링 단계에서 계산한 이전 렌더링 이후 변경된 내용을 적용해 DOM이 최신 렌더링과 일치하도록 함
- React는 렌더링 간에 차이가 있는 경우에만 DOM 노드를 변경한다.
React에서의 렌더링은 컴포넌트를 호출해 이전 렌더링과 변경 사항을 계산하여 화면에 노출할 내용을 파악하는 단계이다.
반면, 보통 렌더링이라고 생각하는 내용을 화면에 그리는 것은 React에서는 커밋 단계라는 것을 알 수 있다.
render 함수 구현
이를 바탕으로 render 함수를 구현해보자. 이번 챕터에서는 컴포넌트 추가, 초기 렌더링만 다룰 것이다.
이후에 리렌더링과 컴포넌트 삭제, 수정에 대해 다룰 예정이다.
createRoot 함수 구현
1단계: 렌더링 트리거에 대해서 알아봤을 때, 초기 렌더링 시에는 createRoot 함수로 리액트의 진입점을 생성하고 생성된 root에 render 함수를 호출한다.
그럼, 먼저 createRoot 함수를 생성해보자
createRoot(domNode, options?) : {
render(children: ReactNode) : void;
unmount() : void;
}
api 문서에서 createRoot 함수에 대한 정의를 참고해 아래와 같이 설계했다.
const createRoot = (domNode) => {
const _root = domNode;
return {
render: function (children) {
// TODO : render 함수 세부 구현
},
};
};
_root: React의 진입점을 나타내는 root 컴포넌트
react-dom/client의 createRoot 함수 실행 후 결과를 콘솔에 출력해보면 _internalRoot 같은 변수가 포함되어 있다.
사진 추가
지금으로서는 정확히 어떤 역할인지에 대해 알 수는 없지만, root에 대한 정보를 저장하고 있으며 렌더링 단계에서 변경 사항 비교 시에 사용 되는 값일 수도 있을까? 정도로 짐작해볼 수 있을 것 같다.
그래서 _root 변수를 추가했고 지금은 루트로 사용할 domNode를 간단히 할당해줬다.
React 만으로 작성된 앱일 경우 root 컴포넌트에서 unmount를 호출할 일이 없기 때문에 따로 unmount는 구현하지 않았다.
재귀적 함수 렌더링
const root = createRoot(document.getElementById("root"));
root.render(<App />);
render 함수를 통해 렌더링 되는 <App /> 컴포넌트는 jsx가 아니라 일반 자바스크르립트 객체이다.
그러므로 render 함수에서 받는 children 매개변수는 아래와 같은 형식이다.
{
type: ""; // 태그 이름
key: null; // 전달 받은 값이 있을 경우 전달 받은 값으로 할당, 기본은 null
ref: null; // 전달 받은 값이 있을 경우 전달 받은 값으로 할당, 기본은 null
props: {
children: []; // 자식 노드 :: 문자열, 숫자, 빈 노드, 리액트 엘리먼트 등등
}
}
중첩 컴포넌트들은 children으로 정의되어 있기 때문에, 더이상 children이 없을 때까지 재귀함수를 실행시키면 중첩 컴포넌트 렌더링을 구현할 수 있다.
render: function (children) {
const element = children;
const dom = document.createElement(element.type);
if (typeof children === 'string' || typeof children === 'number') {
const textNode = document.createTextNode(element.props.children);
dom.appendChild(textNode);
} else if (Array.isArray(children)) {
element.props.children.forEach((child) => {
render(child); // 중첩 컴포넌트 재귀
});
} else {
render(children); // 중첩 컴포넌트 재귀
}
root.appendChild(dom);
},
하지만, 이렇게 재귀 랜더링을 구현하게 될 경우에는 올바르게 중첩이 되지 않는다. root.appendChild(dom); 호출로 인해, div 태그 안에 쌓여야할 h1 태그와 button 태그가 root(#root) 태그 안에 쌓이게 된다.
올바른 중첩 관계를 가질 수 있도록 별도의 함수를 만들어서 생성하도록 하겠다.
const updateContainer = function (element, container) {
if (!element) return;
if (typeof element === 'string' || typeof element === 'number') {
const textNode = document.createTextNode(element);
container.appendChild(textNode);
return;
}
const dom = document.createElement(element.type);
const { children } = element.props;
if (!children) {
// no-op
}
// 텍스트 노드 생성
else if (typeof children === 'string' || typeof children === 'number') {
const textNode = document.createTextNode(element.props.children);
dom.appendChild(textNode);
}
// 여러 자식 컴포넌트
else if (Array.isArray(children)) {
element.props.children.forEach((child) => {
updateContainer(child, dom);
});
}
// 단일 자식 컴포넌트
else {
updateContainer(children, dom);
}
container.appendChild(dom);
};
render: function (children) {
const root = _root;
updateContainer(children, root);
},
props 할당
const Counter = () => {
let count = 0;
return (
<button
id="counter"
type="button"
onClick={() => {
alert(++count);
}}
>
click
</button>
);
};
위 코드처럼 button 태그에 id, type 와 같이 props를 추가할 수도 있고, onClick과 같이 이벤트 리스너를 추가할 수도 있다.
속성 정보들은 props 에 담기기 때문에 이를 활용해 구현해보도록 하겠다.
props에 있는 속성들을 크게 3부분으로 나눌 수 있다.
- 이벤트 : 'on'으로 시작하며, 값에는 함수가 할당됨
- Element에 없는 속성 : HTML Element 객체에 없는 속성
- Element에 있는 속성 : HTML Element 객체에 있는 속성
const updateContainer = function (element, container) {
if (!element) return;
if (typeof element === "string" || typeof element === "number") {
const textNode = document.createTextNode(element);
container.appendChild(textNode);
return;
}
const dom = document.createElement(element.type);
// props 추가
Object.entries(element.props)
.filter(([name]) => name !== "children") // props 중 children 제외
.forEach(([name, value]) => {
// event 일 경우 - `addEventListener`로 이벤트 연결
if (name.startsWith("on") && typeof value === "function") {
const eventName = name.toLowerCase().slice(2);
dom.addEventListener(eventName, value);
return;
}
// style 속성 할당 : HTML Element에 있는 속성 - vlaue 객체를 한 번 더 파싱해서 전달
if (name === "style") {
Object.entries(value).forEach(([styleName, styleValue]) => {
dom.style[styleName] = styleValue;
});
return;
}
// HTML Element에 있는 속성 - dom 객체에 바로 할당
if (name in dom) {
dom[name] = value;
}
// HTML Element에 없는 속성 - setAttribute 함수 사용
else {
dom.setAttribute(name, value);
}
});
const { children } = element.props;
if (!children) {
// no-op
} else if (typeof children === "string" || typeof children === "number") {
const textNode = document.createTextNode(element.props.children);
dom.appendChild(textNode);
} else if (Array.isArray(children)) {
element.props.children.forEach((child) => {
updateContainer(child, dom);
});
} else {
updateContainer(children, dom);
}
container.appendChild(dom);
};
HTML Element에 있는 속성들은 MDN - HTML 특성 참고서에서 확인할 수 있다
🍕 회고
오늘 내가 구현한 것들은 jsx를 브라우저 화면에 보이도록 렌더링 하는 것이다.
jsx를 작성하고 태그 안에 속성들을 사용하고, 이벤트를 추가하고.
지금까지 React를 사용하며 당연하게 사용해 오던 것들을 직접 구현하는 과정에서 '당연하게 사용했는데, 당연하지 않았던 거구나' 하는 생각이 들었다.
오히려 이번 기회를 통해 다시 한 번 이런 부분들에 대해 생각해 볼 수 있는 시간이어서 좋았고, 관련 문서들도 블로그를 정리하려고 한 번더 찾아보게 되면서 확실하게 알고 넘어가는 시간이 되었던 것 같다. 그리고 구현해 가는 과정도 너무 재밌었다. 앞으로 남은 구현들도 내가 당연하게 사용했지만, 그 내부의 미지의 동작들을 공부하고 구현해가는 과정일텐데 남은 시간들이 기대된다!