우리는 지금까지 React의 핵심 개념들을 배우고 다양한 기능들을 활용하는 방법을 익혔습니다.
이제는 단순히 '동작하는' 코드를 넘어, '잘 만들어진' 코드를 작성하는 방법에 대해 고민해 볼 시간입니다.
특히 React 개발의 핵심 단위인
컴포넌트
를 어떻게 설계하느냐는 전체 애플리케이션의 품질, 유지보수성, 확장성에 큰 영향을 미칩니다.
잘 설계된 컴포넌트는 마치 잘 만들어진 레고 블록처럼, 쉽게 이해하고 테스트할 수 있으며 다른 곳에서도 가져다 쓰기(재사용) 편리합니다.
반면, 잘못 설계된 컴포넌트는 코드를 읽기 어렵게 만들고, 작은 수정이 예상치 못한 문제를 일으키며, 협업을 힘들게 만듭니다.
오늘은 좋은 React 컴포넌트를 만들기 위한 몇 가지 중요한 설계
원칙
과 널리 사용되는
패턴
들을 함께 살펴보겠습니다.
🚀
단일 책임 원칙 (Single Responsibility Principle, SRP) 🔗
소프트웨어 설계의 기본 원칙 중 하나인
단일 책임 원칙(SRP)
은 React 컴포넌트 설계에도 매우 중요하게 적용됩니다.
SRP는 간단히 말해,
하나의 컴포넌트는 단 한 가지의 책임(역할)만 가져야 한다
는 원칙입니다.
컴포넌트의 책임은 다양할 수 있습니다. 예를 들어,
특정 UI 조각을 렌더링하는 책임.
데이터를 가져오는(fetching) 책임.
사용자의 입력을 처리하는 책임.
전역 상태를 구독하고 업데이트하는 책임.
특정 비즈니스 로직을 수행하는 책임.
SRP는 하나의 컴포넌트가 이러한 여러 책임을 동시에 떠안지 않도록 권장합니다.
만약 컴포넌트가 너무 많은 일을 하고 있다면, 그 컴포넌트는 변경될 이유가 너무 많아지고, 코드가 복잡해지며, 테스트하기 어려워집니다.
SRP를 적용하는 핵심은
관심사의 분리(Separation of Concerns)
입니다.
서로 다른 책임(관심사)을 가진 코드들을 별도의 컴포넌트나 훅(Hook)으로 분리하는 것입니다.
로직 분리
데이터 fetching, 상태 관리 로직, 복잡한 계산 등은 커스텀 훅(Custom Hook)이나 별도의 유틸리티 함수로 분리합니다.
UI 분리
하나의 거대한 UI 컴포넌트를 더 작은 단위의 역할(예: 버튼, 입력 필드, 카드, 리스트 아이템)을 가진 컴포넌트들로 나눕니다.
컨테이너/프레젠테이셔널 분리
데이터 처리 로직과 순수 UI 렌더링 로직을 분리합니다. (뒤에서 자세히 다룰)
SRP 위반 예시 (하나의 컴포넌트가 너무 많은 일을 함) function UserProfileWithPosts ({ userId } : { userId : string }) {
const [ user , setUser ] = React. useState ( null );
const [ posts , setPosts ] = React. useState ([]);
const [ loading , setLoading ] = React. useState ( false );
React. useEffect (() => {
// 데이터 fetching 책임
setLoading ( true );
Promise . all ([ fetchUser (userId), fetchPosts (userId)])
. then (([ userData , postData ]) => {
setUser (userData);
setPosts (postData);
})
. finally (() => setLoading ( false ));
}, [userId]);
// UI 렌더링 책임 (사용자 정보 + 게시글 목록)
if (loading) return < div >로딩 중...</ div >;
if ( ! user) return < div >사용자 정보 없음</ div >;
return (
< div >
{ /* 사용자 정보 렌더링 부분 */ }
< h2 > { user.name } </ h2 >
< p > { user.email } </ p >
< hr />
{ /* 게시글 목록 렌더링 부분 */ }
< h3 >게시글</ h3 >
< ul >
{ posts. map ( post => < li key ={ post.id } > { post.title } </ li >) }
</ ul >
</ div >
);
}
SRP 적용 예시 (로직과 UI 분리) // 데이터 fetching 로직 분리 (커스텀 훅)
function useUserData ( userId : string ) { /* ... user fetching 로직 ... */ return { user, loadingUser }; }
function usePostData ( userId : string ) { /* ... posts fetching 로직 ... */ return { posts, loadingPosts }; }
// UI 컴포넌트 분리
function UserInfo ({ user } : { user : any }) { /* ... 사용자 정보 UI ... */ }
function PostList ({ posts } : { posts : any [] }) { /* ... 게시글 목록 UI ... */ }
// 각 책임을 조합하는 상위 컴포넌트
function UserProfilePage ({ userId } : { userId : string }) {
const { user , loadingUser } = useUserData (userId);
const { posts , loadingPosts } = usePostData (userId);
if (loadingUser || loadingPosts) return < div >로딩 중...</ div >;
if ( ! user) return < div >사용자 정보 없음</ div >;
return (
< div >
< UserInfo user ={ user } />
< hr />
< PostList posts ={ posts } />
</ div >
);
}
SRP 적용 전후
테스트 용이성
: 각 컴포넌트/훅이 명확한 책임만 가지므로 독립적으로 테스트하기 쉬워집니다.
재사용성 향상
: 특정 기능(UI 조각, 로직)이 필요할 때 해당 컴포넌트나 훅만 가져다 사용할 수 있습니다.
이해도 및 유지보수성 증가
: 코드가 간결해지고 역할이 명확해져 이해하기 쉽고 수정하기 용이합니다. 변경의 영향 범위를 예측하기 쉬워집니다.
Q: "단일 책임 원칙(SRP)이란 무엇이고 React 컴포넌트에 어떻게 적용할 수 있나요?"
A: "하나의 컴포넌트는 하나의 책임만 가져야 한다는 원칙입니다. 로직은 커스텀 훅으로, UI는 더 작은 컴포넌트로 분리하여 적용할 수 있으며, 이는 테스트 용이성과 재사용성을 높입니다."
🚀
Atomic Design 패턴: 체계적인 UI 구성 🔗
Atomic Design은 UI 컴포넌트를 마치 화학의 원자, 분자, 유기체처럼 체계적인 단계로 나누어 설계하는 방법론입니다.
디자인 시스템 구축과 UI 일관성 유지에 매우 유용합니다.
Atomic Design은 UI를 다음 5단계의 계층 구조로 나눕니다.
Atoms (원자)
UI를 구성하는 가장 작은 기본 단위입니다. 더 이상 분해될 수 없는 HTML 요소나 기본적인 스타일링 단위입니다. (예: 버튼, 입력 필드, 레이블, 아이콘)
Molecules (분자)
여러 개의 원자가 모여 특정 기능을 수행하는 단위입니다. 원자들을 조합하여 조금 더 복잡한 UI 요소를 만듭니다. (예: 검색 폼(입력 필드 + 버튼), 네비게이션 링크(아이콘 + 레이블))
Organisms (유기체)
여러 개의 분자나 원자가 모여 더 복잡하고 독립적인 UI 섹션을 구성합니다. 페이지의 특정 영역을 나타내는 경우가 많습니다. (예: 헤더(로고 + 네비게이션 + 검색 폼), 상품 카드, 게시글 목록)
Templates (템플릿)
페이지의 전체적인 레이아웃 구조를 정의합니다. 실제 콘텐츠는 없고, 어떤 유기체와 분자들이 배치될지 보여주는 뼈대 역할을 합니다. (예: 블로그 포스트 레이아웃, 상품 목록 페이지 레이아웃)
Pages (페이지)
템플릿에 실제 콘텐츠(데이터)를 넣어 완성된 최종 페이지 모습입니다. 사용자가 실제로 보게 되는 화면입니다. (예: 특정 블로그 포스트 페이지, 특정 카테고리의 상품 목록 페이지)
Atomic Design의 5단계
✅
Atomic Design의 장점과 고려사항 🔗
재사용성 극대화
: 작은 단위(원자, 분자)부터 재사용 가능하게 만들어져 UI 개발 효율성이 높아집니다.
일관성 유지
: 디자인 시스템을 기반으로 컴포넌트를 만들기 때문에 전체 애플리케이션의 UI 일관성을 유지하기 쉽습니다.
개발 및 디자인 협업 용이
: 디자이너와 개발자가 동일한 용어와 구조를 공유하여 소통이 원활해집니다.
테스트 용이성
: 작은 단위 컴포넌트별로 테스트하기 편리합니다.
과도한 분리
: 너무 작은 단위까지 엄격하게 나누려고 하면 오히려 개발 복잡성이 증가할 수 있습니다. 프로젝트의 규모와 팀 상황에 맞게 유연하게 적용하는 것이 중요합니다.
초기 설정 비용
: 디자인 시스템과 함께 구축하는 경우가 많아 초기 설정에 시간이 소요될 수 있습니다.
🚀
Container / Presentational 패턴: 로직과 뷰의 분리 🔗
Container/Presentational 패턴은 컴포넌트를 두 가지 종류로 나누어 관심사를 분리하는 고전적인 디자인 패턴입니다.
데이터 fetching, 상태 관리(Redux 연결 등), 이벤트 처리 로직 등을 담당합니다.
내부에 Presentational 컴포넌트를 포함하며, 필요한 데이터와 콜백 함수를 props로 전달합니다.
보통 자체적인 DOM 마크업이나 스타일은 거의 가지지 않습니다.
데이터를 props로 받아서 화면에 어떻게 표시할지를 담당합니다.
스타일, UI 구조에만 신경 씁니다.
애플리케이션의 다른 부분(상태, API 등)에 직접 의존하지 않습니다.
자체적인 상태를 거의 가지지 않으며, 데이터 변경이 필요하면 props로 받은 콜백 함수를 호출합니다.
Presentational 패턴 예시 import React from 'react' ;
// --- Presentational Component ---
interface TodoListProps {
todos : { id : number ; text : string ; completed : boolean }[];
onToggleTodo : ( id : number ) => void ;
}
// 오직 props를 받아 UI를 그리는 역할만 수행
function TodoListUI ({ todos , onToggleTodo } : TodoListProps ) {
return (
< ul >
{ todos. map ( todo => (
< li
key ={ todo.id }
onClick ={ () => onToggleTodo (todo.id) }
style ={ { textDecoration: todo.completed ? 'line-through' : 'none' , cursor: 'pointer' } }
>
{ todo.text }
</ li >
)) }
</ ul >
);
}
Container 패턴 예시 // --- Container Component ---
import { useTodos } from './hooks/useTodos' ; // 가상의 커스텀 훅 (데이터 로직 분리)
function TodoListContainer () {
// 데이터 fetching 및 상태 관리 로직은 커스텀 훅 또는 여기서 직접 처리
const { todos , toggleTodo } = useTodos (); // 가상의 훅 사용 예시
// Presentational 컴포넌트에 필요한 데이터와 콜백 전달
return < TodoListUI todos ={ todos } onToggleTodo ={ toggleTodo } />;
}
가상의 커스텀 훅 예시 // --- 가상의 커스텀 훅 예시 ---
// hooks/useTodos.ts (가상 예시)
function useTodos () {
const [ todos , setTodos ] = React. useState ([]);
// ... 데이터 fetching 로직 ...
const toggleTodo = ( id : number ) => { /* ... 상태 업데이트 로직 ... */ };
return { todos, toggleTodo };
}
Container 컴포넌트와 Presentational 컴포넌트의 관계
✅
Hooks 시대의 변화와 여전히 유효한 철학 🔗
React Hooks(특히 커스텀 훅)가 등장하면서, Container 컴포넌트가 담당하던 로직 부분을 커스텀 훅으로 분리하는 것이 더 일반적이 되었습니다.
이로 인해 명시적인 Container 컴포넌트의 필요성이 줄어들기도 했습니다.
하지만 이 패턴의 핵심 철학인
관심사의 분리
(로직과 뷰의 분리)는 여전히 매우 중요합니다.
Presentational 컴포넌트처럼 순수하게 UI 렌더링에만 집중하는 컴포넌트를 만드는 것은 여전히 코드의 재사용성과 테스트 용이성을 높이는 좋은 방법입니다.
Q: "Container/Presentational 패턴이란 무엇인가요? Hooks 등장 이후 이 패턴은 어떻게 변화했나요?"
A: "로직(Container)과 뷰(Presentational)를 분리하는 패턴입니다. Hooks 등장 후 로직은 커스텀 훅으로 분리하는 경향이 있지만, 관심사 분리라는 핵심 철학은 여전히 유효하며, 순수한 UI 컴포넌트 작성은 재사용성과 테스트 용이성에 도움이 됩니다."
위에서 소개한 원칙과 패턴 외에도 컴포넌트의 품질을 높이기 위한 몇 가지 실용적인 팁이 있습니다.
의미 있는 이름
Props 이름만 보고도 어떤 역할을 하는지 명확히 알 수 있도록 짓습니다. (예: isEnabled
vs flag
)
필수/선택 구분
TypeScript 등을 사용하여 어떤 props가 필수이고 선택적인지 명확히 정의합니다. 선택적 prop에는 기본값을 제공하는 것을 고려합니다.
인터페이스/타입 사용
TypeScript를 사용한다면, props의 타입을 명시하는 인터페이스나 타입을 정의하여 컴포넌트의 사용법을 명확히 하고 타입 안정성을 높입니다.
✅
컴포넌트 합성 (Composition) 활용 🔗
children prop
컴포넌트 내부에 다른 내용을 유연하게 삽입하고 싶을 때 children
prop을 적극적으로 활용합니다.
이는 컴포넌트의 재사용성을 크게 높여줍니다. (예: 카드 레이아웃 컴포넌트 안에 다양한 내용 넣기)
function Card ({ children , title } : { children : React . ReactNode ; title : string }) {
return < div className = "card" >< h2 > { title } </ h2 > { children } </ div >;
}
// <Card title="제목"><p>내용</p></Card>
Render Props 패턴
특정 UI를 렌더링하는 로직 자체를 prop으로 전달하는 패턴입니다. 매우 유연한 구조를 만들 수 있지만, 코드가 다소 복잡해질 수 있습니다. (최근에는 Hooks로 대체되는 경향)
외부 의존성 줄이기
컴포넌트가 전역 상태나 외부 모듈에 직접 의존하기보다는, 필요한 데이터나 함수를 props로 명시적으로 전달받는 것이 좋습니다.
이는 컴포넌트의 예측 가능성과 재사용성을 높입니다.
순수 컴포넌트처럼 만들기
가능하면 컴포넌트가 동일한 props에 대해 항상 동일한 UI를 렌더링하도록 만듭니다(부수 효과 최소화).
이런 컴포넌트는 이해하기 쉽고 테스트하기 매우 편리합니다. React.memo와도 잘 어울립니다.
오늘은 재사용 가능하고, 테스트하기 쉬우며, 유지보수하기 좋은 React 컴포넌트를 설계하기 위한 몇 가지 중요한 원칙과 패턴을 살펴보았습니다.
단일 책임 원칙(SRP)
: 컴포넌트가 한 가지 역할에만 집중하도록 하여 코드의 명확성과 테스트 용이성을 높입니다.
Atomic Design
: UI를 체계적인 단계(원자, 분자 등)로 나누어 일관성과 재사용성을 높이는 방법론입니다.
Container/Presentational 패턴
: 로직과 뷰를 분리하는 고전적인 패턴으로, 관심사 분리라는 핵심 철학은 여전히 유효합니다 (Hooks로 로직 분리).
명확한 Props 설계, 컴포넌트 합성, 의존성 최소화
등은 컴포넌트 품질을 높이는 실용적인 방법들입니다.
프로젝트의 규모, 팀의 경험, 요구사항 등을 고려하여 적절한 방법을 선택하고 유연하게 적용하는 것이 중요합니다.
또한, 처음부터 완벽한 설계를 하려고 하기보다는, 코드를 작성하고 리팩토링하는 과정을 통해 점진적으로 개선해 나가는 것이 현실적인 접근 방식일 수 있습니다.
다음 시간에는 마이크로서비스 아키텍처의 프론트엔드 버전인 *마이크로 프론트엔드(Micro Frontend)*에 대해 알아보겠습니다.
거대한 프론트엔드 애플리케이션을 여러 팀이 독립적으로 개발하고 배포하는 방법에 대해 살펴보겠습니다.