React가 사랑 받는 이유 (feat. React 없이 Javascript로 React 핵심기능 구현해보기)

yoorabaek·2022년 9월 14일
0
post-thumbnail

(드래그한 영역도 민트색인 진정한 민초단(?) 우형 기술블로그..ㅎ)



어제 자바스크립트 공부 방향에 대해서 고민하다가 우연히 발견한 우아한형제들의 기술블로그 포스트 제목이 너무너무 나한테 필요한 주제라서 안읽고 베길 수 없었다. (이게 표현이 맞나)

아무튼 그래서 이번 포스트는 내가 너무 재밌게, 그리고 쉽게 읽은 해당 포스트에 대해 언제든 꺼내먹기 좋게 요약해서 정리해보려고 한다!

본문을 읽어보시는 걸 강추드린다 ㅎㅎ
출처 : 만들어 가며 알아보는 React: React는 왜 성공했나 (2022/07/06)



React 두두등장


웹은 DOM(Document Object Model)으로 구성되어 있다. 그리고 이 DOM의 동적 변경을 위해 웹은 DOM API를 제공한다. => 근데 이게 참 까다롭다 (자바스크립트에서 이벤트 처리하는 코드 하나만 짜보고 오면 알쥬^^)

프론트엔드 UI 개발 라이브러리는 다루기 까다로운 DOM API를 쉽게 다룰 수 있게 만들어졌고 DOM API를 어떻게 성공적으로 잘 다룰 수 있는지가 핵심이다.

이를 해결하고자 나온 것이 jQuery. 하지만 성능을 고려한 라이브러리가 아니었기 때문에 매우 느리다 ㅠ

jQuery는 Call tree가 굉장히 복잡하기 때문이다. 여러 코드로 wrapping 되어 있어, 실제 태그를 추가하기까지 상당히 많은 과정을 거치기 때문이다.
=> 과거에는 썩 괜찮은 라이브러리였으나 현재는 느리고 개발 생산성이 좋지 않다.


그럼 React는 어떻게 DOM API를 효과적으로 다루었나?

React는 개발자가 DOM API를 쓸 필요가 없게 만들었다. 개발자가 할 일은 React의 선언적인 문법에 맞춰 상태(State = 변하는 데이터) 관리만 하면 DOM API는 React가 처리해서 DOM을 렌더링한다.


React는 이런 기능을 어떻게 구현했을까?

React의 핵심 기능을 한 번 뜯어서 Javascript로 대신 구현해 보쟈.
(가상 돔, JSX, RealDOM 렌더링, Diffing Update, Hooks)


1) 가상돔 (VirtualDOM)

가상돔은 DOM의 형태를 본떠 만든 객체이다.

위와 같은 형태로 데이터를 계속 추가하기 위해 createElement 함수를 만들어 준다.

하 zl 만, 이렇게 매번 복잡한 UI를 웹 개발자가 VirtualDOM을 생성해 줄 수는 없단 말씀... 이를 해결해 주는 것이 바로 JSX이다.



2) JSX (Javascript And Xml)

JSX는개발자는 익숙한 마크업 문법으로 개발하고, Javascript 컴파일러인 Babel에서 createElement 함수로 변환(transpile) 해주는 역할을 한다.


React 없이 JSX 사용하기

JSX는 @babel/preset-react 플러그인에 의해 JSX를 위의 createElement 함수로 변환한다. React 없이 JSX를 Babel로 Transpile하려면 소스코드 주석에

@jsx '함수명'

을 기입해야 한다.


Q. React 17 버전 이전의 함수 컴포넌트에서 반드시 React를 import해야 하는 이유

React를 import하지 않으면 'React is not defined' 에러가 뜬다.

JSX를 transpile하려면 React.createElement 함수를 사용해야 하는데, React를 import하지 않으면 React 패키지의 createElement 함수를 사용할 수 없게 된다. 정확히 말해 런타임에 빌드된 코드 안에서 React.createElement 함수를 찾을 수 없게 된다.

하지만 React 17 버전 부터는 함수 컴포넌트에서 React를 import하지 않아도 에러를 반환하지 않는다. 왜냐면,

React 17 버전부터 새로운 JSX Transform 덕분에 React를 import 하지 않고도 React 패키지 자체의 _jsxRuntime 함수를 불러와 JSX 문법을 사용하게 해준다.

출처 : Babel 공식 사이트




3) 가상돔을 리얼돔으로 렌더링하기

