
리액트는 화면의 UI 요소를 컴포넌트 단위로 구분한다.
즉 컴포넌트란 리액트 애플리케이션의 구성 단위로, 각각이 특정 기능이나 UI 요소를 담당한다.
예를 들어, 레고로 집을 만든다고 생각해보자.
레고 블록으로 벽, 창문, 문, 지붕 등 특정 블록들을 만든다. 이렇게 만들어진 각각의 블록들을 조합하여 전체적인 집을 만든다.
이때, 각 레고 블록이 바로 리액트의 컴포넌트에 해당한다.
이러한 컴포넌트들을 조합하여 전체적인 UI를 만들어나가는 것이 리액트 애플리케이션 개발의 핵심이다.
그렇다면 컴포넌트는 어떻게 나누고 설계하는 것이 좋을까?
📌 하나의 기능을 가지게 한다.
하나의 컴포넌트가 여러 가지 역할을 수행하게 되면 재사용성과 유지보수성이 떨어지게 된다.
📌 상태나 스타일은 최소화한다.
컴포넌트는 가능한 일반적으로 설계하고, 특정 동작이나 스타일은 외부에서 넘겨 받을 수 있도록 설계하는 것이 좋다.
즉, 컴포넌트는 재사용 가능하고 유지 보수하기 쉽게 만드는 것이 중요하다.
Checkbox 컴포넌트와 Dropdown 컴포넌트를 만들어보자.
Checkbox 컴포넌트는 클릭하면 input 체크박스가 체크되고, 한번 더 클릭하면 체크가 해제된다.
import { useState } from 'react';
export function Checkbox() {
const [isChecked, setIsChecked] = useState(false);
const handleCheck = () => {
setIsChecked(!isChecked);
};
return (
<label>
<h2>checkbox</h2>
<input type="checkbox" checked={isChecked} onChange={handleCheck} />
</label>
);
}
Dropdown 컴포넌트는 h2 타이틀을 클릭하면 서브 타이틀이 보이고, 한번 더 클릭하면 서브 타이틀이 사라진다.
import { useState } from 'react';
export function Dropdown() {
const [isOpened, setIsOpened] = useState(false);
const handleOpen = () => {
setIsOpen(!isOpen);
};
return (
<div>
<h2 onClick={handleOpen}>Dropdown</h2>
{isOpened && <div>bla bla</div>}
</div>
);
}
두 컴포넌트는 UI는 다르지만, boolean 상태 값을 토글하여 UI를 변경한다는 점에서 같은 기능을 가지고 있다.
다시말해 로직이 중복된다.
이렇게 중복되는 로직을 분리하여 별개의 컴포넌트로 만들 수 있다.
import { useState } from 'react';
export function useToggle(init: boolean): [boolean, () => void] {
const [state, setState] = useState(init);
const handleState = () => {
setState(!state);
};
return [state, handleState];
}
이 컴포넌트는 상태(state)와, 상태를 토글하는 이벤트핸들러 함수(handleState)를 반환한다.
분리해준 useToggle 컴포넌트는 hook처럼 다른 컴포넌트에서 불러 사용할 수 있다. (Custom Hook)
Checkbox와 Dropdown 컴포넌트를 아래와 같이 수정한다.
// Checkbox.tsx
import { useToggle } from './useToggle';
export function Checkbox() {
const [isChecked, handleCheck] = useToggle(false);
return (
<label>
<h2>checkbox</h2>
<input type="checkbox" checked={isChecked} onClick={handleCheck} />
</label>
);
}
// Dropdown.tsx
import { useToggle } from './useToggle';
export function Dropdown() {
const [isOpened, handleOpen] = useToggle(false);
return (
<div>
<h2 onClick={handleOpen}>Dropdown</h2>
{isOpened && <div>bla bla</div>}
</div>
);
}
이외에도 다양한 UI 요소에서 동일한 토글 기능을 쉽게 재사용할 수 있다.
useToggle 컴포넌트처럼 스타일은 없고 로직만 존재하는 컴포넌트를 Headless 컴포넌트라고 한다.
이렇게 UI와 로직을 분리하면 마크업과 스타일 수정이 자유롭기 때문에 재사용성이 높아진다.
개발자는 필요에 따라 해당 컴포넌트를 다양한 UI 요소에 적용할 수 있으며, 이를 통해 코드의 중복을 줄이고 유지보수성을 높일 수 있다.
물론 모든 컴포넌트를 이렇게 분리하는 것이 좋은 것은 아니다.
로직을 분리하다보면 오히려 코드가 복잡해질 수 있다.
또한 UI가 없는 컴포넌트는 동작 방식을 이해하기 어려울 수 있기에 적절한 문서화와 코드 주석이 필요하다.
따라서 프로젝트의 특성과 요구사항을 고려한 적절한 판단과 활용이 중요하다.