** 리액트 공식문서와 함께 읽으시면 더욱 좋습니다.
** 거침없는 피드백은 더 더욱 좋습니다
엘리먼트는 React 앱의 가장 작은 단위입니다.
브라우저에 돔 엘리먼트가 있듯 리액트에도 엘리먼트라는 개념이 존재합니다. 리액트 엘리먼트는 일반적인 자바스크립트 객체입니다. 뒤에서 다룰 리액트 돔을 통해 리액트 엘리먼트를 렌더링하면 이는 실제 돔에 그려지게 됩니다. 리액트 엘리먼트를 생성하는 방법은 돔과 유사합니다. 바닐라 자바스크립트에서 돔 엘리먼트는 document.createElement 메서드로 만들 수 있습니다. 리액트에서는 React.createElement를 통해 엘리먼트를 만듭니다.
React.createElement는 type, props, children을 인자로 받고 리액트 엘리먼트를 반환합니다. 리액트 엘리먼트는 렌더링에 필요한 정보를 가지고 있는 객체입니다. 반환되는 리액트 엘리먼트의 형태는 대략 다음과 같습니다.
function createElement(type, props, ...children) {
...
return {
type,
props: {
...props,
children,
},
...
}
}
리액트 엘리먼트는 type과 props을 가지고 있습니다. 물론 이 두 가지 외 다른 프로퍼티도 가지고 있지만 지금은 이 두가지만 알고 있어도 충분합니다. type과 props 그리고 props에 포함되는 children을 중심으로 더 살펴보도록 하겠습니다.
type은 말그대로 리액트 엘리먼트의 타입을 의미합니다. type으로 div, input, ... 과 같은 태그 이름 문자열을 지정할 수도 있고 리액트 컴포넌트를 지정할 수도 있습니다. 리액트 컴포넌트에 대해서는 아직 다루지 않았지만 우선 리액트 엘리먼트를 반환하는 함수 정도로만 이해하도록 합시다. type이 태그 이름 문자열이면 이 엘리먼트는 나중에 type에 해당하는 태그의 돔 엘리먼트로 렌더링됩니다. type이 컴포넌트라면 그 컴포넌트가 반환하는 엘리먼트가 렌더링 됩니다.
// type이 태그 이름 문자열일 경우
const element = React.createElement("div")
// element = { type: "div", ... }
// element는 나중에 <div></div>로 렌더링됩니다.
// type이 리액트 컴포넌트일 경우
const Div = () => React.createElement("div");
const element = React.createElement(Div);
// element = { type: () => React.createElement("div"), ... }
// element는 나중에 <div></div>로 렌더링됩니다.
// 이는 컴포넌트 Div가 type이 "div"인 엘리먼트를 반환하기 때문입니다.
props는 리액트 엘리먼트의 속성을 의미하는 객체입니다. 만약 type이 태그 이름 문자열 이라면 props는 HTML태그의 어트리뷰트(Attribute)와 비슷한 역할을 합니다. 각 어트리뷰트는 props 객체의 키와 값으로 전달됩니다. 이때, 주의할 점이 한가지 있습니다. props가 어트리뷰트와 비슷한 역할을 한다고 했지만 어디까지나 props는 자바스크립트 객체이기 때문에 HTML 어트리뷰트와 분명한 차이점을 가지고 있습니다. 다음은 몇가지 예시입니다.
이 외에도 리액트의 props는 HTML 어트리뷰트와 많은 차이점을 갖습니다. 추가적인 내용은 문서를 통해 확인하시기바랍니다.
const element = React.createElement(
"div",
{
className: "div",
"aria-label": "aria-label은 기존대로!"
}
);
// element: {
// type: div,
// props: { className: "div", "aria-label": "aria-label은 기존대로!" },
// }
// element는 <div class="div" aria-label="aria-label은 기존대로!"></div>로 렌더링됩니다.
const element = React.createElement(
"div",
{
style: { backgroundColor: "red" }
}
);
// element는 <div style="background-color: red;"></div>로 렌더링됩니다.
type이 컴포넌트일 경우 props는 조금 다른 의미를 갖습니다. 조금 전 리액트 컴포넌트에 대해 리액트 엘리먼트를 반환하는 함수로 이해하자고 했습니다. 컴포넌트에게 있어 props는 함수의 인자와 같다고 생각하면 좋겠습니다. 컴포넌트는 props에 따라 동적으로 리액트 엘리먼트를 반환할 수 있습니다.실제로 엘리먼트를 렌더링할 때 props는 컴포넌트인 type의 인자가 됩니다.
const Div = (props) => React.createElement("div", { id: props.id });
const element = React.createElement(Div, { id: "tanney" });
// <div id="tanney"></div>
children은 말 그대로 엘리먼트의 자식 엘리먼트들의 배열를 의미합니다. children은 리액트 엘리먼트뿐 아니라 string, number타입의 값도 가질 수 있습니다. React.createElement의 세번째 이후 인자들은 모두 children의 요소가 되며 children은 반환된 리액트 엘리먼트의 props 안에 있는 값으로 취급됩니다. 해당 엘리먼트가 렌더링되면 이 children은 모두 자식 요소로 추가됩니다. ReactFragment와 ReactPortal 또한 children이 될 수 있지만 아직 다루지 않았으므로 자세히 이야기하지는 않겠습니다.
const element = React.createElement(
"div",
null,
"tanney" // children 1
102, // children 2
React.createElement("span") // children 3
);
// element = {
// type: "div",
// props: {
// children: ["tanney", 102, { type: "span", }]
// },
// }
// <div>
// "tanney"
// "102"
// <span></span>
// </div>
const Div = (props) => React.createElement("div", null, props.children);
const element = React.createElement(Div, null, "tanney");
// element = {
// type: (props) => React.createElement("div", null, props.children);
// props: {
// children: ["tanney"],
// }
// }
// 추후 렌더시 element.type(element.props)의 반환값이 돔에 전달됩니다.
// <div>tanney</div>
리액트를 더욱 효율적으로 사용하기 위해 권장되는 문법이 있습니다. 이는 바로 JSX라고하는 JavaScript의 확장 문법입니다. 먼저 간단한 예시를 살펴봅시다.
const element = <h1 id="hello">Hello, World!</h1>
언뜻보면 자바스크립트의 단순한 할당문 같기습니다. 그러나 할당되는 값을 자세히 살펴보면 뭔가 조금 이상한 점이 느껴집니다. 문자열도 아닌 것이 HTML 태그가 그 자체로 할당되고 있습니다. 바로 저 태그가 JSX입니다. React에서는 컴포넌트라는 요소를 통해 이벤트 처리, 상태 변화 등을 관리하는 UI로직과 이를 렌더링하는 로직을 함께 취급합니다. 이때, 엘리먼트를 만들고 그 안에 어떤 상태를 주입하는 등 렌더링과 관련된 로직을 다루는데 있어 JSX가 시각적으로 큰 도움을 줄 수 있습니다. React를 사용하는데 있어 JSX가 필수는 아닙니다. 그러나 특별한 이유가 없다면 굳이 사용하지 않을 이유도 없습니다. 만약 JSX를 사용한다면 babel을 이용해 반드시 트랜스파일 해주어야 합니다.
그럼 이제 JSX에 대해 본격적으로 알아봅시다. 먼저 위 예시에 있는 태그(JSX)가 무엇을 의미할까요? HTML 문서에서 그러하듯 태그는 엘리먼트를 의미합니다. 특히 여기에서는 리액트 엘리먼트를 의미하겠죠. 즉, 위 예시를 React.createElement를 사용해 표현하면 다음과 같습니다.
// const element = <h1 id="hello">Hello, World!</h1>;
const element = React.createElement("h1", { id: "hello" }, "Hello, World!");
위 예시를 통해 JSX의 각 요소가 무엇을 의미하는지 유추해 봅시다. 먼저 '<' 표현 바로 뒤에 오는 단어가 React.createElement의 type인자로 들어가는 것 같습니다. 위 예시에서는 "h1"이 type인자로 주입되고 있습니다. 그리고 HTML 속성처럼 넣어준 JSX의 속성이 props로 취급되는 것도 쉽게 알 수 있습니다. 또한 태그 문법상 하위에 속한 요소 "Hello, World"가 자연스럽게 children이 됩니다. 이처럼 JSX는 UI로 표현되는 각 요소를 시각적으로 쉽게 파악할 수 있도록 합니다. 특히 HTML과 같이 태그 구조를 통해 부모-자식 계층을 표현하기 때문에 보다 직관적으로 children을 정의할 수 있습니다.
const element = (
<div id="hello">
<h1>Hello, World</h1>
<h2>It's JSX World!</h2>
</div>
);
// 가독성을 위해 여러 줄에 걸쳐 표현했습니다.
// 여러 줄로 JSX를 표현하는 경우 세미콜론(;)의 자동 삽입을 방지하기 위해 괄호를 넣어주는 것이 좋습니다.
위 예시의 엘리먼트를 React.createElement로 표현하면 다음과 같습니다.
const element = React.createElement(
"div",
{ id: "hello" },
React.createElement("h1", null, "Hello, World"),
React.createElement("h2", null, "It's JSX World!")
);
HTML 태그처럼 />
를 이용해 JSX를 표현할 수도 있습니다. 자식이 없다면 아래와 같이 바로 닫아주는 것이 좋겠습니다.
const element = <img src="..." alt="..." />
예시에서처럼 JSX는 React.createElement로 표현될 수 있습니다. 그리고 실제로 JSX는 Babel을 통해 React.createElement를 호출하도록 트랜스파일 됩니다. 따라서 JSX 코드의 스코프 내에 React 라이브러리가 존재해야합니다. 즉, es module 시스템을 기준으로 반드시 상단에 React 라이브러리가 import 되어야합니다.
앞서 React.createElement는 type인자에 무엇을 전달하느냐에 따라 다르게 동작한다고 했습니다. 그리고 JSX에서는 '<'문자 바로 뒤에 오는 단어가 type인자로 주입된다고 했습니다. 그렇다면 JSX는 type이 태그 이름을 뜻하는 문자열인지, 혹은 컴포넌트인지 어떻게 구별할까요? 이 질문을 조금 더 구체적으로 발전시켜보겠습니다. 다음 표현을 보고 우리는 두가지 생각을 할 수 있습니다.
<someWord />
이 경우 type인자가 컴포넌트일 때는 JSX를 사용할 수 없게 됩니다. 따라서 someWord는 항상 문자열일 수 없습니다.
이 경우에는 마치 type이 문자열일 때와 컴포넌트 일때 모두 대응할 수 있는 것처럼 보입니다.
// 주의! 이해를 돕기 위한 표현입니다. 실제 리액트에서는 다르게 동작합니다.
const tagName = "div"
const element1 = <tagName /> // React.createElement("div", null, null);
const component = () => <tagName />;
const element2 = <component /> // React.createElement("div", null, null);
그러나 이 또한 문제점이 있습니다. 단순히 태그이름만을 사용해 엘리먼트를 만들 때에도 그 이름에 해당하는 문자열을 특정 변수에 담거나 표현식으로 전달해야한다면 이는 불필요한 코드의 작성으로 이어질 수 밖에 없습니다. 그리고 실제로도 <div>...</div>
로 표현되는 JSX는 React.createElemnt("div"...)와 같이 type인자에 식별자가 아닌 문자열 "div"를 전달합니다.
someWord는 항상 문자열로 취급되거나 항상 식별자로 취급될 수 없습니다. 그럼에도 JSX는 이를 문자열 또는 식별자로 구분해야합니다. JSX는 이를 단순한 방법으로 해결합니다. 태그의 첫 부분이 소문자로 시작한다면 JSX는 이를 문자열로 취급합니다. 만약 첫 부분이 대문자로 시작한다면 식별자로 취급됩니다.
const SomeWord => () => ....
const element1 = <someWord /> // React.createElement("someWord", null, null);
const element2 = <SomeWord /> // React.createElement(SomeWord, null, null);
// <SomeWord /> 와 같은 표현을 사용하려면 SomeWord식별자가 반드시 스코프 체인상에 있어야합니다.
이러한 특징 덕분에 생기는 자연스러운 컨벤션이 있습니다. 함수나 클래스로 컴포넌트를 정의할 때에는 반드시 대문자로 시작하는 식별자를 사용해야합니다. 특히 함수의 경우 여느 함수처럼 소문자로 시작하는 식별자를 사용한다면 JSX가 이를 컴포넌트로 인식하지 못합니다.
대문자로 시작하는 태그가 해당하는 식별자로 취급된다는 점을 활용하면 객체를 통해 컴포넌트를 참조할 수도 있습니다. 이처럼 JSX특성을 잘 이해한다면 상황에 맞게 여러방면으로 활용할 수 있습니다.
const Components = {
SomeComponent: () => <div>...
};
const element = <Components.SomeComponent />
JSX 안에서 중괄호를 사용하면 자바스크립트 표현식을 넣을 수 있습니다. 예컨대 다음과 같은 일이 가능합니다.
const name = "Tanney";
const element = <h1>Hello, {name}</h1> // <h1>Hello, Tanney</h1>
실제 엘리먼트에는 표현식을 평가한 값이 들어갑니다. 자바스크립트의 표현식으로써 유효하다면 무엇이든 중괄호 안에 넣을 수 있습니다.
const getName = () => "Tanney";
const element = <h1>Hello, {getName()}</h1> // <h1>Hello, Tanney</h1>
JSX 속성에도 역시 표현식을 넣을 수 있습니다.
const id = "hello"
const element = <h1 id={id} style={{ backgroundColor: "red" }}>Hello</h1>
// <h1 id="hello" style="background-color=red;">Hello></h1>
HTML태그 처럼 속성에 아무 값도 전달하지 않을 수 있습니다. 이때 기본값은 true입니다. 아래 두 표현은 동일합니다.
<TextBox autocomplete /> <TextBox autocomplete={true} />
JSX안에 표현식을 넣을 수도 있지만 JSX 자체도 표현식입니다. 어쩌면 당연한 말입니다. 앞서 살펴보았듯 JSX가 의미하는 값은 React.createElement의 반환값과 같습니다. JSX는 표현식이기에 if문이나 for문 안에서 사용하거나 변수에 할당할 수도 있습니다.
const getGreeting = (username) => {
if (username) {
return <h1>Hello, {username}<h1>;
}
return <h1>Hello!!</h1>;
}
const greeting = getGreeting("Tanney");
실제로 JSX를 이용해 개발을 하다보면 JSX 내부에 조건문이나 반복문이 필요한 경우가 자주 발생합니다. if문이나 for문 같은 경우 표현식이 아닌 문이기 때문에 JSX 내부에서는 사용할 수 없습니다. 이때, 위 예시와 같이 따로 함수를 만들어 해결을 할 수도 있지만 표현식을 이용해 해결할 수 있는 방법도 존재합니다. 자세한 내용은 추후 포스팅을 통해 다뤄보도록 하겠습니다.
React.createElement를 통해 만들어진 엘리먼트는 ReactDOM.render 메서드를 통해 실제 돔에 렌더링 됩니다. ReactDOM은 React와 별개의 패키지입니다. ReactDOM안에 여러 메서드들이 있지만 오늘은 render 메서드만 살펴보도록 하겠습니다.
React.render 메서드는 다음과 같은 인자를 받습니다.
function render(element, container [, callback])
먼저 element는 리액트 엘리먼트 입니다. 돔에 렌더링 하고 싶은 엘리먼트를 만들어 첫번째 인자로 넘깁니다. container는 element의 부모가 될 돔 엘리먼트입니다. 리액트가 필요한 만큼 점진적으로 적용될 수 있도록 설계되었다는 말의 근거가 바로 이 부분에 해당합니다. 사용자가 원하는 곳에 div와 같은 빈 컨테이너를 만들어 리액트 엘리먼트를 렌더링 할 수 있습니다. 추가적으로 콜백을 전달할 수 있는데 이는 엘리먼트가 렌더링 되거나 업데이트 된 이후에 호출됩니다.
// index.html
<div id="root"></div>
// index.js
import React from "react"
import ReactDOM from "react-dom"
const element = React.createElement("h1", null, "Hello, World");
const $container = document.getElementById("root");
ReactDOM.render(element, $container);
ReactDOM.render 메서드가 처음 호출되면 컨테이너에 엘리먼트를 렌더링합니다. 이후 호출에서는 이전에 렌더링 되어있던 엘리먼트를 업데이트합니다. 이때, React의 DOM diffing 알고리즘이 사용됩니다. ReactDOM은 diffing 알고리즘을 통해 기존 리액트 엘리먼트 트리와 render 메서드를 통해 새롭게 만들어진 트리를 루트 엘리먼트부터 비교합니다. 이때, 엘리먼트의 타입(type인자)과 key prop을 통해 동일한 엘리먼트인지 판단합니다. 만일 동일 엘리먼트라면 변경된 속성만 갱신합니다. 동일하지 않다면 기존 노드를 버리고 새로운 노드가 추가됩니다. 이와 관련해 자세한 내용은 기회가 될 때 따로 다뤄보도록 하겠습니다.
리액트 엘리먼트는 불변객체입니다. 다시말해 한 번 생성된 엘리먼트의 type, props, children은 변경될 수 없습니다. 엘리먼트는 마치 영화 필름의 한 프레임과 같습니다. 영화 속 주인공이 움직이도록 하려면 어떻게 해야할까요. 프레임 자체는 바꿀 수 없습니다. 프레임은 그저 한 장의 사진에 불과하기 때문입니다. 영화 속에서 움직임은 지속적으로 교체되는 프레임에 의해 구현됩니다. 마찬가지로 엘리먼트는 특정 시점의 UI를 나타냅니다. UI에 변화를 주기 위해선 새로운 엘리먼트를 만들어 ReactDOM.render 메서드로 전달하는 방법밖에 없습니다.
리액트로 프로그래밍을 한다는 것은 어쩌면 영화의 프레임을 찍어내듯 각 시점의 UI를 찍어내 ReactDOM에게 전달하는 과정의 반복일지도 모르겠습니다. 물론 매번 명시적으로 ReactDOM.render를 호출하지는 않습니다. 이어지는 포스트에서는 컴포넌트를 통해 어떻게 UI에 변화를 줄 수 있는지 이야기 해보도록 하겠습니다.
React 공식문서 - JSX 소개
React 공식문서 - 엘리먼트 렌더링
React 공식문서 - JSX 이해하기
React 공식문서 - React DOM API
React 공식문서 - DOM 엘리먼트
👍