반응형

리액트 컴포넌트 설계 마스터:
Atomic Design, 합성, 상속 완전정복

리액트 컴포넌트를 더 유연하고 재사용 가능하게 만들고 싶다면,
Atomic Design과 합성, 상속
제대로 이해해야 해요! 🚀
반응형

 

안녕하세요, 개발자 여러분 😊
리액트를 처음 배울 땐 UI만 띄우는 것도 벅찼는데, 어느 순간부터는 코드의 재사용성구조의 일관성이 중요해지죠.
그래서 이번 글에서는 실무에서도 강력하게 통하는 Atomic Design 패턴부터, 컴포넌트 합성, 상속, 그리고 HOC, Render Props 패턴까지 한 번에 정리해드리려 합니다.

실전 예제와 함께 쉽게 설명해드릴 테니, 초보자 분들도 걱정 마세요!

 

1. Atomic Design 패턴이란? 🧬

처음 리액트를 배우다 보면, 어떤 컴포넌트를 어디까지 쪼개야 할지 고민되죠?

이럴 때 Atomic Design은 정말 유용한 설계 철학이자 구조화 전략이 되어줘요.

Atomic Design은 ‘작은 단위부터 큰 단위까지 계층적으로 UI를 구성하자’는 아이디어에서 출발했는데요, 이름처럼 ‘원자’ 단위로 UI를 분해하고 조립하는 접근입니다.

Atomic Design 구성요소 🔍

Atomic Design은 아래의 다섯 가지 구성요소로 나뉘어요:

  • Atoms: 버튼, 인풋 같은 더 이상 나눌 수 없는 UI 요소들
  • Molecules: Label + Input 같이 Atoms를 조합한 간단한 기능 단위
  • Organisms: Header, Footer처럼 여러 Molecule이 모인 복합 구조
  • Templates: 페이지의 레이아웃 구조 (Organisms 배치)
  • Pages: 실제 콘텐츠가 들어간 완성된 페이지

Atomic Design, 왜 쓰는 걸까? 🤔

Atomic Design의 가장 큰 장점은 재사용성과 일관성이에요.

프로젝트가 커질수록 컴포넌트가 복잡해지고 중복되는 코드가 생기기 쉬운데, 원자 단위로 분리해두면 언제든 조립해서 다른 곳에 사용할 수 있어요.

예를 들어,

어떤 버튼 하나를 수정했는데 그게 로그인 페이지, 회원가입 페이지, 마이페이지까지 다 바뀌어야 한다면? 🫠

이럴 땐 원자 단위로 관리하는 Atomic 구조가 유지보수의 신세계를 열어줍니다!

🧾 예제

// Atoms/Button.jsx
const Button = ({ children, onClick }) => (
  <button onClick={onClick} style={{ padding: '8px 16px', background: '#1b6ca8', color: '#fff' }}>
    {children}
  </button>
);

// Molecules/FormGroup.jsx
import Button from '../atoms/Button';
const FormGroup = () => (
  <div>
    <label>이메일</label>
    <input type="email" />
    <Button>제출</Button>
  </div>
);

 

이처럼 Atoms, Molecules, Organisms 순서로 쪼개서 설계하면 리팩토링도 쉽고, 다른 프로젝트에 복사해서 붙여넣기만 해도 호환이 척척 됩니다.

 

 

2. 컴포넌트 합성이란? 🔧

리액트에서 가장 강력한 개념 중 하나가 바로 컴포넌트 합성(Component Composition)이에요.

말 그대로, 컴포넌트를 다른 컴포넌트 안에 포함시켜서 더 큰 UI 구조를 만드는 거죠.

마치 레고처럼요!

합성은 코드 재사용성을 극대화하고, 공통 UI를 유연하게 확장할 수 있게 도와줘요.

이건 단순히 "다시 쓰기"의 개념이 아니라, 역할과 책임을 나눠서 컴포넌트를 더 똑똑하게 설계하는 방식이기도 하죠.

