리액트에서 컴포넌트란 마크업, CSS 및 JavaScript를 앱에서 재사용 가능한 UI 요소로 결합한 것을 말한다. 리액트는 컴포넌트를 통해 UI를 재사용 가능한 개별적인 여러 조각으로 나누고, 각 조각을 개별적으로 살펴볼 수 있다. 개념적으로 컴포넌트는 JavaScript 함수와 유사하다. “props”라고 하는 임의의 입력을 받은 후, 화면에 어떻게 표시되는지를 기술하는 React elements를 반환한다.
엘리먼트는 컴포넌트의 구성 요소이다. React 엘리먼트는 불변객체이며, 엘리먼트를 생성한 이후에는 해당 엘리먼트의 자식이나 속성을 변경할 수 없다. 엘리먼트는 하나의 프레임과 같이 특정 시점의 UI를 보여준다.
리액트에는 함수 컴포넌트와 클래스 컴포넌트가 존재한다.
이 둘은 각각 함수 컴포넌트와 클래스 컴포넌트를 직접 다룰 때 자세히 다루도록하고 이 글에서는 함수 컴포넌트를 사용해서 컴포넌트에 대해서 설명하도록 하겠다.
// 함수 컴포넌트
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
// 클래스 컴포넌트
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
React의 관점에서 볼 때 위 두 가지 유형의 컴포넌트는 동일하다.
React 컴포넌트는 “props”를 사용하여 서로 통신한다. 상위 컴포넌트는 props를 제공하여 하위 컴포넌트에 일부 정보를 전달할 수 있다. props는 “HTML attribute”처럼 생겼지만 모든 JavaScript 값을 전달할 수 있다. 어떻게 props를 사용할 수 있는지 알아보자.
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
const element = <Welcome name="Harry" />;
ReactDOM.render(element, document.getElementById('root');
위 코드는 페이지에 “Hello, Harry”를 렌더링하는 예시 코드이다.
이 코드를 실행하면 어떤 일이 일어나는지 알아보자.
<Welcome name="Harry" />
엘리먼트로 ReactDOM.render()
를 호출한다.{name: 'Harry'}
를 props로 하여 Welcome
컴포넌트를 호출한다.Welcome
컴포넌트는 결과적으로 <h1>Hello, Harry</h1>
엘리먼트를 반환한다.<h1>Hello, Harry</h1>
엘리먼트와 일치하도록 DOM을 효율적으로 업데이트한다React는 작성하는 모든 컴포넌트는 순수 함수라고 가정하고 설계되었다. 그렇기 때문에 React의 컴포넌트는 동일한 입력에 대해 동일한 출력을 반환해야하며, 외부에 영향을 줘서는 안 된다.
그렇기 때문에 “props”를 수정해서는 안 된다.
React 엘리먼트에서 이벤트를 처리하는 방식은 DOM 엘리먼트에서 이벤트를 처리하는 방식과 매우 유사하다. 몇 가지 문법 차이는 다음과 같다. React의 이벤트는 소문자 대신 캐멀 케이스(camelCase)를 사용하 JSX를 사용하여 문자열이 아닌 함수로 이벤트 핸들러를 전달한다.
// HTML
<button onclick="activateLasers()">
Activate Lasers
</button>
// React
<button onClick={activateLasers}>
Activate Lasers
</button>
일반적으로 이벤트 핸들러 함수는 컴포넌트 내부에 정의되고, “handle”로 시작하는 이름 뒤에 발생할 이벤트 이름이 붙는다. 예를 들어, onClick={handleClick}
와 같다. 이 때 함수는 DOM 엘리먼트와 달리 함수 호출이 아닌 함수 자체를 이벤트 핸들러 어트리뷰트에 바인딩해야한다.
위에서 컴포넌트는 순수 함수이기때문에 “props”를 수정해서는 안 된다고 했다. 그러나 실제 웹 애플리케이션의 UI는 동적이며 상호 작용의 결과로 화면에 표시되는 내용을 변경해야 하는 경우가 많다.
function Component(props) {
let index = 0;
function handleClick() {
index = index + 1;
}
return (<button onClick={handleClick}>{index}</button>);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Component />);
위의 코드는 props를 수정하지 않고, 내부 지역 변수 index를 활용해서 동작한다. 나는 button을 클릭하면 index가 증가되어 렌더링되는 것을 원했지만 위 코드는 그렇게 동작하지 않는다.
여기에는 두 가지 이유가 있다.
즉, 새 데이터로 컴포넌트를 업데이트하려면 렌더 간의 데이터를 유지하면서 React가 새 데이터로 컴포넌트를 다시 렌더링하도록 인식하도록 해야한다.
리액트에서 이를 위해서 존재하는 컴포넌트 별 메모리인 “state”가 있다.
앞의 예제 코드를 state를 활용한 코드로 바꿔보도록 하겠다.
const { useState } = React;
// useState는 함수형 컴포넌트에서 사용할 수 있는 Hooks이다.
// 함수형 컴포넌트에서 useState를 통해 state 변수를 만들 수 있다는 사실만 받아들이자.
function Component(props) {
const [index, setIndex] = useState(0);
// React는 초기 상태를 한 번 저장하고 다음 렌더링에서 이를 무시한다.
function handleClick() {
setIndex(index + 1);
// state를 직접 수정하지 말아야 한다.
// state를 직접 수정하면 컴포넌트를 다시 렌더링하지 않는다.
}
return (<button onClick={handleClick}>{index}</button>);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Component />);
이제 함수 컴포넌트에서 state를 활용해서 상호작용에 대응할 수 있게 되었다. 리액트에서 state의 비교는 전체 state를 비교하는 것이 아닌 이전 state와의 레퍼런스로 비교를 통해 변화를 감지하기 때문에 불변성을 지켜주어야한다.
리액트는 컴포넌트 기반의 View를 중심으로 한 라이브러리이다. 각각의 컴포넌트에는 생명주기(lifecycle)이 존재한다. 생명주기(lifecycle)란 보통 컴포넌트가 페이지에서 렌더링되기 전인 생성시기인 Mount
, props나 state가 변경되는 Update
, 마지막으로 페이지에서 제거되는 Unmount
로 이루어진다.
클래스 컴포넌트와 함수 컴포넌트의 lifecycle event들은 완전히 다르다. 아까 설명한 것처럼 각각을 다루면서 더 자세히 알아보도록하자.
리액트는 강력한 합성 모델을 가지고 있으며, 컴포넌트는 합성을 이용해 코드를 재사용할 수 있다. 만약 내가 Button
컴포넌트를 가지고 있다면 이를 다른 컴포넌트에서 참조하여 사용할 수 있다.
아래 코드를 살펴보자.
function Button(props) {
return <button>{props.value}</button>;
}
function App() {
return (
<>
<Button value="Y"/>
<Button value="N"/>
</>
)
}
이러한 방식을 통해 컴포넌트의 합성을 할 수 있고, 이러한 합성을 활용하기 위해 컴포넌트를 작은 단위로 추출해 작은 컴포넌트로 만들고, 이를 다른 컴포넌트에 합성하면 코드를 재사용할 수 있다.