JSX를 활용해 VirtualDOM까지 생성했다. 하지만 VirtualDOM은 언제까지나 객체일 뿐이다.
이 객체를 리얼돔으로 렌더링하려면 DOM API를 사용해야 한다. renderRealDOM 함수로 RealDOM에 반영해 줄 DOM을 생성해 보자.
이 renderRealDOM 함수가 하는 일은

  1. VirtualDOM의 tagName을 바탕으로 document.createElement API를 이용해 태그를 생성한다.
  2. VirtualDOM의 자식(children) 구조가 동일하므로 재귀 호출로 renderRealDOM을 호출한다.
  3. 각각의 Children Node 데이터를 appendChild API로 Element를 추가한다.
  4. 가장 끝 하위 요소 Children은 String이기 때문에 예외 처리를 해주고, createTextNode로 TextNode를 생성한다.

요약하면, 재귀호출로 DOM API를 이용해 태그를 생성해 준다.




4) Diffing Update 적용

VirtualDOM은 객체이기 때문에, 이전에 적용된 Old VirtualDOM과 New VirtualDOM을 변경된 부분만을 손쉽게 업데이트할 수 있다.
출처 : minemanemo님의 티스토리

Diffing Update 구현하기

쉬운 예시로 텍스트가 변경된 경우에 한해서 Diffing Update를 구현해 보자.


1. VirtualDOM 객체의 구조(tag, props, children)는 동일하므로 diffingUpdate 함수를 재귀 호출함으로써, 모든 자식 태그를 순회한다.
2. 함수의 인자로 부모 노드, 변경할 노드, 이전 노드, parentIndex를 받아서, replaceChildDOM API로 변경된 부분만을 업데이트한다.


여기서 핵심은 비교적 용량이 큰 RealDOM과 업데이트된 RealDOM을 비교해 변경 부분을 적용하는게 아닌, 비교적 작은 용량의 객체인 VirtualDOM과 업데이트된 VirtualDOM을 비교해 충분히 빠르게 업데이트 해주는 것이다.

React 없이 직접 만든 createElement, renderRealDOM, diffingUpdate 메서드를 활용해서 JSX로 VirtualDOM을 구현하고 renderRealDOM으로 렌더링했으며 diffingUpdate까지 적용한 코드다.


(벨로그는 왜 소스코드 형식은 그냥 흰글씨인거즹,, ㅠ 구분이 안되서 그냥 캡쳐했다!)


react.js

app.js




5) Hook 구현하기

클래스 컴포넌트는 최초로 생성되는 컴포넌트만 새로 인스턴스를 만들고 컴포넌트가 삭제되기 전까지 만들어진 인스턴스로 render 메서드를 이용해 상태 변경을 감지(setState)한다.

즉, 해당 인스턴스에서 필요한 부분만 업데이트해서 context 상태를 계속 유지할 수 있다. 하지만 함수 컴포넌트는 props를 인자로 받아서 JSX 문법에 맞는 React Component를 반환하기 때문에 함수 컴포넌트의 호출은 무조건 렌더링을 일으킨다.

이미 만들어진 인스턴스를 가지고 render만 호출하는 클래스 컴포넌트와 달리 함수 컴포넌트는 상태가 변경될 때마다 새로운 인스턴스를 생성하기 때문이다. 따라서 함수 컴포넌트는 호출될 때마다 늘 동일한 상태, 초기화된 상태만 가질 수 있었다.

하지만 React 16.8 버전부터 함수 컴포넌트에서도 상태(State)를 갖고 유지할 수 있는 Hook을 제공해 주었다. 즉, Hook은 함수 컴포넌트에서 상태를 정의하고 수정하는 기능이다.

좀 더 정확히는 함수 컴포넌트가 다시 실행되어도, 해당 함수의 상태(State) 값이 초기화되지 않고, React에 의해 사라지지 않는 것이다.

Hook을 사용할 때 쓰는 함수인 useState 함수를 간단히 구현해 보자.

  1. 초깃값이 설정되어 있지 않을 시 초깃값을 설정한다.
  2. setState로 상태를 수정하고 수정 후에는 렌더링한다.
  3. 상태와 상태를 변경할 핸들러의 배열을 반환해 destructuring한 형태의 배열로 받아서 사용한다.
  4. hookState는 useState 함수 외부에 두어, useState 호출과는 상관없이 데이터를 유지한다.

정리 : hookState를 useState 함수 외부에 두어, 클로저(MDN - Closure)로 데이터를 유지시켜, 함수가 다시 호출되더라도 이전 상태를 기억할 수 있다.



다중 상태 관리

하지만, 위와 같은 형태는 1개 이상의 상태는 다룰 수 없다. 각기 다른 컴포넌트에서 useState로 hookState의 값을 수정하더라도, 수정되는 값은 동일한 hookState 변수를 바라보기 때문이다.

따라서, 1개 이상의 상태를 다루기 위해 hookState를 배열로 변경해야 한다.덧붙이자면, Hook은 클로저로 구현된 배열일 뿐이다.

