티스토리 뷰
ChatGPT에게 사람들이 React를 사용하는 이유가 무엇인지 물어보았다.
chatGPT의 답변을 정리해보자면 사람들이 React를 사용하는 이유는 다음과 같다.
1) 재사용이 가능한 컴포넌트 기반 개발이 가능하여 복잡한 UI를 쉽게 관리할 수 있다는 점.
2) Virtual DOM (가상돔)을 사용함으로써 페이지의 나머지 부분에 영향을 주지않고 UI 변경사항을 빠르게 업데이트하고 렌더링하여 성능을 향상시킬 수 있다는 점.
3) 선언적 구문을 사용하여 코드 작성 및 추론이 쉬워진다는 점.
4) 온라인 포럼 및 리소스를 통해 지원을 제공하는 대규모 개발자 커뮤니티가 존재한다는 점.
5) 다른 라이브러리 및 프레임워크와 함께 사용할 수 있어 유연하고 다양한 프로젝트와 호환된다는 점.
6) 확장가능하도록 설계되어 트래픽이 많은 대규모 애플리케이션을 구축하는데 이상적이라는 점.
위와 같이 이유들로 사람들이 React를 많이 사용하는데
조금 더 기술적인 부분에서 프론트엔드 UI 개발 라이브러리로써 React가 성공한 이유를 알아보자.
웹은 DOM(Document Object Model)로 구성되어 있고 이 DOM의 동적 변경을 위해서 웹은 DOM API를 제공한다.
이 DOM API를 다루는 방법은 매우 까다로운데 React는 개발자에게 DOM API를 다룰 필요가 없게 만들었다.
React 문법에 맞춰 state를 관리하면, state를 기준으로 DOM API는 React가 처리하여 DOM을 렌더링한다.
개발자는 React의 선언적인 문법으로 state(변하는 데이터)를 관리해주면 나머지는 React가 알아서 처리하는 방식이다.
이러한 기능을 구현하기 위한 React의 핵심 기능 5가지는 Vitrual DOM, JSX, Real DOM 렌더링, Diffing Update, Hooks이다.
1️⃣ Vitrual DOM (가상 돔)
Vitrual DOM이란 실제 DOM의 형태를 본떠서 만든 객체이다.
실제 DOM의 경량 복사본이라고 볼 수 있다.
예시로 아래와 같은 태그 정보가 있다고 가정하면
<div id="container">
<p>VirtualDOM</p>
</div>
Vitrual DOM은 아래와 같은 객체로 추상화하고
const VirtualDOM = {
tag: 'div',
props: {
id: 'container'
},
children: [
{
tag: 'p',
props: {},
children: ["VirtualDOM"],
},
],
};
위와 같은 객체 형태로 데이터를 계속 추가하기 위해 createElement 함수를 만든다.
function createElement(tagName, props, ...children) {
return { tagName, props, children: children.flat() }
}
const VirtualDOM2 = (
createElement('div', { id: 'container' },
createElement('p', { style: 'color: red' }, '제목 입니다'),
)
)
하지만 복잡한 UI를 개발자가 매번 createElement 함수로 Virtual DOM을 생성해주기엔 어려움이 있다.
이 createElement를 쉽게 사용하기 위해 JSX를 사용한다.
2️⃣ JSX (JavaScript And Xml)
JSX로 작성된 코드는 자바스크립트 컴파일러인 Babel에서 createElement 함수로 변환된다.
JSX를 사용하면 익숙한 마크업 문법으로 사용하여 createElement 함수를 직접 사용하는 것 보다 간단하게 Virtual DOM을 만들 수 있다.
💡 React 없이 JSX 사용하기
JSX는 @babel/preset-react 플러그인에 의해 JSX를 createElement 함수로 변환되는데 React 없이 JSX를 Babel로 Transpile(변환)하려면 소소코드 주석에 @jsx '함수명'을 기입해야 한다.
React와 다르게 React.createElement가 아니라 그냥 createElement로 변환되는 것을 확인할 수 있는데 이는 직접 작성한 createElement함수를 사용할 수 있다는 의미이다.
→ @jsx '함수명'을 기입해 직접 만든 createElement 함수로 JSX코드를 변환할 수 있다.
3️⃣ Vitrual DOM을 Real DOM으로 렌더링
JSX를 활용해 Virtual DOM을 생성하였지만 Virtual DOM은 어디까지나 객체일 뿐이다.
이 객체(Virtaul DOM)을 렌더링하기 위해 DOM API를 사용해야 한다.
RealDOM에 반영하는 DOM을 생성하는 renderRealDOM 함수(재귀 호출로 DOM API를 이용해 태그를 생성하는 함수)를 아래와 같이 작성해볼 수 있다.
1) Virtual DOM의 tagName을 바탕으로 document.createElement API를 이용하여 태그를 생성한다.
2) VirtualDOM의 자식(children) 구조가 동일하므로 재귀 호출로 renderRealDOM을 호출한다.
3) 각 children Node 데이터를 appendChild API로 Element를 추가한다.
4) 가장 끝 하위 요소 children은 String이기 때문에 예외처리를 하고 createTextNode로 TextNode를 생성한다.
export function createElement(tagName, props, ...children) {
return { tagName, props, children: children.flat() }
}
const jsxDOM = <div id="container">
<p>제목 입니다</p>
</div>
export function renderRealDOM(VirtualDOM){
// 4)
if(typeof VirtualDOM === 'string'){
return document.createTextNode(VirtualDOM)
}
// 1)
const $Element = document.createElement(VirtualDOM.tagName);
// 2), 3)
VirtualDOM.children
.map(renderRealDOM)
.forEach(node => $Element.appendChild(node));
return $Element;
}
4️⃣ Diffing Update 적용
Virtual DOM은 객체이기 때문에 Old Virtual DOM과 New Vitrual DOM을 비교해서 변경된 부분만 업데이트가 가능하다.
비교적 용량이 큰 Real DOM과 업데이트된 Real DOM을 비교하여 변경된 부분을 적용시키는 것이 아니라,
비교적 용량이 작은 Virtual DOM과 업데이트된 Virtual DOM을 비교하여 빠르게 UI를 업데이트한다.
💡 React 없이 JSX로 VirtualDOM을 구현하고 renderRealDOM으로 렌더링, diffingUpdate를 적용한 예시 코드
쉬운 예시를 위해 diffing Update 함수는 텍스트가 변경된 경우에 한에서만 diffing Update를 구현한 코드이다.
diffingUpdate 함수를 재귀 호출함으로써 모든 자식 태그를 순회하며
함수의 인자로 부모 노드, 변경할 노드, 이전 노드, parentIndex를 받아 replaceChild DOM API로 변경된 부분만 업데이트한다.
// react.js
export function createElement(tagName, props, ...children) {
return { tagName, props, children: children.flat() }
}
export function renderRealDOM(VirtualDOM){
if(typeof VirtualDOM === 'string'){
return document.createTextNode(VirtualDOM)
}
const $Element = document.createElement(VirtualDOM.tagName);
VirtualDOM.children
.map(renderRealDOM)
.forEach(node => $Element.appendChild(node));
return $Element;
}
export function diffingUpdate (parent, nextNode, previousNode, parentIndex = 0) {
if (typeof nextNode === "string" && typeof previousNode === "string") {
if (nextNode === previousNode) return;
return parent.replaceChild(
renderRealDOM(nextNode),
parent.childNodes[parentIndex]
)
}
for (const [index] of nextNode.children.entries()) {
diffingUpdate(
parent.childNodes[parentIndex],
nextNode.children[index],
previousNode.children[index],
index
)
}
}
// app.js
/* @jsx createElement */
import { createElement, renderRealDOM, diffingUpdate } from './react';
const previousState = [
{ title: '에스프레소' },
{ title: '아메리카노' },
];
const nextState = [
{ title: '에스프레소' },
{ title: '아메리카노 샷추가1' },
];
const CoffeeList = (state) => (
<ul>
{ state.map(({ title }) => (
<li>{ title }</li>
)) }
</ul>
)
const previousNode = CoffeeList(previousState);
const nextNode = CoffeeList(nextState);
const $root = document.querySelector('#root');
$root.appendChild(renderRealDOM(previousNode));
setTimeout(() =>
diffingUpdate($root, nextNode, previousNode),
2000
);
5️⃣ Hooks 구현
클래스 컴포넌트의 경우, 최초로 생성되는 컴포넌트만 새롭게 인스턴스를 만들고, 컴포넌트가 삭제되기 전까지 만들어진 인스턴스를 통해 render 메서드를 이용하여 상태 변경을 감지(setState)한다.
→ 해당 인스턴스에서 필요한 부분만 업데이트하여 context 상태를 계속 유지 할 수 있다.
함수 컴포넌트의 경우, props를 인자로 받아 JSX 문법에 맞는 React Component를 반환해주기 때문에 함수 컴포넌트의 호출은 무조건 렌더링을 일으킨다.
(이미 만들어진 인스턴스를 가지고 render만 호출하는 클래스 컴포넌트와는 다르게, 함수 컴포넌트는 상태가 변경될 때마다 새로운 인스턴스를 생성하기 때문)
따라서 함수 컴포넌트는 호출될 때마다 늘 동일한 상태, 초기화된 상태만 가질 수 있었는데, React16.8 버전부터 함수 컴포넌트에서도 상태를 갖고 유지할 수 있는 Hook을 제공해주었다.
Hook은 함수 컴포넌트에서 상태를 정의하고 수정할 수 있는 기능이다. Hook을 사용하면 함수 컴포넌트가 다시 실행되어도 해당 함수의 상태값이 초기화되지 않고, React에 의해 사라지지 않는다.
컴포넌트에서 상태를 관리할 수 있게 해주는 useState 함수를 간단히 구현해보면 아래와 같다.
let hookState = undefined; // 4) useState 함수 외부에 두어 useState 호출과는 상관없이 데이터를 유지 (클로저)
export function useState (initState) {
if (hookState === undefined) hookState = initState; // 1) 초기값이 설정되어 있지 않을 시 초기값 설정
const setState = (nextState) => { // 2) 상태를 수정할수 있는 메서드 제공 (Hooks의 두 번째 인자)
hookState = nextState;
rendering(); // 상태 수정 후 렌더링
}
return [ hookState, setState ]; // 3) 상태와 상태를 변경할 핸들러를 배열로 반환, destructuring한 형태의 배열로 받아서 사용
}
hookState를 useState 함수 외부에 두어, 클로저로 데이터를 유지시켜 함수가 다시 호출되더라도 이전 상태를 기억할 수 있다.
하지만 위와 같은 형태는 1개 이상의 상태를 다룰 수 없다.
각각 다른 컴포넌트에서 setState로 hookState의 값을 수정하더라도 수정되는 값은 동일한 hookState 변수를 바라본다.
따라서, 1개 이상의 상태를 다루기 위해서는 hookState를 배열로 변경해야 한다.
→ Hook은 일종의 배열 데이터로, 클로저에 의해 저장된다.
hookState를 배열로 변경한 코드는 아래와 같다.
1) 각 함수 컴포넌트가 useState를 호출할 때마다 currentIndex로 해당 컴포넌트의 배열 위치 값을 관리한다. useState를 호출하는 각각의 컴포넌트에 대해 순서대로 currentIndex를 일종의 Key로 구분한다.
2) setState로 데이터를 수정 시, 해당 배열 내부의 값을 변경한다.
3) useState 함수가 종료되기 전 currentIndex 값을 증가시켜 다음 hookStates 배열의 Index 값을 업데이트한다.
let currentIndex = 0; // Hook을 사용하는 컴포넌트의 배열 위치 값
const hookStates = []; // Hook 데이터를 보관할 배열
function useState(initialState) {
const index = currentIndex;
if (hookStates.length === index) {
hookStates.push(initialState);
}
const setState = (newState) => {
hookStates[index] = newState;
rendering();
}
currentIndex++;
return [ hookStates[index], setState ];
}
→ useState를 사용하는 컴포넌트들의 상태는 hookState 배열에 순서대로 저장되므로 Hook은 사용 규칙에 따라 작성해야 한다.
사용 규칙 1) 최상위(at the Top Level)에서만 Hook을 호출해야 한다.
Hook은 순서대로 배열에 저장되므로 최상위 레벨이 아닌 조건문이나 반복문, 중첩 함수에서 Hook을 사용한다면 맨 처음 함수가 실행될 때 저장되었던 순서와 맞지 않게 된다. 따라서 최초 저장되었던 Hook의 상태 테이블에서 다른 상태 값을 참조하는 버그를 유발할 수 있다.
(Hook의 상태 테이블은 useState 내부가 아닌 외부 상태를 참조하고 있기 때문)
사용 규칙 2) 오직 React 함수 내에서 Hook을 호출해야 한다.
Hook은 React 함수 컴포넌트가 상태를 가질 수 있게 제공하는 기능이므로 React 함수가 아닌 일반 함수는 Hook을 저장할 수도, 위치 값을 알 수도 없다. 클래스 컴포넌트는 상태가 변경될 때 인스턴스를 새롭게 만들지 않고, render 메서드를 통해 상태가 업데이트되기 때문에 Hook의 호출 시점을 만들 수 없으므로 Hook을 사용할 수 없다.
자세한 Hook의 사용규칙은 공식문서를 참고하면 좋다.
💡 Hook을 간단히 구현한 코드 보기
let currentIndex = 0;
const hookStates = [];
function useState(initialState) {
const index = currentIndex;
if (hookStates.length === index) {
hookStates.push(initialState);
}
const setState = (newState) => {
hookStates[index] = newState;
rendering();
}
currentIndex++;
return [ hookStates[index], setState ];
}
function Espresso () {
const [espresso, setEspresso] = useState(2000);
window.addEspresso = () => setEspresso(espresso + 2000);
return `
<div>
<button onclick="addEspresso()">에스프레소 추가</button>
<strong>금액: ${espresso} </strong>
</div>
`;
}
function Americano () {
const [americano, setAmericano] = useState(3000);
window.addAmericano = () => setAmericano(americano + 3000);
return `
<div>
<button onclick="addAmericano()">아메리카노 추가</button>
<strong>금액: ${americano}</strong>
</div>
`;
}
const rendering = () => {
const $root = document.querySelector('#root');
$root.innerHTML = `
<div>
${Espresso()}
${Americano()}
</div>
`;
currentIndex = 0
}
rendering();
이 글은 우아한형제들의 기술블로그 중 '만들어 가며 알아보는 React: React는 왜 성공했나'를 읽으며 정리한 글입니다.
참고 자료
🔗 [우아한형제들 기술블로그] 만들어 가며 알아보는 React: React는 왜 성공했나
'개발공부 > 🟦 React.js' 카테고리의 다른 글
[React] 리액트 생명주기 (React Lifecycle) (0) | 2023.03.21 |
---|---|
[React] JSX 작동 원리 (0) | 2023.02.17 |
[React] React 앱 빌드와 배포 (0) | 2023.02.16 |
[React] Server Side Rendering (0) | 2023.02.16 |
[React] React 테스팅 (0) | 2023.02.15 |
프론트엔드 개발자 삐롱히의 개발 & 공부 기록 블로그