2주차 회고 - React 파헤치기
프론트엔드 개발자 신입으로서, 처음에 리액트는 그냥 내가 아는 바닐라자바스크립트를 제외한 프론트엔드 개발 방식의 거의 전부였고, 자바스크립트 웹개발의 여러 불편함들로부터 나를 해방시켜줬던 너무나도 유용한 툴이었다. 널리 그만큼 사용되고있는 명확한 이유들은 분명 존재한다.
내가 당장 생각해보기에도, 가상돔, jsx 구문, react hooks, 활발한 커뮤니티 생태계, 유연성과 확장성 등은 거부할 수 없는 리액트만의 큰 장점임이 분명하다. 그래서인지, 장점들이 나한테는 아직 많이 크게 다가오는듯 해서 딱히 단점이라고는 생각해본 적도 없었고, 리액트를 사용하지 않고 개발한다는 것에 대해서는 아직 생각해 본 적도 없는 것을 보니, 불만도 없었나보다.
하지만 리액트도 프론트엔드 개발을 위한 수많은 방법 중 하나일 뿐이고, 내가 아직 느끼지 못했을 뿐, 장단점 역시 명확히 존재한다고 생각한다. 그렇게 생각하니, 리액트 사용법이 아닌, 그 툴 자체에 대한 공부가 더 필요하다고 생각이 들었고, 내가 작성한 코드가 리액트를 통해 화면에 그려지기까지 어떤 일들이 발생하는지, api는 어떤 것들이 있는지 찬찬히 살펴볼 수 있었던 시간이었다.
과제1) Vanilla Javascript로 React와 유사한 렌더링 시스템 만들기
뭔가 두번째 주 부터, 제대로된 과제를 받은 느낌. 너무나도 당연히 쓰던 리액트였기에, 이걸 자바스크립트로 어떻게 만들지…? 막막한 생각부터 들었던 것 같다. 일단 바닐라 자바스크립트에서 DOM을 그릴 때의 방식과 리액트의 방식을 두고 비교해봤는데, 확실히 리액트의 장점이 더 잘 보이는 것 같았다. HTML은 선언적인 구조만을 담고 있다면, React는 상태값이나 데이터와 로직에 대한 영역도 같이 관리하고, 재사용 가능하며 확장성 있도록 설계할 수 있다. 이렇게 편리하게 웹개발을 하게 해주는 리액트도, 그 기반에는 어차피 다 document.createElement()로 동작하고 있을 것이란 생각을 하니, 조금은 만만해보이기도 했다.
가상돔 만들기
- 실제DOM
<div id="app">
<ul>
<li>
<input type="checkbox" class="toggle" />
todo list item 1
<button class="remove">삭제</button>
</li>
<li class="completed">
<input type="checkbox" class="toggle" checked />
todo list item 2
<button class="remove">삭제</button>
</li>
</ul>
<form>
<input type="text" />
<button type="submit">추가</button>
</form>
</div>
- 가상DOM
virtualDom('div', { id: 'app' },
virtualDom('ul', null,
virtualDom('li', null,
virtualDom('input', { type: 'checkbox', className: 'toggle' }),
'todo list item 1',
virtualDom('button', { className: 'remove' }, '삭제')
),
virtualDom('li', { className: 'completed' },
virtualDom('input', { type: 'checkbox', className: 'toggle', checked: true }),
'todo list item 2',
virtualDom('button', { className: 'remove' }, '삭제')
),
),
virtualDom('form',
virtualDom('input', { type: 'text' }),
virtualDom('button', { type: 'submit' }, '추가'),
)
);
- 커스텀 jsx 함수를 통해 가상 돔 만들기
export function jsx(type, props, ...children) {
return { type, props, children: children.flat() }
}
- createElement 함수로 virtualDom → RealDom 변환
function createElement(node) {
if (typeof node === 'string') {
// text node를 만들어서 반환한다.
return document.createTextNode(node);
}
// tag에 대한 element를 만든다.
const $el = document.createElement(node.type);
// 정의한 속성을 삽입한다.
Object.entries(node.props || {})
.filter(([attr, value]) => value)
.forEach(([attr, value]) => (
$el.setAttribute(attr, value)
));
// node의 children virtual dom을 dom으로 변환한다.
// 즉, 모든 VirtualDOM을 순회한다.
const children = node.children.map(createElement);
// $el에 변환된 children dom을 추가한다.
children.forEach(child => $el.appendChild(child));
// 변환된 dom을 반환한다.
return $el;
}
- 변경된 속성이나 태그 등 업데이트
function updateElement (parent, newNode, oldNode, index = 0) {
// 1. oldNode만 있는 경우
if (!newNode && oldNode) {
return parent.removeChild(parent.childNode[index]);
}
// 2. newNode만 있는 경우
if (newNode && !oldNode) {
return parent.appendChild(createElement(newNode));
}
// 3. oldNode와 newNode 모두 text 타입일 경우
if (typeof newNode === "string" && typeof oldNode === "string") {
if (newNode === oldNode) return;
return parent.replaceChild(
createElement(newNode),
parent.childNodes[index]
)
}
// 4. oldNode와 newNode의 태그 이름(type)이 다를 경우
if (newNode.type !== oldNode.type) {
return parent.replaceChild(
createElement(newNode),
parent.childNodes[index]
)
}
// 5. oldNode와 newNode의 태그 이름(type)이 같을 경우
updateAttributes(
parent.childNodes[index],
newNode.props || {},
oldNode.props || {}
);
// 6. newNode와 oldNode의 모든 자식 태그를 순회하며 1 ~ 5의 내용을 반복한다.
const maxLength = Math.max(
newNode.children.length,
oldNode.children.length,
);
for (let i = 0; i < maxLength; i++) {
updateElement(
parent.childNodes[index],
newNode.children[i],
oldNode.children[i],
i
)
}
}
// 5 - newNode와 oldNode의 attribute를 비교하여 변경된 부분만 반영한다.
function updateAttributes(target, newProps, oldProps) {
// 달라지거나 추가된 Props를 반영
for (const [attr, value] of Object.entries(newProps)) {
if (oldProps[attr] === newProps[attr]) continue;
target.setAttribute(attr, value);
}
// 없어진 props를 attribute에서 제거
for (const attr of Object.keys(oldProps)) {
if (newProps[attr] !== undefined) continue;
target.removeAttribute(attr)
}
}
- 활용
const $root = document.createElement('div')
const App = jsx('div', { id: 'test-id', class: 'test-class' }, jsx('p', null, '첫 번째 문단'), jsx('p', null, '두 번째 문단'))
updateElement($root, App)
console.log($root.innerHTML)
// <div id="test-id" class="test-class"><p>첫 번째 문단</p><p>두 번째 문단</p><p>세 번째 문단</p></div>
- 알게된 것
- 가상돔(VirtualDOM)은 거창한게 아니라 DOM의 형태를 본따 만든 객체 덩어리다.
- DOM에 변경이 있을 경우 렌더트리를 재생성하고(모든 요소들의 스타일이 다시 계산됨) 레이아웃을 만들고 페인팅을 하는 과정이 다시 반복된다. 즉, 브라우저가 연산을 많이 해야한다는 이야기. 전체적인 프로세스를 비효율적으로 만든다는 것.
- 뷰(HTML)에 변화가 있을 때, 구 가상돔(Old Node)과 새 가상돔(New Node)을 비교하여 변경된 내용만 DOM에 적용한다. 이를 통해 브라우저 내에서 발생하는 연산의 양(정확히는 렌더링 과정)을 줄이면서 성능이 개선
- 가상돔의 가독성을 해결하기 위한 것이 jsx
- VirtualDOM을 RealDOM으로 변경하는 과정이고, 성능상의 이점을 가져오기 위해선 Diff 알고리즘 을 통해서 변경된 속성이나 태그에 대해 업데이트 하는 과정이 필요하다
과제B. useState, useMemo 구현
useState
useState. 리액트를 사용하며 가장 많이 사용하는 hook 중에 하나이기도 하지만, 그 동작 방식에 대해 궁금하긴 했다. 간단히 만들 수 있을 것 처럼 보였지만, 은근히 생각해야할 것들이 많았다.
- useState를 실행하면 첫 번째 인자는 state를 반환하고, 두 번째 인자는 state를 변경하는 setState를 반환하다. 그리고 setState 를 실행하면 render가 실행
- 컴포넌트 단위로 상태값을 유지
- setState를 통해 상태값 업데이트
- 컴포넌트가 리렌더 되어도 값은 초기화되지 않고 유지된다.
- state의 값이 이전과 동일할 경우, 다시 실행되지 않는다.
- setState가 동시에 여러번 실행될 경우, 마지막 setState에 대해서만 render가 호출된다.
export function createHooks(callback) {
const stateContext = {
current: 0,
states: [],
}
function resetContext() {
stateContext.current = 0
}
const debounce = (callback) => {
let next = -1
return () => {
stateContext.current += 1
cancelAnimationFrame(next)
next = requestAnimationFrame(callback)
}
}
const useState = (initState) => {
const { current, states } = stateContext
states[current] = states[current] ?? initState
const debouncedCallback = debounce(callback)
const setState = (newState) => {
if (newState === states[current]) return
states[current] = newState
debouncedCallback()
}
return [states[current], setState]
}
return { useState, resetContext }
}
클로저
💡 클로저(Closure)는 내부 함수가 외부(둘러싸는) 함수의 변수에 접근할 수 있는 JavaScript의 기능. 이는 외부 함수의 실행이 완료된 후에도 내부 함수가 외부 함수의 변수 및 매개변수에 접근할 수 있음을 의미한다.
useState가 반환되기 전 render 함수가 정의되었는데, 어떻게 render 안에서 useState 실행될까?
createHooks 가 실행되기 전에 render가 정의되었더라도 폐쇄 메커니즘으로 인해 useState는 render 내에서 계속 사용할 수 있다.
- 함수 정의: createHooks가 정의되면 useState 함수도 정의되지만 실행되지는 않는다. 그러나 주변 범위(stateContext, callback 및 render)에 대한 참조는 유지.
- createHooks 실행: createHooks(render)가 호출되면 useState 함수가 실행되어 반환된다. 이 시점에서 useState 는 완전히 초기화되었으며 render 함수를 포함하여 주변 범위에 액세스할 수 있다.
- render 실행: 이제 render() 가 호출되면 useState("foo")는 이전에 실행된 createHooks 에서 반환된 useState 함수를 호출. useState 는 정의 중에 이미 주변 범위를 캡처했으므로 createHooks 가 실행되기 전에 render 가 정의되었더라도 stateContext, callback 및 render에 액세스할 수 있다.
useMemo
...to be continue
'개발 일기' 카테고리의 다른 글
컴포넌트 주도 개발(CDD)을 도입해보면 어떨까? (feat: 디자인시스템 라이브러리 & 스토리북) (1) | 2024.11.06 |
---|---|
노션 블록 데이터를 활용한 NotionRenderer 설계 및 구현 과정 (2) | 2024.11.03 |
음대생, 프론트엔드개발자, 스타트업에서의 1년, 회고 (4) | 2024.09.04 |
React로 만든 일정 관리를 위한 백오피스 캘린더 컴포넌트 (2) | 2024.08.31 |
230316 항해 1주차 회고 (1) | 2024.03.26 |