React의 Template(JSX)은 어떻게 동작할까?
인터넷에 글을 보다가 평소에 음.. 그렇게 동작하는 군.. 하지만 내부에서는 어떻게 동작을 하는걸까 하고 생각하던 내용을 마주하게 됐다. 나의 궁금증을 99% 해결해 주는 내용이라 이해한 내용을 잠시 옮겨볼까 한다.
웹 어플리케이션을 개발하는 개발자들 사이에 React, Vue 등을 언급하는 것을 많이 볼 수 있다. 개인적으로 이런 프레임워크를 그리 선호하지는 않는다. 왜냐하면 히트곡이나 패션 아이템처럼 잠시 왔다 스쳐 지나가는 유행처럼 보이기 때문이다. 하지만 이들 근간에는 어떤 기술이 활용되고 있는지 파악해 놓을 필요는 있다. 그래야 유사한 형태의 프레임워크가 나오더라도 쉽게 이해할 수 있고 트랜드를 빨리 따라갈 수 있기 때문이다.
React의 핵심으로 여러 기능이 있겠지만 그 중의 하나가 템플릿 엔진일 것이다. JavaScript를 좀 만져 본 사람이라도 배경지식 없이 템플릿인 JSX를 처음 만나면 고개를 갸우뚱 할 수 있다. 이 글을 통해 그 갸우뚱이 끄덕임으로 바뀌길 기대해 본다.
React의 근본을 잠깐 터치하자면 React는 Facebook에서 개발한 오픈소소의 JavaScript UI 프레임워크이다. 즉 웹 UI를 개발하기 위해 사용하며, 재사용 가능한 컴포넌트 개발이라는 개념적 바탕을 두고 있다.
JSX Scope 내에서 React 객체를 이용할 경우
JavaScript 코드를 작성해 본 사람들이라면 바로 알아차리겠지만 JSX는 JavaScript 문법과는 맞지 않다. 따라서 브라우저에서는 이를 바로 로딩해서 실행하지 못한다. 따라서 문법에 맞는 JavaScrpt로 변환하는 과정이 필요한데 이를 Transpile이라고 한다. 이 과정을 거치면 JSX에서 기술 된 HTML tag들은 브라우저가 이해할 수 있도록 React.createElement() 메소드 호출 행태로 변환된다. 당연히, Transpile을 하려면 하나의 구문을 다른 구문으로 변환시켜주는 Transpiler가 필요하다. Babel, TypeScript가 이런 역할을 한다.
이런 과정은 런타임이 아니라 빌드과정에서 수행되므로 브라우저는 이런 과정을 알 수 없다. 당연한 얘기겠지만 결과적으로 브라우저는 React API로 기술된 Object Tree를 전달받아 실행하게 된다.
이 과정이 JSX가 동작하는 기본 메커니즘이라 생각하면 된다. 그럼 한꺼풀 더 들어가서 간단한 예제를 통해 어떻게 동작하는지 살펴보자.
import React from 'react'
function Greet(){
return <h1>Hello World!</h1>
}
위의 간단한 React Component 코드를 실행하면 브라우저에 “Hello World!”가 출력될 것이다. 이 컴포넌트는 출력을 위한 HTML코드를 반환할 것 같지만 그렇지 않다. Greet 컴포넌트가 출력하는 h1 태그는 순수 JavaScript function인 React.createElement()을 호출한 결과로 생성된다. 위 예제는 Transpiler를 통해 아래의 코드로 Compile 된다.
import React from 'react'
function Greet() {
return React.createElement("h1", {}, "Hello, World!")
}
JSX 예제에서 React객체를 직접 참조(reference)하지 않았지만 Compile된 코드를 보면 React.createElement()를 호출한다는 것에 주목하자. 따라서 Compile된 JavaScript가코드를 실행하기 위해서는 현재 Scope에 React객체가 있어야 하므로 import를 통해 이 객체를 가져온다.
createElement() function은 아래의 Definition처럼 3개의 파라미터를 받고 React element를 반환한다.
React.createElement(
type,
[props],
[...children]
)
그럼 이제 JSX를 이용해 React Component를 작성해 보고 이것이 일반 JavaScript function call로 어떻게 변환되는지 알아보자.
import React from 'react'
function App() {
return (
<div>
<p>This is a list</p>
<ul>
<li>List item 1</li>
<li>List item 2</li>
</ul>
</div>
);
};
Compile하면 위의 코드는 아래의 코드로 변환된다.
import React from 'react'
function App() {
return React.createElement(
"div",
null,
React.createElement("p", null, "This is a list"),
React.createElement(
"ul",
null,
React.createElement("li", null, "List item 1"),
React.createElement("li", null, "List item 2"))
);
}
JSX Template을 사용하지 않고 위의 코드처럼 직접 JavaScrpt 코드로 작성해도 된다. 하지만 코드를 보면 알게 되지만 읽기가 힘들고 코드가 아름답지 못하다. 즉, 코드 작성이 어려울 뿐만 아니라 유지보수 하기에도 힘들어 보인다. 이것이 바로 JSX를 통해 깔끔한 HTML코드와 JavaScript의 강점을 합친 이유이기도 하다.
위 예제의 React.createElement() function이 반환하는 객체는 아래와 같다.
{
"type": "div",
"key": null,
"ref": null,
"props": {
"children": [{
"type": "p",
"key": null,
"ref": null,
"props": {
"children": "This is a list"
},
"_owner": null
}, {
"type": "ul",
"key": null,
"ref": null,
"props": {
"children": [{
"type": "li",
"props": {
"children": "List item 1"
},
// truncated for brevity
}, {
"type": "li",
"props": {
"children": "List item 2"
},
// truncated for brevity
}]
},
"_owner": null
}]
},
"_owner": null
}
이 반환 객체를 React Element라 하고, 보면 알겠지만 순수 JavaScript Object(JSON) 형태를 가진다. 이 객체는 화면에 무엇을 출력할지 설명한다. HTML element를 나타내지만 페이지(DOM)에 직접 그려지지는 않고 virtual DOM이라는 곳에 그려진다. React는 이들 객체를 읽은 후 virtual DOM상에 HTML element를 생성하기 위해 활용한다. 이렇게 한 다음 실제 DOM과 sync를 맞춘다.
이렇게 하면 virtual DOM과 실제 DOM에 object tree가 생성되며 React element에 변경이 생길 경우 React가 자동으로 관련된 실제 DOM element를 업데이트 시킨다.
아래는 종종 만나게 될 DOM element의 일부 속성을 설명한다.
- type: React element의 타입을 기술한다. “div”, “h1″과 같은 문자열을 기술하거나 React component(class or function) 또는 React fragment가 될 수 있다.
- props: null 또는 property를 표현하는 object로 기술할 수 있으며 component로 전달된다.
- children: 해당 element의 children element. 위에서 본 것처럼 인용(quoted) 문자열일 경우 content는 text로 간주된다. 다중 children을 추가할 경우 array를 사용해야 하며 원하는 만큼 중첩(nest)시킬 수 있다.
- key: 배열을 통한 매핑과정에서 sibling들을 유일하게 식별하기 위해 사용된다.
- ref: 실제 DOM node에 대한 reference이며 DOM element 또는 Component 인스턴스에 직접 접근해서 작업할 수 있도록 한다.
- $$typeof: 특정 object가 React element object인지 확인한다. XSS(Cross-site Scripting) 공격에 대응하기 위해 사용된다.
JSX Scope 내에서 React 객체를 이용하지 않을 경우 (React 17)
앞에서 살펴봤듯이 JSX를 사용하면 Compiler는 브라우저가 이해할 수 있도록 React function call로 변환한다. React 17 출시 이후 Facebook은 Babel과의 협업을 통해 기존의 설정에 영향을 주지 않으면서도 JSX transform을 개선하는 작업을 수행했다.
이 업데이트는 JSX 문법에는 전혀 영향을 주지 않으며 필수로 적용해야 하는 것도 아니다. 이전 버전의 JSX transform도 여전히 정상적으로 동작하며 향후 이 지원을 중단할 계획도 없다고 한다.
이전 버전에서 JSX를 사용했었다면 JSX가 React.createElement() call로 컴파일되기 때문에 JSX scope상에 React객체가 존재해야 했다. 새로운 transform에서는 필수로 작성했던 import React from ‘react’ 선언을 컴포넌트 file에서 생략할 수 있다. 최상위 레벨에서 React를 import하지 않거나 Scope내에 React객체가 없이도 JSX를 사용하는 것이 가능해 졌다.
React 17은 React 패키지에 2개의 새로운 entry point를 추가했다. 이 entry point는 Babel과 TypeScript와 같이 Compiler 내에서만 사용된다. 따라서 JSX가 React.createElement()로 변환하지 않고 새로운 JSX transform은 자동으로 React 패키지의 새로운 entry point로부터 special function을 import한 후 호출 한다.
따라서, 아래와 같이 코드를 작성할 수 있다.
function Greet(){
return <h1>Hello World!</h1>;
}
새로운 transform을 이용하면 직접 React를 import하지 않고도 JSX를 이용하여 컴포넌트를 작성할 수 있다.
// Inserted by a compiler (don't import it yourself!)
import {jsx as _jsx} from 'react/jsx-runtime';
function App() {
return _jsx('h1', { children: 'Hello world' });
}
위 코드를 보면 React를 import하지 않고 작성된 것을 알 수 있다. 직접 import를 작성하지 않아도 Compiler가 내부적으로 이를 처리해 준다.
이렇게 하면 코드 작성을 향상시키고 코드에서 보듯이 React.createElement()를 사용하지 않아 속도도 개선시켜 준다. 하지만 React객체는 여전히 compiler가 import시키고 있다. 이 변화는 기존 작성된 JSX 코드와 완벽하게 호환한다. 따라서, 기존에 작성된 컴포넌트를 다시 변경할 필요는 없다.
react/jsx-runtime과 react/jsx-dev-runtime에 포함된 function들은 Compiler가 transform하는 과정에서만 사용하므로 JavaScript코드 내에서 element를 직접 생성하려고 할 경우 기존에 했던 방법과 동일하게 React.createElement를 사용하면 된다.