Post

컴포넌트 구조와 책임 분리 기준

컴포넌트 책임, 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 조각이 아니라 역할 단위
  • 나눌수록 좋은 게 아니라, 의미가 분리될 때 나눈다
  • 구조는 숨기면 안 되고, 드러나야 한다

컴포넌트를 보고 “아, 이런 역할이구나”라는 생각이 들지 않는다면, 그건 이미 구조가 아니라 단순 분해다.


This post is licensed under CC BY 4.0 by the author.