리액트의 기본 합성 방식: children 속성 🌱

리액트에서 컴포넌트를 합성하는 가장 기본적인 방법은 바로 props.children을 사용하는 거예요.

부모 컴포넌트가 자식 컴포넌트를 감싸고, 자식은 전달된 내용을 자신의 위치에 출력하죠.

// Wrapper.jsx
const Wrapper = ({ children }) => (
  <div style={{ border: '2px solid #1b6ca8', padding: '20px', borderRadius: '8px' }}>
    {children}
  </div>
);

// App.jsx
<Wrapper>
  <h2>안녕하세요!</h2>
  <p>이 부분은 Wrapper 컴포넌트 내부에 렌더링돼요.</p>
</Wrapper>

이렇게 하면 Wrapper 컴포넌트는 언제든지 다른 UI 요소를 감쌀 수 있는 공통 컴포넌트로 활용할 수 있죠.
정말 실용적인 방식이에요!

명시적 합성: 슬롯 패턴 🧩

때로는 children을 넘기는 것만으로는 부족할 수 있어요.

예를 들어 Header, Footer, Content를 각각 명확히 나누고 싶을 때는 슬롯 패턴을 쓰는 게 좋아요.

// Layout.jsx
const Layout = ({ header, content, footer }) => (
  <div>
    <header>{header}</header>
    <main>{content}</main>
    <footer>{footer}</footer>
  </div>
);

// App.jsx
<Layout
  header={<h1>타이틀입니다</h1>}
  content={<p>본문 내용입니다</p>}
  footer={<small>© 2025</small>}
/>
 

이 패턴을 사용하면 컴포넌트의 역할이 명확해지고, 구조도 훨씬 직관적이 돼요.

특히 복잡한 레이아웃에서 효과적입니다.

📋 합성 vs 상속

리액트 팀에서도 공식 문서에서 말하죠. "React는 상속보다 합성을 선호합니다."
이유는요? 합성은 컴포넌트 간 결합도를 낮추고, 더 명확한 데이터 흐름을 만들 수 있기 때문이에요.

 

그럼 다음 단계에서는 “왜 리액트는 상속보다 합성을 더 선호하는가?”를 실제로 비교해보며 알아볼게요!

 

 

3. 상속은 리액트에서 어떻게 다룰까? 🧩

리액트는 객체지향 프로그래밍에서 익숙한 상속을 기본 전략으로 삼지 않아요. 왜일까요? 🤔

컴포넌트 간의 관계가 복잡해지고 유지보수가 어려워지기 때문이에요.

리액트는 대신 합성(Composition)을 활용한 명확한 구성과 책임 분리를 더 선호하죠.

클래스 상속 vs 컴포넌트 합성 비교표 📊

구분 상속 (Inheritance) 합성 (Composition)
관계 구조 수직적 (부모-자식) 수평적 (독립적 조합)
유지보수 복잡하고 어렵다 간단하고 예측 가능
테스트 상위 클래스 영향 받음 각각 독립 테스트 가능
유연성 확장에 한계 있음 유연한 재사용 가능

왜 React는 상속보다 합성을 선택했을까? 💡

React의 설계 철학은 "컴포넌트는 독립적이고, 예측 가능하게"입니다.

상속 기반의 구조는 기능을 재사용하는 데는 편리할 수 있지만, 상위 컴포넌트의 변경이 하위 컴포넌트에 직접적인 영향을 준다는 치명적인 단점이 있어요.

반면, 합성은 이런 문제를 원천적으로 차단해 줍니다.

각 구성 요소는 독립적으로 존재하고, 합쳐질 때만 기능이 발생하니까요. 마치 퍼즐처럼요! 🧩

💡 실제 코드로 비교해보기

// 상속을 사용한 컴포넌트 설계 (지양)
class Button extends React.Component {
  render() {
    return <button style={{ background: 'blue' }}>{this.props.label};
  }
}

class DangerButton extends Button {
  render() {
    return <button style={{ background: 'red' }}>{this.props.label};
  }
}

