컴포넌트 구조와 책임 분리 기준
컴포넌트 책임, Fragment 사용, 네이밍, key와 구조화 원칙을 다룬다
컴포넌트는 구조다
— JSX를 쪼개는 것과 설계를 하는 것은 전혀 다르다
서론: 컴포넌트를 “나눈다”는 착각
React를 어느 정도 쓰다 보면 컴포넌트를 잘게 나누기 시작한다.
문제는 이 시점부터다.
- 파일은 많아졌는데, 구조는 더 복잡해졌다
- 컴포넌트는 쪼갰는데, 수정은 더 어렵다
- “이 컴포넌트가 왜 존재하는지” 설명하기 힘들다
이유는 단순하다.
컴포넌트를 JSX 묶음으로만 보고, ‘역할과 책임’ 단위로 보지 않았기 때문이다.
이 글의 목표는
컴포넌트를 잘게 쪼개는 기준이 아니라,
언제 쪼개고, 언제 묶어야 하는지 판단 기준을 제공하는 것이다.
1) 컴포넌트의 책임 범위
왜 중요한가
컴포넌트의 책임이 불분명하면, 수정 시 영향 범위를 예측할 수 없다.
잘못된 예
1
2
3
4
5
6
7
function UserPage() {
// 데이터 패칭
// 필터 상태 관리
// 리스트 렌더링
// 모달 제어
// 버튼 이벤트 처리
}
한 컴포넌트가 페이지 로직 + UI + 상태 + 이벤트를 전부 책임진다. 이 상태에서 “조금만 고치자”는 말은 거의 불가능해진다.
더 나은 설계
- 컴포넌트 하나 = 하나의 책임
- 책임은 “JSX 일부”가 아니라 의미 있는 역할
1
2
3
4
5
<UserPage>
<UserFilter />
<UserList />
<UserModal />
</UserPage>
실무 체크리스트
- 이 컴포넌트의 역할을 한 문장으로 설명할 수 있는가
- 상태/로직/UI가 한 컴포넌트에 과도하게 섞여 있지 않은가
- “이 컴포넌트는 왜 존재하는가”에 답할 수 있는가
- 수정 요청이 왔을 때 고칠 파일을 바로 떠올릴 수 있는가
- 책임이 늘어날 때 분리 시점을 인지하고 있는가
2) Fragment를 써야 할 때 / 피해야 할 때
왜 중요한가
Fragment는 구조를 숨긴다. 잘 쓰면 깔끔하지만, 남용하면 맥락이 사라진다.
잘못된 예
1
2
3
4
5
<>
<Title />
<Description />
<Actions />
</>
이 Fragment가 무슨 의미인지 알 수 없다. 단순히 DOM를 줄이기 위해 구조를 없앤 상태다.
더 나은 설계
Fragment는 의미 없는 그룹일 때만 사용한다. 의미가 생기면 컴포넌트로 승격한다.
1
2
3
4
5
<Header>
<Title />
<Description />
<Actions />
</Header>
실무 체크리스트
- Fragment가 “의미 없는 묶음”인가, 아니면 책임을 숨기고 있는가
- 나중에 스타일/로직이 붙을 가능성은 없는가
- 이 묶음에 이름을 붙일 수 있는가(붙일 수 있다면 컴포넌트)
- Fragment가 중첩되어 가독성을 해치지 않는가
- DOM 최소화가 설계 명확성보다 우선되고 있지 않은가
3) 컴포넌트 네이밍의 영향
왜 중요한가
컴포넌트 이름은 사용자(다른 개발자)를 안내하는 문서다.
잘못된 예
1
2
3
4
<Item />
<Box />
<Wrapper />
<Data />
이 이름들은 역할을 말해주지 않는다. 사용자는 내부 구현을 열어보기 전까지 판단할 수 없다.
더 나은 설계
- “무엇인가”보다 “무슨 역할인가”
- UI 동작이 아니라 도메인 의미 중심
1
2
3
<UserList />
<UserListItem />
<UserActions />
실무 체크리스트
- 이름만 보고 책임이 추측되는가
- UI 구조가 아니라 도메인 개념을 드러내는가
Wrapper,Container같은 회피성 이름을 쓰고 있지 않은가- 동일한 역할의 컴포넌트가 다른 이름으로 존재하지 않는가
- 이름이 길어졌다면, 책임이 명확해진 결과인가
4) JSX를 함수로 반환한다는 의미
왜 중요한가
JSX를 함수로 빼는 순간, 구조가 아니라 로직이 된다.
잘못된 예
1
2
3
4
5
6
7
8
function renderHeader() {
return (
<div>
<h1>Title</h1>
<button>Save</button>
</div>
);
}
이 함수는 컴포넌트도 아니고, 단순 JSX 조각이다. 역할도, 재사용 기준도 불명확하다.
더 나은 설계
- JSX를 함수로 빼는 이유가 조건 분기/재사용/가독성 중 무엇인지 명확히 한다
- 의미가 있으면 컴포넌트로 만든다
1
2
3
4
5
6
7
8
function Header() {
return (
<header>
<h1>Title</h1>
<SaveButton />
</header>
);
}
실무 체크리스트
- JSX를 함수로 뺀 이유를 설명할 수 있는가
- 이 함수는 상태/props를 가지는가(그렇다면 컴포넌트)
- 재사용 가능성이 실제로 존재하는가
- 함수 분리가 구조를 명확히 했는가, 숨겼는가
- 나중에 확장될 가능성을 고려했는가
5) inner component 선언의 비용
왜 중요한가
컴포넌트 안에 컴포넌트를 선언하면 매 렌더마다 재정의된다.
잘못된 예
1
2
3
4
5
6
function Page() {
function Header() {
return <h1>Title</h1>;
}
return <Header />;
}
작아 보여도, 렌더링/메모이제이션/디버깅 비용이 숨어 있다.
더 나은 설계
- inner component는 정말로 외부 재사용이 불가능할 때만
- 대부분은 파일 상단으로 끌어올린다
실무 체크리스트
- inner component가 부모 상태에 강하게 의존하는가
- 렌더링 성능/메모이제이션에 영향이 없는가
- 디버깅 시 컴포넌트 트리가 읽기 쉬운가
- 파일 외부로 빼면 책임이 더 명확해지는가
- “편해서” 안에 선언한 건 아닌가
6) 리스트 key의 실제 역할
왜 중요한가
key는 경고를 없애는 옵션이 아니라, 컴포넌트 정체성의 기준이다.
잘못된 예
1
2
3
items.map((item, index) => (
<Item key={index} />
));
정렬/추가/삭제가 발생하면, React는 다른 아이템을 같은 컴포넌트로 착각한다.
더 나은 설계
- key는 변하지 않는 고유 식별자
- index는 “순서가 절대 안 바뀔 때만”
1
2
3
items.map(item => (
<Item key={item.id} />
));
실무 체크리스트
- 이 key는 아이템의 “정체성”을 표현하는가
- 정렬/필터/삽입이 일어날 가능성은 없는가
- key 변경이 컴포넌트 재생성을 유발하는가
- index 사용을 선택한 이유를 설명할 수 있는가
- key 문제로 상태가 섞일 위험은 없는가
7) Raw HTML을 다루는 기준
왜 중요한가
Raw HTML은 React의 안전장치를 우회한다.
잘못된 예
1
<div dangerouslySetInnerHTML={{ __html: html }} />
XSS, 구조 파악 불가, 스타일/이벤트 제어 불가 문제가 뒤따른다.
더 나은 설계
- 정말 필요한 경우에만 사용
- 사용 범위를 컴포넌트 하나로 격리
- 입력 소스와 책임을 명확히 한다
실무 체크리스트
- 이 HTML은 신뢰 가능한 출처인가
- Raw HTML이 필요한 이유를 설명할 수 있는가
- 사용 범위가 컴포넌트로 격리돼 있는가
- 대체 표현(JSX)으로 풀 수는 없는가
- 보안/스타일/이벤트 제어 문제를 인지하고 있는가
결론: 좋은 컴포넌트 구조의 기준
- 컴포넌트는 JSX 조각이 아니라 역할 단위
- 나눌수록 좋은 게 아니라, 의미가 분리될 때 나눈다
- 구조는 숨기면 안 되고, 드러나야 한다
컴포넌트를 보고 “아, 이런 역할이구나”라는 생각이 들지 않는다면, 그건 이미 구조가 아니라 단순 분해다.
- 참고: 클린코드 리액트(React)
