
React 애플리케이션이 커질수록 컴포넌트 내부에는 다양한 로직이 함께 존재하게 됩니다.
예를 들어 하나의 컴포넌트 안에는 다음과 같은 코드들이 함께 들어갈 수 있습니다.
- API 호출
- 상태 관리
- 데이터 가공
- 이벤트 처리
- UI 렌더링
처음에는 간단한 코드로 시작하지만, 프로젝트가 커질수록 하나의 컴포넌트 안에 너무 많은 책임이 생기게 됩니다.
이렇게 되면 다음과 같은 문제가 발생할 수 있습니다.
- 컴포넌트의 크기가 지나치게 커집니다.
- 로직 재사용이 어려워집니다.
- 테스트가 어려워집니다.
- 유지보수가 어려워집니다.
이러한 문제를 해결하기 위해 React에서는 로직을 구조화하는 다양한 패턴을 활용합니다.
대표적으로 다음과 같은 방법들이 있습니다.
- Custom Hook을 활용한 로직 / UI 분리
- useReducer + Context 조합 패턴
- Hook Composition 전략
- 같은 기능을 다양한 훅 구조로 구현해보기
이번 글에서는 위 패턴들을 하나씩 살펴보면서 React에서 로직을 어떻게 구조화하면 좋은지 알아보겠습니다.
1. Custom Hook을 활용한 로직 / UI 분리
React 컴포넌트는 기본적으로 UI와 로직이 함께 존재하는 구조입니다.
하지만 로직이 많아질수록 컴포넌트의 역할이 지나치게 커질 수 있습니다.
예를 들어 다음과 같은 컴포넌트가 있다고 가정해보겠습니다.
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
fetch("/api/users")
.then((res) => res.json())
.then((data) => {
setUsers(data);
setLoading(false);
});
}, []);
if (loading) return <p>loading...</p>;
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
이 컴포넌트는 다음 두 가지 역할을 동시에 수행하고 있습니다.
1. 데이터를 가져오는 로직
2. UI 렌더링
이처럼 로직과 UI가 하나의 컴포넌트에 섞여 있는 구조는 코드가 커질수록 관리하기 어려워집니다.
이때 사용할 수 있는 방법이 바로 Custom Hook입니다.
Custom Hook으로 로직 분리하기
먼저 데이터 로직을 커스텀 훅으로 분리합니다.
function useUsers() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
fetch("/api/users")
.then((res) => res.json())
.then((data) => {
setUsers(data);
setLoading(false);
});
}, []);
return { users, loading };
}
이제 컴포넌트에서는 UI만 담당하게 됩니다.
function UserList() {
const { users, loading } = useUsers();
if (loading) return <p>loading...</p>;
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
이렇게 하면 다음과 같은 장점이 있습니다.
관심사의 분리 (Separation of Concerns)
- Hook -> 로직 담당
- Component -> UI 담당
재사용성 증가
다른 컴포넌트에서도 같은 로직을 사용할 수 있습니다.
function UserCount() {
const { users } = useUsers();
return <p>총 사용자 수: {users.length}</p>;
}
테스트 용이성
UI와 로직이 분리되었기 때문에 로직만 따로 테스트하는 것도 가능해집니다.
2. useReducer + Context 조합 패턴
React에서는 여러 컴포넌트에서 상태를 공유해야 하는 경우가 많습니다.
예를 들어 다음과 같은 상태가 있을 수 있습니다.
- 로그인 사용자 정보
- 장바구니 상태
- 테마 상태
- 애플리케이션 설정
이러한 상태를 여러 컴포넌트에서 공유하기 위해 React에서는 Context API를 제공합니다.
하지만 상태 로직이 복잡해지면 useState만으로 관리하기 어려워질 수 있습니다.
이때 사용할 수 있는 패턴이 바로 useReducer + Context 조합 패턴입니다.
reducer 정의
먼저 상태 변경 로직을 reducer로 분리합니다.
function counterReducer(state, action) {
switch (action.type) {
case "INCREMENT":
return { count: state.count + 1 };
case "DECREMENT":
return { count: state.count - 1 };
default:
return state;
}
}
Context 생성
const CounterContext = createContext(null);
Provider 구현
function CounterProvider({ children }) {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
<CounterContext.Provider value={{ state, dispatch }}>
{children}
</CounterContext.Provider>
);
}
Context 사용
function Counter() {
const { state, dispatch } = useContext(CounterContext);
return (
<div>
<p>{state.count}</p>
<button onClick={() => dispatch({ type: "INCREMENT" })}>
+
</button>
<button onClick={() => dispatch({ type: "DECREMENT" })}>
-
</button>
</div>
);
}
이 패턴의 핵심은 상태 변경 로직을 reducer로 중앙 관리하는 것입니다.
useReducer + Context 패턴의 장점
상태 변경이 명확해집니다
모든 상태 변경은 action을 통해 이루어집니다.
dispatch({ type: "INCREMENT" })
상태 로직이 한 곳에 모입니다
복잡한 상태 로직을 reducer에서 관리할 수 있습니다.
Redux와 유사한 구조
React의 기본 기능만으로도 Redux와 유사한 상태 관리 구조를 만들 수 있습니다.
3. Hook Composition 전략
React Hook은 함수이기 때문에 다른 Hook을 내부에서 사용할 수 있습니다.
이러한 구조를 Hook Composition이라고 합니다.
즉, 작은 Hook들을 조합해서 더 큰 Hook을 만드는 방식입니다.
예시
먼저 인증 정보를 관리하는 Hook이 있다고 가정해보겠습니다.
function useAuth() {
const [user, setUser] = useState(null);
return { user, setUser };
}
이 Hook을 기반으로 사용자 프로필 Hook을 만들 수 있습니다.
function useUserProfile() {
const { user } = useAuth();
const [profile, setProfile] = useState(null);
useEffect(() => {
if (!user) return;
fetch(`/api/profile/${user.id}`)
.then((res) => res.json())
.then(setProfile);
}, [user]);
return profile;
}
여기서 중요한 점은 다음과 같습니다.
- 작은 Hook → 재사용 가능
- 큰 Hook → 여러 Hook을 조합
Hook Composition의 장점
로직을 작은 단위로 나눌 수 있습니다
예를 들어 다음과 같이 분리할 수 있습니다.
useAuth
useUser
useProfile
재사용성이 높아집니다
각 Hook은 독립적으로 사용할 수 있습니다.
복잡한 로직을 단계적으로 구성할 수 있습니다
작은 기능들을 조합해서 큰 기능을 만들 수 있습니다.
4. 같은 기능을 다양한 훅 구조로 구현해보기
React를 학습할 때 매우 좋은 방법 중 하나는 같은 기능을 여러 방식으로 구현해보는 것입니다.
예를 들어 "상품 목록 검색 기능"을 구현한다고 가정해보겠습니다.
방법 1 - 단순 useState
const [products, setProducts] = useState([]);
const [keyword, setKeyword] = useState("");
방법 2 - Custom Hook
const { products } = useProducts();
방법 3 - useReducer
const [state, dispatch] = useReducer(productReducer);
방법 4 - Hook Composition
const { products } = useProducts();
const { keyword, setKeyword } = useSearch();
이렇게 여러 방식으로 구현해보면 다음과 같은 점을 이해할 수 있습니다.
- 어떤 구조가 가장 읽기 쉬운지
- 어떤 구조가 확장성이 좋은지
- 어떤 구조가 재사용성이 높은지
마무리
React에서 좋은 컴포넌트 구조를 설계하기 위해서는 로직을 어떻게 분리할 것인지가 매우 중요합니다.
대표적인 방법은 다음과 같습니다.
패턴목적
| Custom Hook | 로직 재사용 |
| useReducer + Context | 상태 중앙 관리 |
| Hook Composition | 작은 Hook 조합 |
| 다양한 구현 방식 실험 | 설계 능력 향상 |
결국 핵심은 다음과 같습니다.
컴포넌트는 가능한 한 UI에 집중하도록 만들고,
비즈니스 로직은 Hook으로 분리하는 것입니다.
이러한 구조를 잘 활용하면
- 코드 재사용성이 높아지고
- 유지보수가 쉬워지며
- 확장 가능한 React 애플리케이션을 만들 수 있습니다.
감사합니다!
'React' 카테고리의 다른 글
| TanStack Query 에 대해서 알아보기! (0) | 2026.04.04 |
|---|---|
| 상태 관리 전략 비교 - Context API, Zustand, Redux Toolkit 중 무엇을 선택해야 할까? (0) | 2026.03.23 |
| React useState 완전 이해하기 (0) | 2026.03.21 |
| React Hook 완전 이해하기 (0) | 2026.03.21 |
| React에서 컴포넌트를 어떻게 설계할 것인가? (0) | 2026.03.09 |