// 합성을 사용한 컴포넌트 설계 (권장)
const Button = ({ children, style }) => (
  <button style={{ ...style, padding: '8px 16px' }}>{children}</button>
);

const DangerButton = ({ children }) => (
  <Button style={{ background: 'red', color: 'white' }}>{children}</Button>
);

 

보이시죠? 합성 방식은 훨씬 간단하고, 각 기능이 독립적이기 때문에 수정도 훨씬 쉬워요.

이게 바로 리액트가 합성을 선호하는 이유입니다.

 

 

4. HOC(Higher-Order Component) 패턴 🧠

리액트에서는 컴포넌트를 마치 함수처럼 다루는 패턴이 있는데요,

그게 바로 HOC(Higher-Order Component)입니다.

고차 컴포넌트라고도 불리죠.

이건 컴포넌트를 인수로 받아서, 새로운 기능을 덧붙인 새 컴포넌트를 반환하는 함수예요.

HOC의 기본 구조 🔁

고차 컴포넌트는 보통 이런 식으로 작성돼요:

// withLogger.js
const withLogger = (WrappedComponent) => {
  return (props) => {
    console.log("Props:", props);
    return <WrappedComponent {...props} />;
  };
};

// 사용 예시
const Hello = ({ name }) => <h1>안녕하세요, {name}님!</h1>;
const HelloWithLogger = withLogger(Hello);

<HelloWithLogger name="민지" />;

 

이렇게 HOC는 원래 컴포넌트를 변경하지 않고 확장할 수 있어서, 인증 처리, 로깅, 권한 제어 같은 부가 기능을 추가할 때 많이 사용돼요.

HOC 사용 시 주의할 점 ⚠️

  • Props 충돌 방지: HOC가 전달하는 props와 원본 컴포넌트의 props가 겹치지 않도록 조심해야 해요.
  • displayName 설정: 디버깅을 위해 HOC로 감싼 컴포넌트에 식별 가능한 이름을 설정하면 좋아요.
  • Wrapper 남용 금지: HOC를 너무 많이 중첩하면 컴포넌트 트리가 복잡해져서 디버깅이 어려워질 수 있어요.

🧾 HOC를 언제 쓰면 좋을까?

HOC는 반복적인 로직을 한 번에 추상화하고 싶을 때 유용해요.

예를 들어 로그인 여부 체크, API 통신 후 데이터 전달 등…

공통 기능을 재활용할 수 있는 곳이라면 HOC가 제격이죠.

하지만 너무 남발하면 컴포넌트의 흐름이 꼬이기 쉬우니, 적절한 선에서, 필요한 경우에만 사용하는 게 좋아요!

 

 

5. Render Props 패턴 이해하기 🎥

리액트에서 컴포넌트 재사용성을 높이는 또 다른 강력한 기법이 바로 Render Props 패턴입니다.

이건 말 그대로 props로 함수를 전달해서, 그 함수가 내부에서 렌더링할 내용을 결정하게 하는 방식이에요.

Render Props의 기본 구조 🔁

// MouseTracker.jsx
class MouseTracker extends React.Component {
  state = { x: 0, y: 0 };

  handleMouseMove = (event) => {
    this.setState({ x: event.clientX, y: event.clientY });
  };

  render() {
    return (
      <div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}>
        {this.props.render(this.state)}
      </div>
    );
  }
}

// App.jsx
<MouseTracker render={({ x, y }) => (
  <h1>마우스 위치: ({x}, {y})</h1>
)} />

 

위 예제를 보면, MouseTracker 컴포넌트는 마우스 좌표를 추적하지만 UI는 props로 받은 함수에 따라 다르게 바뀔 수 있어요.

로직과 표현을 분리해서 유연하게 설계할 수 있다는 게 핵심이죠.

Render Props vs HOC 비교 🤜🤛

구분 HOC Render Props
패턴 방식 함수로 컴포넌트를 감쌈 함수를 props로 전달
장점 코드 분리, 재사용 용이 로직과 UI 완벽 분리
단점 Wrapper 중첩 가능성 JSX 중첩, 가독성 저하

