
React로 개발하다 보면 한 번쯤 이런 고민을 하게 된다.
“부모 컴포넌트 코드가 너무 많아졌는데 하위 컴포넌트로 나눠야 할까?”
특히 과제나 프로젝트를 진행할 때 컴포넌트 분리 기준이 모호해지는 경우가 많다.
이번 글에서는 실제로 고민했던 예제 코드를 기준으로 실무에서 사용하는 컴포넌트 분리 기준을 정리해보려고 한다.
다음과 같은 구조가 있다고 가정해보자.
function DummyData(millisecond: number = 100): Promise<number> {
return new Promise(resolve =>
setTimeout(() => resolve(Math.floor(Math.random() * 100)), millisecond)
);
}
const App = () => {
const [varA, setVarA] = useState<number>();
const [varB, setVarB] = useState<number>();
const [varC, setVarC] = useState<number>();
const fetchA = useCallback(async () => setVarA(await DummyData()), []);
const fetchB = useCallback(async () => setVarB(await DummyData()), []);
useEffect(() => {
fetchA();
fetchB();
}, [fetchA, fetchB]);
const handleClickA = () => fetchA();
const handleClickB = () => fetchB();
const handleClickC = (value: number) => setVarC(value);
return (
<div style={{ display: "flex", flexDirection: "column" }}>
<ChildA values={[varA, varB, varC]} onClick={handleClickA} />
<ChildB values={[varA, varB, varC]} onClick={handleClickB} />
<ChildC values={[varA, varB, varC]} onClick={handleClickC} />
</div>
);
};
그리고 버튼 UI를 담당하는 컴포넌트:
const Button = ({ values, color, onClick }) => (
<button onClick={onClick} style={{ backgroundColor: color }}>
{values.map((e, idx) => (
<div key={idx}>{idx + 1 + ": " + e}</div>
))}
</button>
);
Child 컴포넌트들은 거의 동일한 구조를 가진다.
const ChildA = ({ values, onClick }) => {
return <Button onClick={onClick} color="purple" values={values} />;
};
const ChildB = ({ values, onClick }) => {
return <Button onClick={onClick} color="blue" values={values} />;
};
이 시점에서 자연스럽게 드는 생각이 있다.
“App 컴포넌트가 길어지는데 Child로 더 나눠야 할까?”
실무 기준에서는 다음 원칙을 더 중요하게 본다.
코드 길이보다 역할 분리가 더 중요하다
부모 컴포넌트가 길어지는 것 자체는 문제가 아니다.
오히려 React 구조에서는 정상적인 경우가 많다.
React에서의 역할 분리 구조
실무에서는 보통 다음 4개의 레이어로 나눈다.
services → API / 데이터 가공
hooks → 상태 + 로직
features → 도메인 UI
components → 순수 UI
예:
App
└ FeatureButton
└ Button
Container 컴포넌트는 다음 역할을 가진다.
그래서 코드가 길어지는 것이 자연스럽다.
문제가 되는 건 “길이”가 아니라 “책임이 섞이는 것”이다.
다음 기준으로 컴포넌트를 나누면 구조가 망가지기 쉽다.
이건 유지보수성을 오히려 떨어뜨린다.
보통 아래 네 가지 기준으로 판단한다.
state를 소유해야 하는 단위인가?
다른 화면에서도 사용할 수 있는가?
컴포넌트 이름이 역할을 설명하는가?
독립적으로 수정될 가능성이 있는가?
ChildA/B는 사실 컴포넌트라기보다 설정값 차이에 가깝다.
차이점:
이 경우는 컴포넌트 분리 대상이 아니라 Feature 컴포넌트 하나로 통합하는 것이 더 자연스럽다.
interface FeatureButtonProps {
values: number[];
color: string;
onClick: () => void;
}
const FeatureButton = ({ values, color, onClick }: FeatureButtonProps) => {
return <Button values={values} color={color} onClick={onClick} />;
};
사용:
<FeatureButton values={[varA, varB, varC]} onClick={handleClickA} color="purple" />
<FeatureButton values={[varA, varB, varC]} onClick={handleClickB} color="blue" />
컴포넌트를 나누기 전에 먼저 고려해야 할 방법이 있다.
hook으로 로직 분리
function useDummyValues() {
const [varA, setVarA] = useState<number>();
const [varB, setVarB] = useState<number>();
const fetchA = async () => setVarA(await DummyData());
const fetchB = async () => setVarB(await DummyData());
return { varA, varB, fetchA, fetchB };
}
App:
const { varA, varB, fetchA, fetchB } = useDummyValues();
이건 거의 규칙처럼 쓰인다.
JSX가 많으면 컴포넌트로 나누고
로직이 많으면 hook으로 나눈다.
React에서 컴포넌트 분리는 “코드 정리” 문제가 아니라
책임 설계 문제에 가깝다.
부모 컴포넌트가 길어지는 것을 두려워하기보다
다음 질문을 먼저 해보는 것이 좋다.
이 컴포넌트는 독립적인 역할을 가지는가?
이 기준만 지켜도 컴포넌트 구조는 자연스럽게 정리된다.