React – 함수 조합으로 만드는 DOM (세미나 자료 #1)

필자가 다녔던 학교에서 최근 연락이 왔다. 이제 HTML·JS·CSS를 갓 배운 학우들을 대상으로 React 기초를 알려주면 어떻겠냐는 연락이었다. 다행히 시간도 맞고 해서 1.5주 짜리 세미나를 준비하게 됐다. 세미나에 쓸 내용을 그저 묵히기가 아쉬워서 여기에 같이 기록해 둔다.

내용 인용시 반드시 출처 표기 부탁 드립니다.

우선 JSX를 써보자

백문이 불여일견이다. 우선 React를 이용하여 간단한 글자와 사진을 표시해 보자.

React는 기본적으로 JS를 사용한다. 브라우저는 HTML(DOM)으로 변환된 내용만 표시할 수 있다. 그러므로, React(JS)에서 HTML 내용을 만들어 낼 수 있어야 한다.

JQuery나 document.append를 사용해 본 사람들이면 JS에서 DOM Tree를 조작하는게 상당히 번거로운 과정임을 알 것이다. 예를 들어, body > div#test<img src="변수A" /> 라는 내용을 추가한다고 하자. 그러면 아래와 같이 스크립트를 작성해야 한다:

<body>
	<div id="test"></div>
	<script>
		let variable = "img_src";
		let img_tag = document.createElement("img");
		img_tag.src = variable;

		document.getElementById("test").appendChild(img_tag);
	</script>
</body>

아니면 innerHTML을 조작할 수도 있긴 하다. 어째됐던, 직접 HTML을 조작해야 한다. 지금이야 하나의 테그로 끝나서 간단해 보일수 있다. 그러나 페이지가 복잡해질수록 이러한 JS로 된 코드 파편들이 늘어난다.

<body>
	<div id="test"></div>
	<script>
		let variable = 'img_src';
		let el = document.getElementById("test");
		el.innerHTML += `<img src="${variable}"/>`
	</script>
</body>

React는 JS에서 HTML을 보다 편하게 작성할 수 있도록 JSX라는 물건을 들고왔다. 아래의 코드를 보면 JS 영역에서 바로 (quote 없이) HTML을 조작하는것을 볼 수 있다.

function App() {
  let variable = "img_src";

  return (
    <div id="test">
      <img src={variable} />
    </div>
  );
}

JSX는 JS에서 HTML을 편리하게 작성할 수 있도록 확장한 언어이다. 이렇게 작성한 HTML 코드는 컴파일러 단에서 적절하게 변환된다. (JSX Transform) 개발자는 그저 평범하게 HTML을 사용하는것 마냥 코드를 작성하면 된다.

위의 코드에서 볼 수 있듯이 변수 부분은 {bracket} 안에 집어 넣으면 된다. 그러면 컴파일러가 JSX를 변환할 때 적절하게 변수를 연결한다. 변수에 < 와 같은 특수문자가 있어도 된다. 컴파일러가 알아서 잘 처리해 주기 때문이다.

함수 기반

위의 코드를 살펴보면 HTML이 App() 이라는 함수에 둘러쌓여 있다. React에서는 함수 개념이 중요하다. React에서는 HTML을 함수의 조합으로 표현한다. 예를 들어 헤더와 글 내용이 있는 페이지를 생각해 보자 (body > (header + main)) 이것을 풀어서 표현한다면 다음과 같을 것이다:

각 단위 HTML 파편마다 함수로 표현한다. 이것을 조합하여 페이지를 만드는것이 React의 기조이다. 여기서 나온 “단위 HTML 파편”이 컴포넌트이다. React에서는 함수를 통해 컴포넌트를 만들고 사용할 수 있다.

컴포넌트 함수를 만들때 주의점이 있다. 컴포넌트 함수 이름의 첫글자는 대문자이어야 한다는 것이다. 위에서는 Header, Main을 사용했다. 일반 HTML 테그와 컴포넌트를 구분하기 위해서 필요하다. 코드에서 <Header><header>가 같이 있음을 볼 수 있다. 이때 대문자 Header는 컴포넌트 함수를, 소문자 header는 HTML tag를 의미한다.

또한, JSX의 최상단 항목(element)은 단일 항목이어야 한다. Body의 JSX는 <div>에 모두 감싸져있다. Header는 <header>에 감싸져있고, Main에서는 <main>에 감싸져있다. 이것은 추후 설명하겠지만, Tree의 Root는 딱 하나만 될 수 있다는 제약 때문에 발생한다.

Div hell (Div가 무한히 중첩되어 있다)

JSX는 Tree 구조로 구현되기 때문에 반드시 Root가 필요하다. 그러나 현실적으로 모든 Component가 Div를 가지면 Div Hell이 될 것이다. HTML 테그 대신 의미상 Root를 만들 수도 있다.<><p>test</p><p>1234</p></> 와 같이 안에 아무것도 쓰여있지 않은 tag를 만들면 된다. 그러면 Root가 생기지만 DOM에는 반영되지 않는다.

<>를 이용하면 의미상의 root를 만들어서 실제 DOM에서는 표현되지 않도록 할 수 있다.

위에서는 function을 이용했다. 하지만 람다 함수를 사용하여 더 간단하게 표현할 수도 있다:

이렇게 하나의 목적·의미를 가진 단위 HTML을 컴포넌트라고 한다. 여기서는 HeaderMain가 함수이면서 동시에 컴포넌트이다. React는 컴포넌트를 조합하여 하나의 큰 HTML을 만들어 낸다.

실제로 페이지를 만들땐 할 땐 Header와 Main의 범위가 너무 크기 때문에 컴포넌트로 보기 어려울 것이다. (하나의 단위 코드로 보기 어렵다)
그러나 여기서는 코드가 매우 짧아 하나의 “컴포넌트”라고 표현해도 적절할 것으로 보았다.

아래는 Bootstrap의 기본 Modal의 모습이다. 이것은 Modal이라는 하나의 의미를 가진 Component로 볼 수 있다. 다시 이 Modal 컴포넌트 아래에 단일 의미를 가진 Header 컴포넌트, Footer 컴포넌트로 나눌 수 있을 것이다. 버튼 또한 의미를 가진 단일 항목으로 볼 수 있기에 컴포넌트라고 부를 수 있다.

어디까지를 컴포넌트로 봐야 하냐는 또 다른 이야기이다. “독립적으로 의미를 가진 HTML 조각”이라는게 상당히 추상적이다. 혹자는 위의 Modal을 컴포넌트 통째로 묶고 하나의 컴포넌트라고 부를수도 있다. 이것 또한 아주 틀린것은 아니다. 필자는 이 코드가 두번 이상 쓰이는가? 와 코드 이해를 방해할 만큼 복잡한가? 두가지를 가지고 판단한다. 이 부분은 경험이 해결해 줄 부분이다.

더 큰 페이지인 네이버 뉴스 페이지를 살펴보자. 이 페이지를 보면 많은 항목들이(구조들이) 중복되어 있음을 볼 수 있다. 여기서 자주 쓰이는 하나하나의 단위 HTML을 컴포넌트라고 부를 수 있다. 만약 이 사이트를 React로 만들었다면 대부분의 것을 컴포넌트 함수로 분리한 후, 함수를 조합하여 하나의 페이지로 만들었을 것이다. 예를들어, Nav, SideNav, (Header + hr + Article > (img + h1 + p + span ~~ )) 와 같은식으로 쪼갰을 것이다.

DOM Tree와 함수 결합

사용자는 브라우저에 표시된 내용에서 계층 구조를 느끼기 어렵다. 하지만 우리는 계층을 이해해야 한다. 브라우저는 HTML을 파싱해서 DOM 트리를 만든다. 그리고 브라우저는 DOM Tree를 기준으로 모든것을 처리한다. 페이지를 여는 과정도 사실은 HTML을 다운로드 한 후 DOM Tree를 만드는 과정이다. 그리고 이 DOM Tree를 표시하는게 브라우저의 임무이다.

Javascript가 보여지는 부분을 조작하는것도 실제로는 DOM을 조작하는 것이다. DOM Tree에 항목을 추가하면 새로운 내용이 보이는것이며, DOM Tree에서 항목을 제거하면 보이지 않는다. style, class, id 속성들도 모두 DOM에 포함되어 있다.

Tree라는 이름에서 알 수 있듯, Root / Leaf / Edge 등의 개념이 존재한다. 이를 통해서 계층을 이해할 수 있다. 위에서 아~~까 작성했던 간단한 HTML은 다음과 같이 변환될 것이다.

브라우저에서 표시될 때도 계층에 따라 처리 된다. 일반 사용자가 보는 화면은 실제로는 Flatten되어 있기 때문에 이해하기 어려울 수 있다. 그러나 아래 사진과 같이 계층이 존재하며, 이 계층에 따라 div의 크기나 위치가 결정된다.

DOM Tree가 Flatten 되어 2d로 보인다.
만약 DOM Tree를 3D로 보게 된다면 Depth에 따라 랜더링 됨을 알 수 있을것이다.

어쨋든 우리는 “결국 본질은 DOM Tree이다” 라는것을 알게 됐다. 평면상에 보일 뿐이지, 브라우저는 모두 DOM Tree로 생각한다.

React는 이 DOM Tree를 편리하게 조작하게 해 주는 도구이다. 이때, 하나의 Sub-Tree를 함수로 분리한다. 이렇게 분리된 Sub-Tree 하나가 컴포넌트가 된다. 즉, 함수를 조합하여 전체적인 Tree를 만드는 것이다.

MDN의 Shadow DOM 항목에서 들고온 사진이다.
하나의 컴포넌트(함수)가 Shadow Tree에 해당된다고 생각할 수 있다.

비로소 React의 JSX에서 무조건 Root가 있어야 하는 이유를 설명할 수 있다. React는 함수에 JSX를 이용하여 HTML을 표현한다. JSX가 리턴되는 컴포넌트 함수는 실제로는 가상 DOM Tree를 리턴하는것과 동일하다. (fn Component() -> JSX == fn Component() -> VirutalDomTree) 이때, Tree는 무조건 Root가 하나 있어야 한다. 그러므로 JSX에서도 이에 해당되는 Root가 있어야 한다.

맨 위에서 언급한 것 처럼, 이런 함수들을 조합하여 전체적인 페이지를 만드는게 React의 기조이다. JSX를 리턴하는 함수를 조합하여 Virutal DOM Tree를 만든다. 그리고 이것을 실제 브라우저의 DOM Tree에 적용한다. 이것을 통하여 정교하게 딱 떨어지는 HTML을 만들 수 있다.

함수여서 생기는 장점

수학의 함수를 생각해 보자. f(x) = x + 5y(x) = x + 9 를 보자. 여기 각 함수는 x라는 변수를 받아서 계산한다. x가 이름은 같을지언정, 실제로는 다른 항이다. f라는 함수는 y라는 함수에 영향을 줄 수 없다. f의 계산 과정 또는 결과가 y에 영향을 줄 수 없다.

React는 HTML (DOM Tree) 구현을 함수로 모두 분리하였다. 함수로 분리하면서 Pure Function 기조 또한 챙겨왔다. A라는 함수는 B라는 함수에 영향을 주지 않는다. (물론 global state라는게 있긴 하지만, 이것은 변칙으로 생각하는것이 옳다)

A라는 컴포넌트 안에서 작동된 JS 코드는 A 안에서만 작동된다. 별도의 파훼법을 사용하지 않는다면 A 밖으로 나갈 수 없다. 기존의 DOM은 전역 상태였다. JS를 작성할 때 “내가 수정하고자 하는 Element” 뿐 아니라 “브라우저 DOM에 있는 모든 항목”을 고려해야 했다.

예를 들어, 계산기를 만들었다고 치자. 계산기에 “초기화” 버튼을 눌린다면 input이 초기화 되도록 하고 싶다. React 스타일에서는 계산기 컴포넌트의 스크립트는 계산기 안에서만 작동할 수 있다. 그러나 쌩 HTML에서는 전역 범위에서 실행된다. input을 초기화 하는 코드를 작성할 때, React는 해당 컴포넌트 안에서만 영향을 미칠수 있다. 하지만 쌩 HTML에서는 브라우저에 표시된 모든 input의 내용이 지워질 것이다.

또한, 위에서 본 것 처럼 정밀하게 계층이 뽑혀저 나온다. 만약 어떤 데이터가 수정된다면 그에 관련된 node만 다시 계산하면 된다. 즉, parent node에 해당되는 함수만 다시 실행하면 된다. 이를 통해서 수많은 DOM 항목이 있더라도 원할하게 처리할 수 있다.

React의 Virutal DOM이 꼭 빠르지는 않다. 속도로 따지면 오히려 Svelte를 비롯한 다른 도구들이 더 빠른 처리가 가능하다.
사실 필자는 “결국 최종적으로는 Virutal DOM도 DOM으로 변환 되어야 한다”는 부분에서 하나의 계산 계층이 낀 것이라 생각한다. 그러므로, Virtual DOM은 DOM을 바로 찍어내는 것에 비해 느릴 수 밖에 없다고 판단한다.

결론

React는 JSX를 통해서 Virtual DOM을 만드는 도구이다. 단위 HTML을 “컴포넌트”라는 단위의 함수로 분리했다. 이 함수를 실행하면 각각의 Virutal DOM이 나온다. 이 Virutal DOM을 조합하면 전체 DOM이 나온다. 이를 브라우저에 적용하여 하나의 큰 페이지를 만든다.

“컴포넌트” 형태로 함수를 전부 분리한 덕분에 컴포넌트 끼리의 독립성을 보장 받는다. 그 덕분에 개발자가 좀 더 작은 부분만을 신경쓸 수 있게 됐고, 꼭 필요한 부분만 다시 연산하면 되므로 연산 범위가 줄어들었다. 그렇지만, 어쨋든 연산 계층이 하나가 더 늘었기 때문에 “pure overhead”라는 질타는 피해갈 수 없을것이다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

목차