-
[React] 디자인패턴 Custom hook Pattern-UI와 비즈니스로직의 분리에 대하여Java Script & Type Script 2024. 1. 20. 01:12728x90
안녕하세요 :)
앞으로 진행될 프로젝트들의 기반이 될 템플릿을 만드는 작업을 하며,
어떤 디자인패턴을 사용하는게 가장 우리 팀에게 맞을지 고민하는 시간이 일주일 정도 있었습니다.
가장 유명한 디자인 패턴 중 제가 템플릿을 만들며 적용시킨 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와 비즈니스로직 분리를 고려하며 개발하고계신가요?
저는 기존에 한 컴포넌트 내에서 모두 처리할 수 있던 형태에서 비즈니스로직을 처리하는 Custom hook과 UI를 담당하는 컴포넌트를 분리하는 2차 작업으로인한 시간과 리소스의 낭비를 이점이 능가하는가에 대한 불신으로 굳이 그 작업하지 않고 있었는데요,UI와 로직의 분리를 전혀 고려하지 않고 작업된 프로젝트를 유지, 보수하는 고된 경험에서 그 필요성에 대해 처음으로 느끼고 다시 한번 생각해보게 되었습니다.💡 UI와 로직 분리의 필요성
1. 장기적으로 프로젝트를 관리하는 관점에서 코드의 재사용성, 유지 관리 용이성, 그리고 전체적인 프로젝트의 확장성을 증가시키는데 도움이 됩니다. 무엇보다 관심사 분리(Separation of Concerns, SoC)가 가장 중요한 이유입니다.관심사의 분리를 통해 UI 컴포넌트는 시각적 요소와 상호작용에 집중할 수 있으며, 커스텀 훅은 데이터 처리나 상태 관리에 집중할 수 있습니다.2. 가독성을 높입니다. 컴포넌트 내부의 로직이 복잡해지면 컴포넌트의 가독성과 유지 관리가 어려워질 수 있습니다. 커스텀 훅을 사용하면 이러한 복잡성을 외부로 분리하여 컴포넌트를 훨~~씬 더 깔끔하고 관리하기 쉽게 만듭니다.3. 테스트를 용이하게 합니다. Custom hook은 독립적인 단위로서 테스트해보기가 더 쉽습니다. UI와 로직이 분리되어있기 때문에 로직에 대한 단위테스트를 수행하기 간편해집니다.지난 게시물에서 Storybook을 도입하는 과정에 대해 작성했는데요,Component-Customhook pattern의 프로젝트는 순수한 UI 컴포넌트의 렌더링과 상호작용을 더 명확하게 테스트할 수 있기 때문에 Storybook과 같은 컴포넌트 기반의 개발 도구를 사용할 때 매우 유용합니다. (채정 뇌피셜)다들 당장 컴포넌트 격리시키시고,박과박과 Custom hook 패턴으로 다 박과728x90'Java Script & Type Script' 카테고리의 다른 글
[리액트] 카드 정보 입력 UI & 카드번호, 카드유효기간, 생년월일 유효성검사 (0) 2024.06.26 [Next.js] App Router의 신기능, Private Folder & Route Group (Next.js 폴더구조 잡기), File (0) 2024.01.28 [React] Tanstack react-query 데이터 리패칭의 네가지 방법 (1) 2023.12.28 [클린코드] 추상화, 관심사의 분리,의존성주입 (Java script version) (0) 2023.07.27 [자료구조] Map과 Set (0) 2023.07.10