일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- 상태 끌어올리기
- deep
- react
- Java
- 가상 DOM
- 꿀팁
- virtual Dom
- axios
- 프로젝트
- Hook
- 유용한 사이트
- Dive
- 상태
- 개발
- DoM
- 요청
- 원리
- API
- EventListener
- 백준
- LeetCode
- BOJ
- memory
- programmers
- State
- 프로그래머스
- javascript
- Today
- Total
탄탄한 기본기!
[React] hook 내부 동작 원리 (feat. VirtualDOM) 본문
React 내부 동작 원리를 알아야 하는 이유
React는 아주 많이 추상화되어 있는데, 일반적으로 React를 공부하는 개발자라면 React가 내부적으로 VirtualDOM을 사용하고 있다는 사실을 다들 알 것이다. (모른다면...? Virtual DOM and Internals)
그리고 React가 16.8 버전업이 되며 hook이라는 개념이 등장한 후 많은 (거의 대부분...) 개발자들이 이 hook에 대해서 공부하고, 사용하고 있다 (참고). 그 이유는 hook이 아주 편리하고, 사용하기 간편하며 심지어 가독성까지 좋기 때문인데 라이브러리가 깊게 추상화가 되어있어서 그런 것이라고 생각한다.
이렇게 사용하기 쉬움에도 불구하고, 내부 동작 원리를 정확하게 이해하지 못하고 사용할 경우 예상치 못한 에러들을 만나게 된다. 실제로 인터넷을 보면, React 질문글들 중에 React hook에 대한 문서를 한 번이라도 읽어보았으면 하지 않았을 질문이 굉장히 많다.
모두 내부적인 동작 원리를 이해하지 않은 채, 단지 쉽게 사용하려고 하기 때문이라고 생각한다. 그래서 본인 또한 그렇게 되지 않기 위해서 React를 공부하기로 한 이상 확실하게 기본을 가져가자는 마음으로 React의 hook을 파보려고 한다.
React.useState
React 공식 문서에서는 Basic Hooks라는 이름으로 useState, useEffect, useContext 세 가지 훅을 소개하고 있으며, 먼저 useState부터 천천히 구현해보며 어떻게 추상화되어있는지 그 원리와 동작 방식을 이해해본다.
React(hook)를 처음 접하게 되었을 때 가장 먼저 접하게 되는 기초, 입문 단계의 hook이다. 실제로 공식 문서에서도 처음으로 소개하고 있다. 아주 간단한 코드로 컴포넌트의 상태를 정의하고, 업데이트할 수 있는 훅이다.
import React, { useState } from 'react';
export default function Car() {
const [color, setColor] = useState('blue'); // 상태 정의
return <button>{color}</button>;
}
단 한 줄로 함수형 컴포넌트에서 상태를 정의해 관리할 수 있는 것이다. 그리고 color 변수에 저장되어 있는 상태를 그대로 가져다 사용하면 되고, 만약 상태를 변경하고 싶은 경우에는 setColor를 통해서 이를 변경해줄 수 있다.
import React, { useState } from 'react';
export default function Car() {
const [color, setColor] = useState('blue'); // 상태 정의
const changeColor = (newColor) => {
setColor(newColor); // 상태를 'newColor'로 변경
};
return <button>{color}</button>;
}
이렇게 동작하는 아주 간단한 React useState 훅을 한번 직접 구현해보겠다.
function useState(initalValue) {
let state = initalValue; // 상태 정의
const setState = (newValue) => {
state = newValue; // 상태 변경
};
return [state, setState];
}
// 상태 관리
const [num, setNum] = useState(5);
console.log(num); // 초기 값 설정 → 5
setNum(10); // 10으로 상태 변경
console.log(num); // 10(?)
끝! 이렇게 하면 아주 간단하게 useState의 기능 구현이 끝나는 것일까? 간단하게 테스트를 해보겠다.
결과를 보면 알겠지만, setNum()이 제대로 동작하지 않았다. 아니, setNum() 은 호출되어 잘 동작했지만 바라보는 setNum이 기억하는 num은 useState가 초기에 호출될 때의 그 오래된 state 값인 것이다. 그래서 아무리 그 값을 바꿔본들, 바뀐 값을 참조하지 못하고 이미 반환되어버린 예전의 num을 바라보아 항상 5가 출력되는 것이다.
그럼 당연하게도, 어떤 자료구조를 통해서 이 값을 기억하면 좋겠지만, 우리는 자바스크립트의 아주 강력한 "클로저"라는 개념을 알고 있기 때문에 이를 통해서 문제에 접근하도록 해보겠다.
setNum이 변경하는 num을 참조해줘야 하기 때문에 코드를 다음과 같이 수정할 수 있을 것이다.
function useState(initalValue) {
let state = initalValue;
const getState = () => state; // 변경된 후에도 그 값을 참조하기 위해서 다시 호출되어 state를 참조함
const setState = (newValue) => {
state = newValue;
};
return [getState, setState];
}
const [num, setNum] = useState(5); // num은 getter 함수이다.
console.log(num()); // 함수 호출 → state 참조
setNum(10);
console.log(num()); // 함수 호출 → 새로운 state 참조
하지만, 우리가 아는 useState는 상태를 얻는 getter 함수로 받지 않고, 상태 그 자체를 받기 때문에 위 코드를 수정해야 할 필요가 있다.
잘 생각해보면 useState는 한번만 호출되고, 이후부터는 state, setState 만으로 상태를 관리해주어야 하기 때문에 state가 값 그 자체라면, 항상 오래된 값을 바라보고 있을 수밖에 없다.
그럼 useState를 재차 호출해주어 업데이트된 state 값 자체를 반환해주면 어떨까? 실제로도, React는 상태가 변경되면 컴포넌트를 다시 렌더링(함수 재실행) 한다. 예를 들면, 다음과 같은 방식이다.
const useState = (function () {
let state; // 이전 값이 존재하는지 알기 위한 클로저 자유 변수
return function (initalValue) {
state = state ?? initalValue; // 이전 값이 존재한다면 (이미 한 번 호출되었다면 그대로 사용)
const setState = (newValue) => {
state = newValue;
};
return [state, setState];
};
})();
let [num, setNum] = useState(5);
console.log(num);
setNum(10);
[num, setNum] = useState(5); // 처음 초기값과 같은 값으로 재호출 (하지만, 이미 호출한적이 있기 때문에 값이 변경되지 않음)
console.log(num);
유심히 보면 알겠지만, 위의 useState는 상태("state")를 보관하는 장소가 하나밖에 없다. 즉, useState를 여러 번 쓸 경우 이전 값이 존재하기 때문에 새롭게 상태를 관리할 수 없고 강제로 처음 선언해준 상태를 계속 사용해야 할 뿐이다.
const useState = (function () {
let state; // 이전 값이 존재하는지 알기 위한 클로저 자유 변수
return function (initalValue) {
state = state ?? initalValue; // 이전 값이 존재한다면 (이미 한 번 호출되었다면 그대로 사용)
const setState = (newValue) => {
state = newValue;
};
return [state, setState];
};
})();
let [num, setNum] = useState(5); // 이미 5로 하나밖에 없는 상태를 초기화해버림
let [text, setText] = useState('not working'); // 위처럼 마찬가지로, 같은 state를 공유하기 때문에 제대로 동작하지 않는다.
console.log(num, text); // 5 5
즉, 우리는 관리해야할 상태가 2개 이상이 될 경우를 대비해야 한다. 그럼 리스트나 객체를 두어서 useState가 호출될 때 본인들만의 "방"을 만들어주면 되지 않을까?
const useState = (function () {
let states = []; // 각각의 상태 저장소
let idx = 0; // 각각 상태 저장소에 접근하기 위한 인덱스
return function (initalValue) {
const _idx = idx; // 매번 업데이트되는 idx를 바라보지 않도록 자신만의 인덱스를 기억하는 변수를 선언해줘야 함
states[idx] = states[idx] ?? initalValue; // 이전 값이 존재한다면 (이미 한 번 호출되었다면 그대로 사용)
// 이 값은 새롭게 저장되는 값이기 때문에 새 idx를 바라봐야 함
const setState = (newValue) => {
states[_idx] = newValue; // 자신의 상태를 업데이트 해야하기 때문에 _idx 참조
console.log(states); // 상태를 한번 출력해봄
};
idx++; // 인덱스를 늘려서 다음 useState 상태를 저장할 준비를 함
return [states[_idx], setState];
};
})();
let [num, setNum] = useState(5);
let [text, setText] = useState('does it work?');
console.log(num, text); // 5 'does it work?'
setNum(1000);
setText('it works!');
console.log(num, text); // 5 'does it work?'
상태는 관리가 잘 되고 있는 것을 확인할 수 있다. (순서대로 5 → 1000, 'does it work?' → 'it works!')
하지만 console.log 가 변경된 저장 상태를 반영하지 못하는 것을 볼 수 있다. 이것은 위에서 말한 오래된 값을 그대로 계속 참조하고 있는 문제 때문이다. 따라서 해당 useState 함수를 다시 업데이트해서 "return [state, useState]" 를 새롭게 계속해주어야 하는 것이다. 하지만 우리는 이제 상태를 여러 개 관리하고 있기 때문에 단순히 아무 useState를 호출한다고 해서 이전의 useState를 호출하는 것과 같은 효과를 볼 수 없다.
따라서 이제부터는 실제 React처럼 MyReact 객체도 만들어보고, 변경된 상태 반영을 직접 보기 위해 컴포넌트별로 나눠서 useState를 작성하는 방식으로 진행해보겠다.
MyReact 객체를 만들 때는 아까 useState를 모듈 패턴으로 만든 것과 마찬가지로 만들 수 있다.
const MyReact = (function () {
let App; // 렌더링할 최상위 컴포넌트 (후에 가상 DOM 적용할 예정)
let states = []; // 상태 리스트
let idx = 0; // 상태 리스트에 접근할 인덱스
return {
useState(initalValue) {
const _idx = idx; // 매번 업데이트되는 idx를 바라보지 않도록 자신만의 인덱스를 기억하는 변수를 선언해줘야 함
states[idx] = states[idx] ?? initalValue; // 이전 값이 존재한다면 (이미 한 번 호출되었다면 그대로 사용)
// 이 값은 새롭게 저장되는 값이기 때문에 새 idx를 바라봐야 함
const setState = (newValue) => {
states[_idx] = newValue; // 자신의 상태를 업데이트 해야하기 때문에 _idx 참조
this.render(App); // setState가 호출되면 무조건 다시 렌더링
};
idx++; // 다음 useState 준비
return [states[_idx], setState];
},
render(Component) {
idx = 0; // 렌더링을 다시 할 때는 인덱스를 처음부터 다시 useState 호출만큼 돌아야하기 때문에 0으로 초기화
App = Component;
return Component();
},
};
})();
function Car() {
const [color, setColor] = MyReact.useState('red');
const [name, setName] = MyReact.useState('붕붕이');
console.log(name, color); // 렌더링 (console.log로 대체)
return {
setName,
setColor,
};
}
const App = MyReact.render(Car); // 가장 초기에 실행
App.setColor('blue'); // event trigger
App.setName('부릉부릉이'); // event trigger
App.setColor('green'); // event trigger
이제는 조금 더 React 스러워졌다. 모듈 패턴을 활용해 MyReact 객체를 만들어주는데, 이때 각각의 상태들을 저장할 수 있는 공간을 만ㄴ들어놓고 idx를 활용해 각각의 저장소에 접근하는 방식으로 상태들을 관리하고, setState가 호출되면 (상태 비교 후) 다시 렌더링 하는 방식으로 구현해보았다.
물론 이러한 생각을 하는 데에는 정말 오랜 시간과 노력이 들었겠지만, 설명을 들은 후 다시 그대로 구현하려고 하면 생각한 것보다 어렵지 않고, 클로저에 대한 개념이 탄탄하다면 간단하게 구현할 수 있었을 것이다.
Virtual DOM
이때까지 렌더링을 console.log로 대신했는데, 이제 DOM을 활용해 페이지에 실제로 렌더링을 해보자. 먼저 기존에 console.log로 찍었던 모든 내용을 DOM 요소를 생성하도록 모두 수정해야 한다.
const MyReact = (function () {
const $root = document.getElementById('root'); // 요소를 렌더링할 최상위 루트 요소를 얻어온다.
let App; // 렌더링할 최상위 컴포넌트 (후에 가상 DOM 적용할 예정)
let states = []; // 상태 리스트
let idx = 0; // 상태 리스트에 접근할 인덱스
return {
useState(initalValue) {
const _idx = idx; // 매번 업데이트되는 idx를 바라보지 않도록 자신만의 인덱스를 기억하는 변수를 선언해줘야 함
states[idx] = states[idx] ?? initalValue; // 이전 값이 존재한다면 (이미 한 번 호출되었다면 그대로 사용)
// 이 값은 새롭게 저장되는 값이기 때문에 새 idx를 바라봐야 함
const setState = (newValue) => {
states[_idx] = newValue; // 자신의 상태를 업데이트 해야하기 때문에 _idx 참조
this.render(App); // setState가 호출되면 무조건 다시 렌더링
};
idx++; // 다음 useState 준비
return [states[_idx], setState];
},
render(Component) {
App = Component;
idx = 0; // 렌더링을 다시 할 때는 인덱스를 처음부터 다시 useState 호출만큼 돌아야하기 때문에 0으로 초기화
$root.innerHTML = '';
$root.appendChild(Component()); // 컴포넌트에서 요소를 얻어온다.
},
};
})();
function Car() {
const [color, setColor] = MyReact.useState('red');
const [name, setName] = MyReact.useState('붕붕이');
const $car = document.createElement('span'); // span 태그에 내용을 담아서 반환한다.
$car.textContent = `이름: ${name}, 색상: ${color}`; // 내용 추가
return $car; // DOM 요소 반환
}
const App = MyReact.render(Car); // 가장 초기에 실행
이 경우, Car 컴포넌트를 MyReact.render를 통해서 렌더링 해주고 있다. 그리고 여기서는 setColor와 setName을 trigger 할 수 있는 아무런 기능도 없다. 왜냐하면 페이지에 단순히 현재 상태(초기값)만을 렌더링하고 있으며, 바꿔줄 상태를 렌더링 하고 있지 않기 때문이다.
그럼 실제 React에서 처럼 Props를 넘겨주고, 받고 하기 위해서는 컴포넌트가 다른 컴포넌트를 포함할 수 있어야 하고, 이런 정보들을 포함한 가장 최상위의 컴포넌트(ex. App.tsx) 를 재귀적으로 렌더링 하며 모든 자식 컴포넌트들을 함께 렌더링 해주어야 한다.
그럼 그런 구조를 위해서 React처럼 가상 DOM을 사용해 구현해보도록 하자.
// 보통 가상 노드 creator 함수를 h라고 표현하는 것 같다.
function h(type, prop, ...children) {
return { type, prop, children }; // type과 props, 그리고 children을 자식으로 가지는 객체를 생성하여 반환한다.
}
export { h };
가상 DOM은 단순히 자바스크립트의 객체로 DOM의 상태를 표현한 것일 뿐이다. 즉 태그의 타입과 props, 그리고 자식 관계를 가지고 있는 셈이다.
그리고 이러한 가상 DOM을 실제 html에 렌더링 할 DOM으로 변환해주는 작업이 필요하다.
// function h { ... }
function createElement(node) {
const $el = document.createElement(node.type); // node의 type 값으로 DOM 요소를 생성한다.
// 모든 prop 객체의 내부 요소들에 대해서 $el의 attribute로 추가해준다.
Object.entries(node.prop ?? {}).forEach(([attr, val]) => {
$el[attr] = val; // $el.setAttribute(attr, val) 의 경우에는 함수로 이벤트 등록이 불가능해서 이렇게 작성해보았다.
});
// 자식이 있는 경우 재귀적으로 모두 추가.
if (node.children) {
const children = node.children.map(createElement);
children.forEach(($child) => {
$el.appendChild($child);
});
}
return $el;
}
export { h, createElement };
이제 이렇게 만들어진 가상 DOM (트리)를 활용해 직접 html에 렌더링 하고, App 컴포넌트에서 상태를 관리해 차의 정보를 바꿀 수 있도록 코드를 수정해보겠다.
import { createElement, h } from './VirtualDOM.js';
const MyReact = (function () {
const $root = document.getElementById('root'); // 요소를 렌더링할 최상위 루트 요소를 얻어온다.
let App; // 렌더링할 최상위 컴포넌트 (후에 가상 DOM 적용할 예정)
let states = []; // 상태 리스트
let idx = 0; // 상태 리스트에 접근할 인덱스
return {
useState(initalValue) {
const _idx = idx; // 매번 업데이트되는 idx를 바라보지 않도록 자신만의 인덱스를 기억하는 변수를 선언해줘야 함
states[idx] = states[idx] ?? initalValue; // 이전 값이 존재한다면 (이미 한 번 호출되었다면 그대로 사용)
// 이 값은 새롭게 저장되는 값이기 때문에 새 idx를 바라봐야 함
const setState = (newValue) => {
// 상태가 바뀌지 않았을 경우에는 re-rendering을 수행하지 않음
if (Object.is(newValue, states[_idx])) {
return;
}
states[_idx] = newValue; // 자신의 상태를 업데이트 해야하기 때문에 _idx 참조
this.render(App); // setState가 호출되면 무조건 다시 렌더링
};
idx++; // 다음 useState 준비
return [states[_idx], setState];
},
render(Component) {
App = Component;
idx = 0; // 렌더링을 다시 할 때는 인덱스를 처음부터 다시 useState 호출만큼 돌아야하기 때문에 0으로 초기화
$root.innerHTML = '';
$root.appendChild(createElement(Component()));
},
};
})();
function App() {
// App 컴포넌트에서 상태를 선언하고 관리하며 props로 하위 컴포넌트 (ex. Car) 에게 상태를 내려줌
const [color, setColor] = MyReact.useState('red');
const [name, setName] = MyReact.useState('붕붕이');
return h(
'div',
{},
Car({ name, color }),
h(
'form',
{
action: '#',
onsubmit: function (e) {
e.preventDefault();
// input tag에서 원하는 값을 입력했을 때 setName or setColor를 trigger 해줌
Array.from(e.currentTarget).forEach(($el) => {
switch ($el.name) {
case 'carName':
setName($el.value || name); // 만약 아무 값도 입력하지 않았다면 기존 name을 그대로 사용
break;
case 'carColor':
setColor($el.value || color); // 만약 아무 값도 입력하지 않았다면 기존 color를 그대로 사용
}
});
},
},
h(
'div',
{},
h('label', { for: 'carName', textContent: '바꿀 이름: ' }),
h('input', { name: 'carName' })
),
h(
'div',
{},
h('label', { for: 'carColor', textContent: '바꿀 색상: ' }),
h('input', { name: 'carColor' })
),
h('button', { type: 'submit', textContent: 'change' })
)
);
}
// name과 color 라는 props를 받아서 이를 렌더링 해줌
function Car({ name, color }) {
return h('span', { textContent: `이름: ${name}, 색상: ${color}` });
}
MyReact.render(App); // 가장 초기에 실행
위 gif에서 확인할 수 있는 것처럼, 이제는 직접 구현한 MyReact에서 2개 이상의 상태 관리를 할 수 있게 되었고, props도 하위 컴포넌트에 전달할 수 있으며, useState 훅을 통해서 상태가 바뀔 때마다(Object.is 비교 활용) re-rendering이 발생할 수 있도록 하였다. Virtual DOM을 활용해 React의 핵심 기능을 간단하게 구현할 수 있게 되었다.
하지만 해결해야 할 큰 문제가 하나 남아있다. 바로 현재 MyReact에서 렌더링 할 때 div#root 내부의 모든 내용을 모조리 re-render 하기 때문에 성능상 문제가 발생할 수 있는 것이다.
render(Component) {
App = Component;
idx = 0;
$root.innerHTML = ''; // 이 부분
$root.appendChild(createElement(Component()));
},
이때 필요한 것이 Diff 알고리즘이다. 이 알고리즘을 활용해야지만 모든 요소들이 상태 변경에 상관없이 모두 re-rendering이 되는 현상을 방지할 수 있다. 모든 요소들이 인터렉션마다, 상태가 바뀔 때마다 계속해서 렌더링 된다고 생각해보면 끔찍하다.
이때까지 만들어 놓은 Virtual DOM을 활용해서, type과 prop, 그리고 children 정보들을 활용해 모든 Virtual DOM을 재귀적으로 순회하며 불변성을 검증해줄 것이고, 만약 바뀌었다면 그 하위의 모든 컴포넌트들(Virtual DOM)을 다시 렌더링 하는 전략을 사용할 수 있을 것이다.
import { createElement, h } from './VirtualDOM.js';
// Props가 다른지 확인하는 순수함수
function isDiffProps(oldProp, newProp) {
const oldPropKeys = Object.keys(oldProp);
const newPropKeys = Object.keys(newProp);
if (oldPropKeys.length !== newPropKeys.length) {
return true;
}
return oldPropKeys.some(
// 'function' 타입의 경우에는 useCallback의 기능을 구현하지 않았기 때문에 항상 function이
// 계속 만들어지게 되고, value 값을 비교할 때마다 다르다고 판단하기 때문에 form 요소까지도 계속
// re-rendering이 발생해서 임시로 function일 경우는 pass 하도록 처리함
(key) =>
typeof oldProp[key] !== 'function' &&
!Object.is(oldProp[key], newProp[key])
);
}
// 한 Virtual DOM의 Node가 이전 상태와 비교했을 때 달라졌는지 확인하며,
// children 리스트까지 모두 재귀적으로 비교해주며 rendering해주는 side-effect 함수
function updateDOM($root, oldNode, newNode, idx = 0) {
// 새로운 노드가 생성되었다는 뜻으로, 단순히 새 node를 추가해줌
if (!oldNode) {
return $root.appendChild(createElement(newNode));
}
// 만약 현재 상태의 node가 변화했다면 해당 요소를 re-rendering 해줌
if (
oldNode.type !== newNode.type ||
isDiffProps(oldNode.prop, newNode.prop)
) {
return $root.children[idx].replaceWith(createElement(newNode));
}
// 모두 같을 경우에는 children을 재귀 순회하며 updateDOM 호출
const maxLength = Math.max(oldNode.children.length, newNode.children.length);
for (let i = 0; i < maxLength; i++) {
updateDOM($root.children[idx], oldNode.children[i], newNode.children[i], i);
}
}
const MyReact = (function () {
const $root = document.getElementById('root'); // 요소를 렌더링할 최상위 루트 요소를 얻어온다.
let App; // 렌더링할 최상위 컴포넌트 (후에 가상 DOM 적용할 예정)
let oldVDOM; // 이전 virtual DOM
let states = []; // 상태 리스트
let idx = 0; // 상태 리스트에 접근할 인덱스
return {
useState(initalValue) {
const _idx = idx; // 매번 업데이트되는 idx를 바라보지 않도록 자신만의 인덱스를 기억하는 변수를 선언해줘야 함
states[idx] = states[idx] ?? initalValue; // 이전 값이 존재한다면 (이미 한 번 호출되었다면 그대로 사용)
// 이 값은 새롭게 저장되는 값이기 때문에 새 idx를 바라봐야 함
const setState = (newValue) => {
if (Object.is(newValue, states[_idx])) {
return;
}
states[_idx] = newValue; // 자신의 상태를 업데이트 해야하기 때문에 _idx 참조
this.render(App); // setState가 호출되면 무조건 다시 렌더링
};
idx++; // 다음 useState 준비
return [states[_idx], setState];
},
render(Component) {
const vDOM = Component();
App = Component;
idx = 0; // 렌더링을 다시 할 때는 인덱스를 처음부터 다시 useState 호출만큼 돌아야하기 때문에 0으로 초기화
updateDOM($root, oldVDOM, vDOM);
oldVDOM = vDOM;
},
};
})();
function App() {
// App 컴포넌트에서 상태를 선언하고 관리하며 props로 하위 컴포넌트 (ex. Car) 에게 상태를 내려줌
const [color, setColor] = MyReact.useState('red');
const [name, setName] = MyReact.useState('붕붕이');
return h(
'div',
{},
Car({ name, color }),
h(
'form',
{
action: '#',
onsubmit: (e) => {
e.preventDefault();
// input tag에서 원하는 값을 입력했을 때 setName or setColor를 trigger 해줌
Array.from(e.currentTarget).forEach(($el) => {
switch ($el.name) {
case 'carName':
setName($el.value || name); // 만약 아무 값도 입력하지 않았다면 기존 name을 그대로 사용
break;
case 'carColor':
setColor($el.value || color); // 만약 아무 값도 입력하지 않았다면 기존 color를 그대로 사용
}
});
},
},
h(
'div',
{},
h('label', { for: 'carName', textContent: '바꿀 이름: ' }),
h('input', { name: 'carName', autocomplete: 'off' })
),
h(
'div',
{},
h('label', { for: 'carColor', textContent: '바꿀 색상: ' }),
h('input', { name: 'carColor', autocomplete: 'off' })
),
h('button', { type: 'submit', textContent: 'change' })
),
h('div', { textContent: 'not Changed (no re-render)' }) // 변화가 전혀 없는 요소이기 떼문에 렌더링이 일어나면 안됨.
);
}
function Car({ name, color }) {
return h('span', { textContent: `이름: ${name}, 색상: ${color}` });
}
MyReact.render(App); // 가장 초기에 실행
마무리하며...
당연히 Diff를 처리해주는 알고리즘이라던지, useState를 처리하고 상태를 관리하는 로직 등이 완벽히 동일하게 동작하지 않을 것이고 훨씬 더 많은 최적화가 되어있을 것이다.
하지만 이번 포스팅의 핵심은 바닐라 JS로 React 라이브러리를 From Scratch로 구현해 어느 정도 비슷한 형태로 사용할 수 있도록 기능들을 구현했고, 그 과정에서 React (hook)의 동작 원리, 내부 구조 등에 대해서 생각해보고 고민해볼 수 있었다는 점이다. 결과적으로 우리는 최소한
- React Hook (useState)
- Virtual DOM (+ Diff Algorithm)
등에 대해서는 어떻게 동작하는지 이해하고 있기 때문에 hook이 마법 같은 새로운 기능이 아니라 클로저를 활용한, 좋은 trick이라는 것 깨달을 수 있었다. 어떠한 개념에 대한 원리를 알고 적용하는 것과 단순히 사용법만 알고 억지로 부딪히는 것과는 성장 속도에 정말 큰 차이가 있다고 생각한다.
다음에는 React의 useEffect, useMemo, useCallback 등 Dependency를 활용해 조건부로 업데이트하는 다양하고도 유용한 hook 들, 그리고 useState의 방식과 다른 방식으로 상태를 중앙집중적으로 관리해주는 Redux라는 개념에 대해서도 한번 알아보면 좋을 것 같다.
'개인 공부 > React' 카테고리의 다른 글
React Router production 경로 라우팅 문제 (0) | 2022.03.03 |
---|---|
[간단 프로젝트] Break-Boredness (1) | 2021.08.25 |
[React] Lifting State Up - 상태 끌어 올리기 (0) | 2021.08.08 |