엘리먼트와 컴포넌트는 리액트의 철학 중 하나입니다.
엘리먼트는 DOM 노드나 컴포넌트에 대한 type과 props를 불변 객체로 관리합니다.
이러한 특징은 컴포넌트 간 의존성을 분리시켜 컴포넌트를 혼합, 중첩할 수 있게 만듭니다.
이 글을 통해 엘리먼트와 컴포넌트, 그리고 관련된 개념들에 대해 알아보겠습니다.
(Dan Abramov의 React Components, Elements, and Instances를 읽어보는 것을 추천합니다.)
한글 버전 영상인 Boaz 유튜브의 React component, element, instance도 있습니다.)
엘리먼트 (Element)
엘리먼트는 type과 props를 가지는 불변 객체입니다.
type의 종류에 따라 DOM 엘리먼트, 컴포넌트 엘리먼트로 나뉘게 됩니다.
등장 배경
class Form extends TraditionalObjectOrientedView {
render() {
// Read some data passed to the view
const { isSubmitted, buttonText } = this.attrs;
if (!isSubmitted && !this.button) {
// Form is not yet submitted. Create the button!
this.button = new Button({
children: buttonText,
color: 'blue'
});
this.el.appendChild(this.button.el);
}
if (this.button) {
// The button is visible. Update its text!
this.button.attrs.children = buttonText;
this.button.render();
}
if (isSubmitted && this.button) {
// Form was submitted. Destroy the button!
this.el.removeChild(this.button.el);
this.button.destroy();
}
if (isSubmitted && !this.message) {
// Form was submitted. Show the success message!
this.message = new Message({ text: 'Success!' });
this.el.appendChild(this.message.el);
}
}
}
위의 코드는 OOP에 기반한 기존의 UI 모델 방식입니다.
다음과 같은 문제점들을 발견할 수 있습니다.
- 부모 컴포넌트와 자식 컴포넌트를 분리하기 어렵습니다.
(부모 컴포넌트에서 자식 컴포넌트의 생성과 종료까지 신경 써야 합니다.) - 관리할 상태(state)가 늘어나면 코드의 복잡도가 지수 단위로 늘어나게 됩니다.
- 자식 컴포넌트를 렌더링 하려면 인스턴스를 생성하고 직접 최신 상태를 유지해줘야 합니다.
React는 엘리먼트를 통해 이러한 의존성의 문제를 해결합니다.
DOM 엘리먼트
// HTML
<button class='button button-blue'>
<b>
OK!
</b>
</button>
// Element
{
type: 'button',
props: {
className: 'button button-blue',
children: {
type: 'b',
children: 'OK!'
}
}
}
- type: string (HTML 태그명)
- props: HTML 속성과 children 정보를 담은 객체
컴포넌트 엘리먼트
// JSX
<Button color="blue">OK!</Button>
// Element
{
type: Button,
props: {
color: 'blue',
children: 'OK!'
}
}
- type: 컴포넌트 (Function, Class)
- props: 컴포넌트의 프로퍼티 정보를 담은 객체
JSX
JSX는 React로 UI를 생성하는 목적을 가진 확장된 JavaScript 문법(신태틱 슈가)입니다.
JSX를 통해 HTML과 닮은 친숙한 코드로 엘리먼트를 생성할 수 있습니다.
- 신태틱 슈가(Syntactic sugar) : 내부 동작은 동일하면서 코드의 가독성을 높인 문법
등장 배경
<div id="parent">
<p class="child1">child</p>
<p class="child2">child</p>
</div>
다음과 같은 HTML 코드를 엘리먼트로 변환해보겠습니다.
엘리먼트는 React.createElement() 를 통해 생성할 수 있습니다.
// Element 생성
React.createElement('div', { id: 'parent' },
React.createElement('p', { class: 'child1' }, 'child')
React.createElement('p', { class: 'child2' }, 'child')
);
// 완성된 Element 객체
{
type: 'div',
props: {
id: 'parent'
},
children: [
{ type: 'p', props: { id: 'child1' }, 'child' }
{ type: 'p', props: { id: 'child1' }, 'child' }
]
}
매번 메서드를 통해 엘리먼트를 생성하는 것은 가독성도 떨어질뿐더러 번거롭습니다.
이를 해결하기 위해 JSX 문법이 등장하였습니다.
규칙
JSX는 HTML와 구별되는 몇 가지 규칙들이 존재하기 때문에 이를 알고 있어야 합니다.
대표적인 예시 몇 개만 적었으니 궁금하면 더 찾아보시는 것을 추천합니다.
- 태그는 반드시 닫혀있어야 합니다.
- 중괄호를 통해 JSX 내부에 JavaScript 표현식을 사용할 수 있습니다.
- class 대신 className을 사용해야 합니다.
- 모든 속성들은 camelCase로 작성해야 합니다.
- 최상위 태그는 반드시 하나여야 합니다.
(두 개 이상의 태그를 쓰고 싶다면 React의 Fragments를 사용할 수 있습니다.)
컴포넌트 (Component)
컴포넌트는 props를 입력받아 엘리먼트(엘리먼트 트리)를 반환하는 함수 혹은 클래스입니다.
또한 컴포넌트는 state(상태값)를 가질 수 있습니다.
- props : 부모 컴포넌트가 자식 컴포넌트에게 주는 값. 자식 컴포넌트 내부에서 값을 변경할 수 없음
- state : 컴포넌트 내부에서 선언되고 관리되는 값. 동적인 데이터를 다룰 때 state를 사용
리액트의 아이디어
엘리먼트와 컴포넌트를 활용한 리액트의 핵심 아이디어들을 찾아 정리해봤습니다.
💡 엘리먼트는 인스턴스가 아닌, 객체입니다.
"Element is just descriptions and not actual instances"
엘리먼트는 인스턴스가 아닙니다.
그저 DOM 노드 또는 컴포넌트를 묘사하기 위한 불변 객체(immutable)입니다.
객체 구조인 엘리먼트는 실제 DOM 노드에 비해 훨씬 가볍고 효율적입니다.
또한 따로 파싱 할 필요도 없고 탐색도 쉽습니다.
💡 컴포넌트는 엘리먼트 트리를 캡슐화합니다.
캡슐화(Encapsulation)
- 객체의 실제 구현 내용을 감추는 것 (정보 은닉)
- 외부에서는 객체 내부 구조를 알지 못하고 내부에서 제공되는 필드와 메서드만 이용할 수 있음
- 외부의 잘못된 사용으로 인해 객체가 손상되지 않도록 하기 위함
엘리먼트는 오직 type과 props, children의 최소 정보만 담고 있습니다.
이러한 특징은 엘리먼트가 컴포넌트의 생성, 종료 등을 신경 쓰지 않게 만들어줍니다.
(엘리먼트의 생성, 변경, 삭제 등의 과정은 리액트 자체에서 처리할 것입니다.)
즉, 컴포넌트간의 의존성을 분리(Decoupling)하여 사용할 수 있습니다.
덕분에 컴포넌트를 is-a, has-a 관계로 표현할 수 있게 됩니다.
💡 엘리먼트간의 중첩과 혼합
"An element describing a component is also an element, just like an element describing the DOM node. They can be nested and mixed with each other."
DOM 엘리먼트, 컴포넌트 엘리먼트 모두 DOM 트리를 묘사하기 위한 동일한 목적을 가집니다.
그리고 DOM 엘리먼트, 컴포넌트 엘리먼트 모두 동일한 객체 구조로 관리됩니다.
그래서 두 엘리먼트는 자유롭게 중첩되거나 혼합될 수 있습니다.
// Element
const DeleteAccount = () => ({
type: 'div',
props: {
children: [{
type: 'p',
props: {
children: 'Are you sure?'
}
}, {
type: DangerButton,
props: {
children: 'Yep'
}
}, {
type: Button,
props: {
color: 'blue',
children: 'Cancel'
}
}]
});
// JSX
const DeleteAccount = () => (
<div>
<p>Are you sure?</p>
<DangerButton>Yep</DangerButton>
<Button color='blue'>Cancel</Button>
</div>
);
위 코드를 보면 컴포넌트가 HTML 태그와 동일한 위계에서 관리되는 것을 볼 수 있습니다.
💡 재조정 (Reconciliation)
ReactDom.render()
, setState()
호출 시 재조정이 발생합니다.
리액트는 반복적인 재조정 과정을 통해 DOM으로만 이루어진 엘리먼트 트리를 생성합니다.
재조정 과정을 순서로 정리하면 다음과 같습니다.
- type이 컴포넌트인 컴포넌트 엘리먼트를 만남
- 해당 컴포넌트 엘리먼트에서 엘리먼트(엘리먼트 트리)를 리턴 받음
- 리턴 받은 엘리먼트가 DOM 엘리먼트가 될 때까지 1,2번을 반복
// React: You told me this...
{
type: Form,
props: {
isSubmitted: false,
buttonText: 'OK!'
}
}
// React: ...And Form told me this...
{
type: Button,
props: {
children: 'OK!',
color: 'blue'
}
}
// React: ...and Button told me this! I guess I'm done.
{
type: 'button',
props: {
className: 'button button-blue',
children: {
type: 'b',
props: {
children: 'OK!'
}
}
}
}
🔎 재조정 알고리즘 (Diffing, Fiber)
(React 16 이후로 Diffing 대신 새롭고 더 효율적인 Fiber 알고리즘이 사용된다고 합니다..)
재조정을 통해 완성된 엘리먼트 트리에 변경 사항이 생길 수 있습니다.
이때 두 엘리먼트 트리 간에는 Diffing 알고리즘을 사용하여 변경된 부분을 감지합니다.
일반적으로 트리 구조를 비교하려면 O(n^3)의 시간이 소요됩니다.
Diffing 알고리즘은 휴리스틱 한 방식을 통해 약 O(n)의 시간으로 비교를 진행합니다.
만약 props가 변하지 않았다면 더 비교하지 않는 등의 방식으로 말입니다.
- 휴리스틱(heuristics) : 한정된 시간과 비용 제약에서 빠르고 합리적인 해를 도출하는 것
(Diffing 알고리즘, Fiber 알고리즘, 가상 DOM 개념은 추후 자세히 정리하겠습니다.)
💡 가상 돔 (Virtual DOM)
가상 돔은 렌더링 효율을 극대화하는 리액트의 핵심 개념 중 하나입니다.
재조정 과정을 거치면 DOM 엘리먼트로만 이루어진 엘리먼트 트리가 생성됩니다.
이처럼 실제 돔을 객체 형태로 추상화한 트리를 가상 돔(Virtual DOM)이라고 합니다.
리액트는 상태 값 변경에 따라 화면에 변화가 생길 때마다 바로 실제 돔에 반영하지 않습니다.
리액트의 화면 업데이트는 렌더 단계와 커밋 단계를 거칩니다.
- 렌더 단계 : 가상 돔을 통해 변경 사항을 파악하는 단계
- 커밋 단계 : 변경된 부분을 실제 돔에 반영하는 단계
먼저 렌더 단계를 통해 변경 사항이 모두 반영된 가상 돔을 만듭니다.
이 과정에서 여러 개의 작은 Reflow 과정이 하나의 큰 Reflow 과정으로 됩니다.
이후 커밋 단계를 통해 변경된 부분들을 한 번에 실제 돔에 반영합니다.
많은 정보들을 전달하려는 욕심에 글의 구성이 깔끔하지 않은 것 같습니다. 🥺
이 글은 저의 첫 리액트 포스팅이기도 하기 때문에 지속적으로 업데이트할 계획입니다.
꾸준히 리액트를 공부한 뒤 다시 돌아와 더욱 보강하겠습니다.
'프론트엔드 > React' 카테고리의 다른 글
[React] SOLID 원칙을 컴포넌트에 적용하기 (0) | 2022.06.28 |
---|---|
[React] useEffect의 철학과 Lifecycle (4) | 2022.05.27 |
[React] useState의 동작 원리와 클로저 (2) | 2022.05.20 |
[React] map()에 key를 사용하는 이유 (index를 key로 쓰면 안되는 이유) (0) | 2022.05.17 |