hookStates를 배열로 변경한 코드
1. 각각의 함수 컴포넌트들이 useState를 호출할 때마다 currentIndex로 해당 컴포넌트의 배열 위치 값을 관리한다. useState를 호출하는 각각의 컴포넌트를 순서대로 currentIndex 즉, 일종의 'Key'로 구분해 준다.
2. setState로 데이터를 수정 시, 해당 배열 내부의 값을 변경해 준다.
3. useState 함수가 종료되기 전 currentIndex 값을 증가시켜 다음 hooStates 배열의 Index 값을 업데이트 해준다.


useState를 사용하는 컴포넌트들의 상태는 hookState 배열에 '순서대로' 저장된다. 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();

(흠.. 벨로그 소스코드 테마 너무 밋밋행 ㅠ)


Hook은 2가지 사용 규칙이 있다.

  1. 최상위(at the Top Level)에서만 Hook을 호출해야 한다.

Hook은 순서대로 배열에 저장된다. 만약 최상위 레벨이 아닌 조건문이나, 반복문, 중첩 함수에서 Hook을 사용하면 맨 처음 함수가 실행될 때 저장됐던 순서와 맞지 않게 된다. 따라서 최초에 저장되었던 Hook의 상태 테이블에서 다른 상태 값을 참조하게 되는 버그를 유발할 수 있다. Hook의 상태 테이블은 useState 내부가 아닌 외부 상태를 참조하기 때문이다.

  1. 오직 React 함수 내에서 Hook을 호출해야 한다.

Hook은 React 함수 컴포넌트가 상태를 가질 수 있게 제공하는 기능이다. 따라서 React 함수가 아닌 일반 함수는 Hook을 저장할 수도, 위치 값을 알 수도 없다.

클래스 컴포넌트는 상태가 변경될 때 인스턴스를 새롭게 만들지 않고, render 메서드를 통해 상태가 업데이트된다. 따라서 Hook의 호출 시점을 만들 수 없으므로 Hook을 사용할 수 없다. (애초에 클래스 컴포넌트가 사용할 이유도 없지만^^)




간단히 위의 내용들을 정리해보면,

  1. jQuery는 다루기 까다로운 DOM API를 직관적으로 손쉽게 다룰 수 있는 모델을 제시했다. 하지만, 성능 최적화의 아쉬움과 개발 생산성의 문제가 있었다.
  2. React는 개발자가 DOM API를 다룰 필요가 없게 만들고, 상태(State)를 기반으로 DOM을 업데이트시켜 준다. 따라서, 충분히 빠른 성능과 개발 생산성에도 효과적인 모델을 제시했다.
  3. 가상돔은 DOM의 형태를 본떠 만든 객체이다. (용량 small)
  4. JSX는 개발자는 익숙한 마크업 문법으로 개발하고, 바벨에서 createElement 함수로 변환해 VirtualDOM을 손쉽게 만들어 준다.
  5. Diffing Update는 비교적 무거운 RealDOM의 비교가 아닌, 가벼운 VirtualDOM의 비교를 통해 적은 비용으로 충분히 빠르게 UI를 업데이트해준다.
  6. Hook은 일종의 배열 데이터로, 클로저에 의해 저장된다. React 함수 컴포넌트에서도 상태를 가질 수 있고, 렌더링 순서가 일정한 특성을 이용해 구현된 기능이므로, 2가지 규약이 있다.

웹 프론트엔드 UI 개발 라이브러리 흐름이 jQuery에서 React로 왜 전환되었고 기능들이 어떻게 Javascript 레벨에서 구현되는지 간단히 알아봤다. React를 더 잘 이해하고 사용할 수 있는데 도움이되었길..바란다는 저자분의 말씀답게 정말 도움이 많이 되었습니다!!!

감사합니다~ !!



P.S. 지금 미리 포스트 내용을 노션에 정리중인데 인터넷에 연결된 상태이면 내가 작성한 내용이 실시간으로 저장이되는 게 너무 좋다..

글 작성이나 작업을 하는중에 항상 문득 드는 걱정이 이러다 컴퓨터 꺼지면 날라가니까 저장해야지 하는, 진짜 그런 좌절을 맛본적 있었던 경험에서 나온 '미리 걱정'인데 노션에 작업할 때는 그러다가도 아 맞다 노션이지? 하면서 걱정 없이 작업에만 집중할 수 있게 해준다. 정말 획기적인 Tool , 페인포인트를 없애준 좋은 서비스가 아닌가 새삼 느끼면서 .. React 같은 라이브러리도 결국 자바스크립트의 큰 페인포인트를 없애기 위해 나온 것처럼 편의성을 높여준다는 것은 많은 사람들의 작업을 방해하던 것을 뛰어넘어 좀 더 핵심에만 집중할 수 있게 해주는, 그 다음 단계로의 발전이 아닌가 라는 생각이 들었다.

0개의 댓글