Component
리엑트에서 컴포넌트(Component)는 UI 블럭(조각)을 기술(설명)하는 곳이다. 컴포넌트는 정규 자바스크립트 함수로 이루어져 있다. 하지만, 해당 함수는 최종적으로 React Element를 return하고 Tree를 구성하게 된다. 일반적으로 JSX 문법으로 이러한 element를 작성하게 된다. 컴포넌트는 청사진 또는 템플릿에 가깝다. 하나의 템플릿(컴포넌트)으로 리엑트는 1개 또는 그 이상의 컴포넌트 인스턴스를 생성할 수 있다.
Component Instance
React는 컴포넌트를 사용할 때 인스턴스를 생성한다. 만약, Tab이라는 컴포넌트를 1번 정의하고, 3번 사용했다면 컴포넌트 트리에 3개의 인스턴스 트리를 구성하게 된다. 즉, 컴포넌트 인스턴스는 함수로 작성한 코드 구성요소를 컴포넌트 트리로 실제 물리적(physical)으로 구현(manifestation)한 것이다. 각 인스턴스는 고유의 state와 props 그리고 라이프사이클을 가지고 있다. 컴포넌트 인스턴스는 생성(born) 후 제거(die)될때까지 사용(live)된다. 일반적으로 '컴포넌트 라이플사이클'이라고만 얘기하지만, 기술적으로는 '컴포넌트 인스턴스 라이플사이클'이 더 정확한 표현이다. 마찬가지로 UI는 '컴포넌트'로 구성되는 것이 아니라 '컴포넌트 인스턴스'로 구성되어 있다고하는 것이 더 정확하다. 기술문서나 스택오버 플로우에서 개념이 혼동되지 않도록 구분하여 알고 있어야 한다.
리엑트가 인스턴스를 통해 코드를 실행하면 각 요소들은 React Element를 return한다.
React Element
JSX는 multiple React.createElement 함수의 호출로 이루어져 있다. 리엑트가 이 함수들을 호출하면서 React element가 생성된다. 다시 말해, React element는 함수 호출의 결과로 이루어진다. React element는 커다란 불변의(immutable) 자바스크립트 객체와 같다. 리엑트는 이것을 메모리에 저장한다. 이러한 React element 객체는 현재 구성된 컴포넌트 인스턴스를 위해 DOM elements를 생성하기 위한 모든 정보를 가지고 있다. 최종적으로 React Element는 실제 DOM Element로 변환되고 브라우저에 paint된다.
DOM Element (HTML)
DOM Element는 브라우저상에서 인스턴스 컴포넌트를 실직적으로 시각적 표현을 해준다. 즉, DOM에 랜더링되는 요소는 React Element가 아니다. React Element는 React 안에서만 존재하고 DOM과는 관련이 없다. React Element는 화면을 paint할 때 쉽게 DOM Element로 변경될 수 있다.
Instance와 Element 직접 확인해보기
브라우저에서 Instance와 Element가 어떻게 나타나는지 확인해보자. 코드는 아래의 '전체 코드' 목차를 참고하면 된다.
코드를 확인해보면 DifferentContent 컴포넌트가 존재한다. 해당 컴포넌트의 Instance를 확인하려면 'console.log(<DifferentContent />'라고 입력하면 된다. 그러면 브라우저 콘솔창에 아래와 같은 정보가 나타난다.
- type은 해당 컴포넌트의 이름을 나타낸다.
- props는 컴포넌트에 전달되는 props 값을 나타난다. 현재는 아무런 props가 없어서 공백이다. 만약, 'console.log(<DifferentContent test={23} />'이라고 입력했다면 'props: {test: 23}'이라고 나타난다.
- $$typeof는 Cross-Site Scripting 공격을 방어하기 위한 보안 기능을 위해 존재한다. Symbol 타입을 사용하는데 Symbol은 자바스크립트 Primitives(원시) 타입으로 JSON으로 변경이 불가하다. 즉, Symbol은 API 호출을 통해 들어올 수 데이터이다. 만약, 해커가 API를 통해 가짜 React Element를 보내는 경우 Symbol 타입을 찾을 수 없기 때문에 React는 DOM에 해당 요소를 사용하지 않는다.
"<DifferentContent />"와 같이 리엑트가 랜더링할 때 컴포넌트를 내부적으로 호출한 경우 왜 직접적으로 호출하지 않는 것일까? "DifferentContent()"로 사용하면 되지 않을까? "console.log(DifferentContent());"를 입력하면 아래와 같이 콘솔에 나타난다.
type에 div라는 내용이 들어가 있다. 이는 HTML의 div를 나타내는 것이며, 결국 인스턴스가 아니라 React Element로 인지한다는 것이다. 또한, props에 className: 'tab-content'가 들어가 있는데 이는 React가 더 이상 컴포넌트 인스턴스가 아니라 단순히 React Element로 인지한다는 두번째 신호이다. Tabbed 함수 안에 아래의 이미지와 같이 상기 코드를 주석처리하고 "{TabContent({ item: content.at(0) })}"를 작성하여 함수를 직접 호출 해보자.
Components 탭에서 트리를 확인해보면 아래의 이미지와 같이 TabContent라는 컴포넌트 트리가 존재하지 않는다. 이 경우 TabContent는 부모 컴포넌트인 Tabbedd에서 관리된다. 아래 이미지는 Tabbed를 클릭한 상태이며 Tabbed hooks에 activeTab State를 제외하고, TabContent이라는 다른 hook이 존재한다. 즉, 인스턴스가 생성되지 않아 TabContent의 고유의 State로 관리되지 않으며 이는 hook 규칙에 위배된다.
아래의 이미지와 같이 코드를 작성하여 컴포넌트를 정상 호출해보자.
인스턴스가 생성되어 컴포넌트 트리에 TabContent가 나타난다. 추가적으로 Tab 컴포넌트를 1번만 정의하였지만, 4번 사용하여 인스턴스가 4개 생성되었다. 각각의 인스턴스는 고유의 State와 Props를 가지며 별도로 관리된다.
전체 코드
Instance와 Element를 브라우저에서 확인하기 위해서 사용했던 전체 코드이다. 4개의 탭을 각각 다른 콘텐츠로 구성한 코드이다.
import { useState } from 'react';
const content = [
{
summary: 'React is a library for building UIs',
details:
'Dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
},
{
summary: 'State management is like giving state a home',
details:
'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
},
{
summary: 'We can think of props as the component API',
details:
'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.',
},
];
export default function App() {
return (
<div>
<Tabbed content={content} />
</div>
);
}
function Tabbed({ content }) {
const [activeTab, setActiveTab] = useState(0);
return (
<div>
<div className='tabs'>
<Tab num={0} activeTab={activeTab} onClick={setActiveTab} />
<Tab num={1} activeTab={activeTab} onClick={setActiveTab} />
<Tab num={2} activeTab={activeTab} onClick={setActiveTab} />
<Tab num={3} activeTab={activeTab} onClick={setActiveTab} />
</div>
{activeTab <= 2 ? (
<TabContent item={content.at(activeTab)} />
) : (
<DifferentContent />
)}
</div>
);
}
function Tab({ num, activeTab, onClick }) {
return (
<button
className={activeTab === num ? 'tab active' : 'tab'}
onClick={() => onClick(num)}
>
Tab {num + 1}
</button>
);
}
function TabContent({ item }) {
const [showDetails, setShowDetails] = useState(true);
const [likes, setLikes] = useState(0);
function handleInc() {
setLikes(likes + 1);
}
return (
<div className='tab-content'>
<h4>{item.summary}</h4>
{showDetails && <p>{item.details}</p>}
<div className='tab-actions'>
<button onClick={() => setShowDetails((h) => !h)}>
{showDetails ? 'Hide' : 'Show'} details
</button>
<div className='hearts-counter'>
<span>{likes} ❤️</span>
<button onClick={handleInc}>+</button>
<button>+++</button>
</div>
</div>
<div className='tab-undo'>
<button>Undo</button>
<button>Undo in 2s</button>
</div>
</div>
);
}
function DifferentContent() {
return (
<div className='tab-content'>
<h4>I'm a DIFFERENT tab, so I reset state 💣💥</h4>
</div>
);
}
'React' 카테고리의 다른 글
[React] Rendering은 어떻게 동작할까? (2/3) - Render 단계 (1) | 2024.02.15 |
---|---|
[React] Render의 진짜 의미와 동작 원리 (1/3) - Overview (0) | 2024.02.12 |
[React] useMemo를 사용한 화면 깜빡거림 제거 (0) | 2024.01.09 |
[React] Typescript에서 React useRef 사용법 (0) | 2024.01.08 |