Render Props는 정말 강력하지만, this.props.render 같은 패턴이 익숙하지 않거나 JSX 트리 깊이가 커질 수 있다는 단점도 있어요.

그래서 React 훅이 등장한 이후로는 점점 쓰임이 줄어드는 추세이기도 하죠.

💡 언제 Render Props를 쓸까?

상태나 데이터를 추적하면서 렌더링 방식은 외부에서 결정되게 하고 싶을 때 Render Props가 정말 빛나요. 마우스 위치, 스크롤, resize 등 사용자 이벤트 처리에 딱이죠!

 

 

6. 합성과 상속의 실전 활용법 🔍

이제까지 컴포넌트 합성상속, 그리고 HOC, Render Props까지 배워봤죠.

이번에는 실무에서 어떻게 써먹는지 실전 사례와 함께 정리해볼게요. 🔧

📦 재사용 가능한 레이아웃 컴포넌트 만들기

대부분의 페이지는 공통된 구조를 갖고 있어요.

예를 들면 Header + Sidebar + Content 조합이죠.

이럴 땐 명시적 합성으로 구성 요소들을 slot처럼 받아보세요.

// Layout.jsx
const Layout = ({ header, sidebar, content }) => (
  <div className="layout">
    <header>{header}</header>
    <aside>{sidebar}</aside>
    <main>{content}</main>
  </div>
);

// 사용 예시
<Layout
  header={<Header />}
  sidebar={<Sidebar />}
  content={<PageContent />}
/>

🔐 인증 처리 공통 로직 추상화

로그인 여부에 따라 페이지 접근을 제어하는 기능, 여러 페이지에서 반복되죠?

이럴 땐 HOC 또는 Render Props로 공통 로직을 추상화하면 좋아요.

// withAuth.js (HOC 예시)
const withAuth = (Component) => {
  return (props) => {
    const isLoggedIn = useAuth();
    if (!isLoggedIn) return <LoginPage />;
    return <Component {...props} />;
  };
};

// 적용
const MyPage = () => <p>내 정보 페이지</p>;
const ProtectedMyPage = withAuth(MyPage);

🧩 합성과 상속을 혼용하면 안 되는가?

꼭 그런 건 아니에요. 아주 특수한 상황에서는 상속을 활용한 클래스 기반 확장이 유리할 수도 있어요.

예를 들어 Canvas나 WebGL 기반 렌더링 라이브러리와 통합할 때 말이죠.

다만, 리액트의 세계에선 합성이 거의 표준입니다.

✨ 마무리 팁

  • 공통 UI → 합성으로 모듈화 (children 또는 slot 방식)
  • 인증, 로깅, 로딩처리 → HOC나 Render Props로 추상화
  • UI 표현 분리 → Render Props 사용 (단점도 고려!)

실무에서는 이 패턴들을 적절히 조합해서 사용하는 것이 핵심이에요.

항상 "이 컴포넌트는 변경될 가능성이 있는가?", "재사용될 수 있는가?"를 고민하며 설계해보세요. 🙌 

여기까지 리액트의 컴포넌트 설계 패턴을 하나씩 살펴봤어요.

Atomic Design으로 UI를 체계적으로 쪼개고, 합성과 상속의 차이를 이해하면서, HOCRender Props로 기능을 추상화하는 방법까지!

사실 이 모든 걸 처음에 한 번에 다 이해하기는 쉽지 않아요.

너무 조급해하지 말고, 지금부터 하나씩 실습해보면서 감을 잡아보세요! 💪

앞으로 컴포넌트 구조를 설계할 때 오늘 배운 내용이 진짜 든든한 무기가 될 거예요.

 

다음 글에서는 리액트 훅(Hooks)을 활용해서 상태와 로직을 더 효율적으로 관리하는 방법에 대해 이야기해볼게요. 기대되시죠? 😊

반응형

+ Recent posts