[React] 디자인패턴 Custom hook Pattern-UI와 비즈니스로직의 분리에 대하여
안녕하세요 :)
앞으로 진행될 프로젝트들의 기반이 될 템플릿을 만드는 작업을 하며,
어떤 디자인패턴을 사용하는게 가장 우리 팀에게 맞을지 고민하는 시간이 일주일 정도 있었습니다.
가장 유명한 디자인 패턴 중 제가 템플릿을 만들며 적용시킨 Atomic + Custom hook 패턴에 대해 소개하겠습니다.
Component & Custom hook Pattern
1. 모든 함수와 state를 custom hook으로만들어 return시킵니다.
2. UI 컴포넌트내에서 해당 custom hook을 호출해 state와 함수를 씁니다.
이 패턴이 제게 가장 매력적이었던 이유는, 로직을 담당하는 컴포넌트를 만들지 않기 때문에 Props drilling 없이 UI를 분리시킬 수 있다는 것이었고, 해당 Custom hook을 다른 컴포넌트에서도 불러와 사용할 수 있기 때문에 중복코드를 줄일 수 있다는 점이었습니다.
예시코드 갈겨
function useTitle() {
const [title, setTitle] = useState("some title");
const data = fetchSomething();
const onClose = () => {
// onClose 기능
};
// ...
return { title, setTitle, data, onClose };
}
const TotalComponent=()=> {
const { title, setTitle, data, onClose } = useTitle();
return (
<div>
<div>** This component only awares about UI</div>
<div>{titleState}</div>
<div>{data.description}</div>
<button onClick={onClose}>Close</button>
{* ... *}
</div>
)
}
그리고 가장 중요한 폴더 구조입니다.
│
├── src/
│ ├── components/ # Atomic Design에 기반한 UI 컴포넌트
│ │ ├── atoms/ # 가장 작은 UI 단위 (버튼, 입력 필드 등)
│ │ │ ├── Button/
│ │ │ ├── Input/
│ │ │ └── ...
│ │ ├── molecules/ # Atoms의 조합 (폼 필드, 검색 바 등)
│ │ │ ├── SearchBar/
│ │ │ └── ...
│ │ ├── organisms/ # Molecules의 조합 (헤더, 사이드바 등)
│ │ │ ├── Header/
│ │ │ └── ...
│ │ └── templates/ # 페이지 레이아웃을 정의
│ │ └── ...
│ │
│ ├── hooks/ # 비즈니스 로직과 상태 관리를 위한 커스텀 훅
│ │ ├── useFetchData.js
│ │ ├── useForm.js
│ │ └── ...
│ │
│ ├── pages/ # 각 페이지를 구성하는 컴포넌트
│ │ ├── HomePage.jsx
│ │ ├── AboutPage.jsx
│ │ └── ...
│ │
│ └── ...
│
├── package.json
└── ...
처음 이 패턴을 적용시키며 '아니 그럼 UI 컴포넌트에서는 state를 아예 두면 안되는건가?' 하는 생각에 조금 부담스러웠는데요,
UI 컴포넌트 내에서 state를 사용하는 아래의 대표적인 경우에서는 state를 적절하게 UI 컴포넌트에 둘 수 있습니다.
- 로컬 UI 상태: UI 요소의 상태(예: 드롭다운 메뉴의 열림/닫힘 상태, 입력 필드의 값 등)를 관리하기 위해 컴포넌트 내에서 state를 사용합니다. 이러한 상태는 종종 해당 컴포넌트에만 국한되므로, 컴포넌트 내부에서 직접 관리하는 것이 적절합니다.
- 폼 입력 및 검증: 사용자 입력과 관련된 상태(예: 폼 필드 값, 검증 오류 메시지 등)를 관리하기 위해 컴포넌트 내에서 state를 사용합니다. 이 경우, 사용자의 입력에 반응하여 UI를 업데이트하거나 검증 로직을 실행하는 것이 필요합니다.
- 컴포넌트의 상태 관리: 컴포넌트의 특정 동작이나 상태(예: 로딩 인디케이터의 표시, 토글 상태 등)를 관리하기 위해 state를 사용합니다.
- 커스텀 훅과의 상호작용: 때로는 커스텀 훅이 컴포넌트로부터 특정 상태를 받아야 할 필요가 있습니다. 이 경우, 컴포넌트 내에서 state를 정의하고, 이를 커스텀 훅에 인자로 전달할 수 있습니다.
커스텀 훅은 주로 데이터 페칭, 사이드 이펙트 관리, 비즈니스 로직 등 컴포넌트 간에 공유될 수 있는 로직을 캡슐화하는 데 사용됩니다. 반면, 각각의 UI 컴포넌트는 자신만의 독립적인 UI 상태를 가질 수 있으며, 이를 통해 특정 UI의 동작을 정밀하게 제어할 수 있습니다.
그럼 useEffect는 어떻게 쓸 수 있을까요?
아래 예시에서는 커스텀 훅이 외부에서 전달받은 state를 의존성 배열에 포함합니다. 이 state가 변경될 때마다 훅 내부의 useEffect가 실행되어, API 호출을 다시 수행합니다.
import { useState, useEffect } from 'react';
function useDataWithDependency(url, dependency) {
// url과 dependency를 parmeter로 받아요
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchData() {
try {
setLoading(true);
const response = await fetch(`${url}?dependency=${dependency}`);
const result = await response.json();
setData(result);
setLoading(false);
} catch (e) {
setError(e);
setLoading(false);
}
}
fetchData();
}, [url, dependency]); // 의존성 배열에 URL과 외부 의존성 포함
return { data, loading, error };
}
export default useDataWithDependency;
이제 useDataWithDependency훅을 사용하는 컴포넌트를 만들어 보겠습니다. 이 컴포넌트는 외부(하위 컴포넌트)에서 변경될 수 있는 state를 가지고 있으며, 이 state는 커스텀 훅의 의존성으로 사용됩니다.
import React, { useState } from 'react';
import useDataWithDependency from './hooks/useDataWithDependency';
function DataDisplayComponent() {
const [dependency, setDependency] = useState('initialValue');
const { data, loading, error } = useDataWithDependency('https://api.example.com/data', dependency);
// 예를 들어, 버튼 클릭으로 dependency를 변경하는 함수
const handleDependencyChange = () => {
setDependency('newValue');
};
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>Data</h1>
<button onClick={handleDependencyChange}>Change Dependency</button>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
}
export default DataDisplayComponent;
이 패턴을 사용하는 가장 큰 목적 : UI와 비즈니스로직의 분리
다들 UI와 비즈니스로직 분리를 고려하며 개발하고계신가요?
💡 UI와 로직 분리의 필요성
