커스텀 훅 패턴(Custom Hook Pattern)으로 React 개발 최적화하기

ClydeHan·2024년 9월 25일
12

React Custom Hook Image

이미지 출처: www.toptal.com

커스텀 훅 패턴(Custom Hook Pattern)

커스텀 훅 패턴(Custom Hook Pattern)은 React에서 자주 사용하는 패턴 중 하나로, 재사용 가능한 로직을 하나의 함수로 캡슐화하는 방법이다. 기존의 React Hooks를 사용하여 상태 관리나 사이드 이펙트 처리를 컴포넌트 내에서 수행할 수 있지만, 여러 컴포넌트에서 비슷한 로직이 반복되면 코드의 중복이 발생할 수 있다. 이러한 문제를 해결하기 위해 커스텀 훅을 사용하여 공통 로직을 분리하고 재사용할 수 있게 된다.

쉽게 말해, 커스텀 훅 패턴은 상태 관리 및 사이드 이펙트를 다루는 로직을 분리해 재사용하는 패턴이다.


📌 커스텀 훅(Custom Hook), 그리고 기본 구조

커스텀 훅은 React의 상태 관리와 사이드 이펙트를 처리하는 재사용 가능한 함수다. 주로 상태와 부수 효과(side effect)를 다루기 위해 만들어지며, 다른 컴포넌트들에서 공통으로 사용되는 로직을 하나의 훅으로 분리해서 재사용할 수 있다.

커스텀 훅은 기본적으로 use라는 접두어를 사용한다. 이 함수는 하나 이상의 React 내장 훅(useState, useEffect 등)을 내부에서 호출하고, 이를 통해 상태 관리나 부수 효과를 처리한다. 예를 들어, 데이터를 가져오는 로직을 여러 컴포넌트에서 사용해야 할 때, 이 로직을 커스텀 훅으로 분리하면 여러 컴포넌트에서 쉽게 재사용할 수 있다.

// Example: useFetch라는 커스텀 훅을 생성
import { useState, useEffect } from 'react';

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(url);
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err);
      } finally {
        setLoading(false);
      }
    };
    fetchData();
  }, [url]);

  return { data, loading, error };
}

이 커스텀 훅을 사용하면 데이터를 가져오는 로직을 컴포넌트 안에 직접 작성하지 않고, useFetch를 호출하여 데이터를 가져오는 기능을 사용할 수 있다.

// 커스텀 훅 사용 예시
function MyComponent() {
  const { data, loading, error } = useFetch('https://api.example.com/data');

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return <div>{data}</div>;
}

이것이 커스텀 훅의 기본 구조이다. 어렵지 않다.


📌 자주 혼용되는 용어

Hooks, Hooks Pattern, Custom Hook, Custom Hook Pattern이라는 용어들이 자주 혼용되는데, 사실상 모두 동일한 개념을 의미한다. 이 용어들은 React의 훅을 사용하여 로직을 재사용 가능하게 만드는 패턴을 가리키며, 그 중에서도 Custom Hook은 React의 내장 훅을 활용해 상태 관리와 사이드 이펙트 같은 로직을 분리하고 재사용하는 함수다.

  • Hooks: React에서 내장 훅(useState, useEffect, useReducer 등)을 활용하는 일반적인 방식.
  • Hooks Pattern: React 훅을 사용하여 로직을 컴포넌트에서 분리하고 재사용하는 패턴을 의미.
  • Custom Hook: React 훅을 조합하여 상태 관리와 사이드 이펙트를 분리하고, 여러 컴포넌트에서 재사용할 수 있도록 만드는 함수.
  • Custom Hook Pattern: Custom Hook을 사용해 공통 로직을 추출하고 재사용하는 구조적 패턴.

따라서, 이 용어들은 커스텀 훅을 사용하여 상태 관리와 로직을 컴포넌트로부터 분리하는 동일한 패턴을 지칭하며, 각각 상황에 따라 조금씩 다르게 표현될 수 있다.

이번 설명에서는 Custom Hook을 활용하여 상태 관리와 사이드 이펙트 로직을 재사용하는 방식에 대해 논의할 것이며, 이를 Custom Hook Pattern이라는 용어로 지칭하겠다.


📌 유틸리티 함수(Utility Function), 비즈니스 로직(Business logic)

커스텀 훅 패턴에 대한 깊은 이해를 위해서는 유틸리티 함수와 비즈니스 로직에 대해서 먼저 이해하고 있는 것이 좋다.

💡 유틸리티 함수(Utility Function)

유틸리티 함수는 상태와 무관하게 입력값을 받아 특정 결과를 반환하는 순수 함수다. 이는 React와 상관없이 일반적으로 로직을 분리해서 사용하는 함수들이다. 즉, 단순하게 자바스크립트 함수로 구현되어 React와 무관하게 독립적으로 동작한다. 예를 들어, 문자열 포맷팅, 배열 변환, 조건에 따른 값 반환 등 상태나 부수 효과와 관계없는 로직을 처리하는 데 사용된다.

// 유틸리티 함수: 간단한 값을 계산하는 순수 함수
function getFormattedDate(date) {
  return new Date(date).toLocaleDateString();
}

💡 비즈니스 로직(Business logic)

비즈니스 로직은 애플리케이션의 핵심 기능을 수행하는 로직이다. 비즈니스 로직은 주로 사용자의 행동에 따른 데이터 처리, 상태 관리, 그리고 각종 도메인 규칙을 적용하는 역할을 한다. 즉, 사용자가 웹 애플리케이션에서 특정 작업을 수행할 때 그 작업이 어떤 방식으로 처리되고, 데이터가 어떻게 변화하는지에 대한 규칙을 정의하는 로직이다.

예를 들어, 사용자가 상품을 구매할 때 가격 계산, 할인 적용, 재고 관리와 같은 로직이 비즈니스 로직에 해당된다. 이 로직은 애플리케이션의 주요 기능을 구현하며, UI와는 직접적으로 연결되지 않는 경우가 많다.


📌 커스텀 훅 패턴에서 비즈니스 로직의 분리

커스텀 훅 패턴은 상태 관리사이드 이펙트를 처리하는 비즈니스 로직을 분리하여 여러 컴포넌트에서 재사용할 수 있도록 돕는다. 하지만 모든 비즈니스 로직을 커스텀 훅으로 처리하는 것은 아니다. 특히, 상태와 무관한 유틸리티 함수는 커스텀 훅으로 분리되지 않는다.

중요한 점은, 유틸리티 함수도 비즈니스 로직의 일부일 수 있다. 하지만 커스텀 훅은 주로 React의 상태 관리 및 사이드 이펙트 처리와 관련된 로직을 분리하는 데 사용되며, 상태와 관계없는 로직은 유틸리티 함수로 따로 분리하여 관리하는 것이 더 적절하다.


💡 커스텀 훅과 유틸리티 함수를 구분하는 방법

커스텀 훅과 유틸리티 함수를 쉽게 구분하는 방법은 React 기능이 사용되었는지를 기준으로 삼는 것이다.

  • React의 훅(useState, useEffect 등)이 포함된 함수커스텀 훅으로 구현할 수 있다. 이 함수는 상태 관리와 부수 효과를 처리하는 로직을 분리하고 재사용하는 목적을 가진다.
  • 반면, 순수한 자바스크립트 함수로만 구성된 로직유틸리티 함수로 구분된다. 이는 상태와 관련이 없는 계산 로직이나 변환 로직을 처리하며, 일반적인 자바스크립트 함수로 작성된다.

💡 유틸리티 함수와의 차별점

커스텀 훅과 유틸리티 함수는 비즈니스 로직을 분리하는 두 가지 다른 방식이다. 커스텀 훅은 React의 상태 관리와 사이드 이펙트에 관련된 로직을 분리하여 재사용성을 높이는 것이 주된 목표다. 반면, 유틸리티 함수는 상태와 무관하게 입력값을 받아 결과를 반환하는 순수 함수로, 상태나 React의 생명주기와는 무관한 비즈니스 로직을 처리하는 데 적합하다.

이렇듯, 커스텀 훅은 상태와 관련된 비즈니스 로직을 분리하는 데 사용되고, 유틸리티 함수는 상태와 상관없는 비즈니스 로직을 처리하는 데 사용된다.


💡 비즈니스 로직의 명확한 분리

커스텀 훅 패턴을 사용하는 이유는, 상태 관리와 사이드 이펙트를 로직과 명확히 분리하여 재사용성을 높이기 위함이다. 비즈니스 로직을 분리함으로써, 여러 컴포넌트에서 동일한 로직을 반복하지 않고 공통 로직을 재사용할 수 있다. 그러나, 상태와 상관없는 유틸리티 함수까지 커스텀 훅으로 분리하지는 않는다. 이는 UI와 비즈니스 로직을 완전히 분리하지 않는 이유이기도 하다.

커스텀 훅 패턴은 상태와 관련된 비즈니스 로직을 다루지만, 모든 비즈니스 로직을 커스텀 훅으로 분리하지 않는다는 점을 기억해야 한다.


💡 (중요) 정리하자면?

즉, 커스텀 훅 패턴은 상태 관리 및 사이드 이펙트와 관련된 비즈니스 로직을 분리해 재사용성을 높이는 것이 목표이다. 반면, 유틸리티 함수는 상태와 무관한 비즈니스 로직을 처리하는 데 사용된다. 따라서, 커스텀 훅과 유틸리티 함수를 적절히 구분하고 관리하는 것이 중요하다. (예: 커스텀 훅을 모아두는 hooks 폴더와, 유틸리티 함수를 모아두는 utils 폴더를 구분하는 방식)

src/
│
├── hooks/
│   ├── useFetch.js
│   ├── useCounter.js
│
├── utils/
│   ├── formatDate.js
│   ├── calculateDiscount.js

이 폴더 구조는 커스텀 훅과 유틸리티 함수를 구분하여 관리하는 예시로, 이를 통해 각 로직의 역할을 명확히 나눌 수 있다.


📌 커스텀 훅 내 유틸리티 함수 사용

상황에 따라 간단한 유틸리티 함수는 커스텀 훅 내에 포함시킬 수 있지만, 다른 곳에서도 자주 사용되거나 로직이 복잡한 경우에는 별도로 유틸리티 함수로 분리하는 것이 더 좋은 선택이다.

function useFormattedDate(initialDate) {
  const [date, setDate] = useState(initialDate);

  // 간단한 유틸리티 함수: 날짜 포맷팅
  const formatDate = (date) => {
    return new Date(date).toLocaleDateString();
  };

  useEffect(() => {
    console.log("Date updated");
  }, [date]);

  return { date, formattedDate: formatDate(date), setDate };
}

📌 커스텀 훅과 유틸리티 함수의 관계 정리

  • 커스텀 훅React의 상태 관리와 사이드 이펙트 로직을 처리하기 위해 사용된다.
  • 유틸리티 함수React와 관계없는 로직을 처리하며, 필요에 따라 커스텀 훅 안에서 사용될 수 있다.
  • 간단한 유틸리티 함수는 커스텀 훅 내에 포함할 수 있지만, 복잡한 로직이나 여러 곳에서 재사용되는 경우에는 유틸리티 함수로 분리하는 것이 바람직하다.

왜 이런 내용들을 알아야하는데?

커스텀 훅 패턴에 대한 깊은 이해를 위해 유틸리티 함수비즈니스 로직 개념을 알아야 하는 이유는, 관심사 분리(Separation of Concerns) 원칙을 명확히 이해하기 위함이다. 비즈니스 로직이라는 용어는 대부분의 디자인 패턴과 컴포넌트 패턴에서 중요하게 다루어지며, 이를 적절히 분리함으로써 유지보수성협업 효율성을 극대화할 수 있다.

커스텀 훅 패턴 역시 관심사 분리의 개념을 적용한다. 하지만, 유틸리티 함수와 커스텀 훅의 역할이 다르다는 점에서 디테일한 차이가 존재한다. 유틸리티 함수는 비즈니스 로직의 일부로 볼 수 있지만, 커스텀 훅은 상태 관리와 사이드 이펙트에 관련된 로직만을 분리한다는 차이가 있다. 이로 인해 유틸리티 함수는 커스텀 훅으로 분리되지 않는다.

즉, 커스텀 훅 패턴은 UI와 비즈니스 로직을 완벽하게 분리하지는 못한다는 얘기가 된다.

관심사 분리에 대한 자세한 설명:
효율적인 소프트웨어 설계를 위한 핵심 원칙: 관심사의 분리(Separation of Concerns)

아래에서 프레젠테이셔널-컨테이너 패턴과 비교를 통해 커스텀 훅 패턴을 더 깊이 살펴보자. 이를 통해 두 패턴이 어떤 방식으로 관심사 분리를 달성하는지를 더 명확하게 이해할 수 있다.


커스텀 훅 패턴과 Presentational-Container 패턴

Presentational-Container 패턴과 커스텀 훅 패턴을 비교해서 설명하는 이유는, 두 패턴의 디테일한 차이를 이해하면 커스텀 훅 패턴에 대해서 더 깊이있는 이해를 할 수 있기 때문이다. 또한, 커스텀 훅 패턴은 Presentational-Container 패턴의 컨테이너 역할을 대체할 수 있다. Presentational-Container 패턴에서는 UI와 로직을 분리하기 위해 두 개의 컴포넌트를 사용하지만, 커스텀 훅을 사용하면 상태 관리와 사이드 이펙트 로직을 별도의 훅으로 분리하고, 해당 훅을 UI 컴포넌트에서 바로 호출해 사용할 수 있다.

심지어 Presentational-Container 패턴의 창시자도 커스텀 훅 패턴을 권장하고 있다.

관련 내용:
Presentational-Container 패턴의 모든 것: Hooks로 대체할 수 있을까?

그렇다면 두 패턴의 차이에 대해서 알아보자.


📌 Presentational-Container 패턴

이 패턴의 목표는 UI와 비즈니스 로직의 명확한 분리다. 프레젠테이셔널 컴포넌트는 순수하게 UI 렌더링만 담당하고, 컨테이너 컴포넌트는 상태 관리 및 비즈니스 로직을 처리한다.

컨테이너 컴포넌트(Container): 상태 관리, 데이터 처리, API 호출 같은 비즈니스 로직을 처리하며, UI 관련 로직은 없다.

프레젠테이셔널 컴포넌트(Presentational): 오직 UI 렌더링만 담당하며, 전달된 props만을 사용해 화면을 그린다.

// 컨테이너 컴포넌트: 비즈니스 로직 담당
function CounterContainer() {
  const [count, setCount] = useState(0);

  const increment = () => setCount(count + 1);

  return <CounterPresentation count={count} onIncrement={increment} />;
}

// 프레젠테이셔널 컴포넌트: UI 렌더링 담당
function CounterPresentation({ count, onIncrement }) {
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={onIncrement}>Increase</button>
    </div>
  );
}

컨테이너 컴포넌트는 상태와 로직을 관리하고, 프레젠테이셔널 컴포넌트는 UI만 담당한다. 이는 UI와 비즈니스 로직의 명확한 분리를 보장한다.

Presentational-Container 패턴에 대한 자세한 설명:
Presentational-Container 패턴의 모든 것: Hooks로 대체할 수 있을까?


📌 두 패턴의 차이

💡 구조의 차이

프레젠테이셔널-컨테이너 패턴두 개의 컴포넌트(프레젠테이셔널과 컨테이너)를 사용하여 UI와 비즈니스 로직을 분리한다. 컨테이너 컴포넌트는 상태 관리 및 비즈니스 로직을 처리하고, 프레젠테이셔널 컴포넌트는 UI만을 담당한다. 폴더와 파일 관리도 쌍으로 구성되는 경우가 많다.

반면, 커스텀 훅 패턴상태 관리와 사이드 이펙트 처리를 훅으로 분리하고, 이를 다양한 컴포넌트에서 재사용할 수 있다. 하지만 상태와 무관한 유틸리티 함수는 커스텀 훅으로 분리되지 않기 때문에, 완전한 UI와 로직 분리는 보장되지 않는다.


💡 유연성

프레젠테이셔널-컨테이너 패턴에서는 컨테이너 컴포넌트가 특정 프레젠테이셔널 컴포넌트와 밀접하게 결합되므로 상태 관리 방식이 고정적일 수 있다.

커스텀 훅 패턴은 훅을 통해 상태 관리와 사이드 이펙트 로직을 쉽게 재사용할 수 있기 때문에, 다양한 컴포넌트에서 유연하게 사용할 수 있다.


💡 재사용성

프레젠테이셔널-컨테이너 패턴컨테이너 컴포넌트가 특정 프레젠테이셔널 컴포넌트와 강하게 결합되기 때문에 재사용성이 떨어질 수 있다.

반면, 커스텀 훅 패턴은 상태 관리 및 사이드 이펙트 로직을 별도의 함수로 만들어, 여러 컴포넌트에서 쉽게 재사용할 수 있다. 그러나 모든 비즈니스 로직을 분리하는 것은 아니므로 재사용성이 로직 전체에 적용된다고 보기는 어렵다.


💡 로직 분리의 방식

프레젠테이셔널-컨테이너 패턴에서 컨테이너 컴포넌트는 로직을 처리한 후 그 결과를 props로 프레젠테이셔널 컴포넌트에 전달한다. 컴포넌트의 역할이 명확히 분리되지만, 두 개의 컴포넌트가 만들어지고, props로 데이터를 주고받는 구조가 필수적이다. 즉, props를 계속 전달하는 방식으로 인해 복잡해질 수 있다. 이 방식은 컴포넌트 간의 결합도가 높아질 수 있다.

커스텀 훅 패턴로직을 훅으로 분리하고, 컴포넌트는 이 훅을 호출해 그 결과를 바로 사용한다. props를 전달하지 않고 함수 호출로 필요한 데이터를 가져오는 방식이기 때문에 더 단순한 구조를 제공한다. 컴포넌트 간의 결합도가 낮아지고 유연성이 증가한다.


📌 분리해야 할 로직이 한 곳에서만 사용될 때

만약 다른 곳에서 재사용되지 않고 한 컴포넌트에서만 커스텀 훅을 사용하게 된다면, 커스텀 훅으로 분리했을 때도 파일이 두 개로 나뉘어 있다는 점에서 프레젠테이셔널-컨테이너 패턴과 크게 다르지 않다고 느낄 수 있다. 이 경우에 왜 굳이 커스텀 훅 패턴을 사용해야 하는가에 대한 고민이 자연스럽게 따라오는데, 프레젠테이셔널-컨테이너 패턴 대신 커스텀 훅 패턴을 사용하는 가장 중요한 이유는 추후 확장 가능성이다.

💡 추후 확장 가능성

한 컴포넌트에서만 로직이 사용되더라도, 커스텀 훅으로 로직을 미리 분리해 두면 추후 다른 컴포넌트에서도 쉽게 확장할 수 있다. 커스텀 훅은 필요한 곳에서 간단히 호출해 사용할 수 있으므로, 장기적인 확장성을 고려하면 미리 로직을 분리하는 것이 더 유리하다.


📌 결국 프레젠테이셔널-컨테이너 패턴 대신 커스텀 훅 패턴

그러면 이제 프레젠테이셔널-컨테이너 패턴 대신 커스텀 훅 패턴을 사용해야 하는 이유에 대해서 명확하게 설명할 수 있다.

프레젠테이셔널-컨테이너 패턴은 항상 컨테이너 컴포넌트와 프레젠테이셔널 컴포넌트가 짝을 이루어야 한다. 즉, 한 번 작성된 컨테이너 컴포넌트는 해당 프레젠테이셔널 컴포넌트와만 동작할 가능성이 크다. 다른 곳에서 이 로직을 사용하려면 새로운 컨테이너 컴포넌트를 만들어야 할 수 있다.

커스텀 훅은 단순한 함수이기 때문에, 다른 컴포넌트에서 재사용하기가 쉽다. 예를 들어, 나중에 다른 컴포넌트에서도 같은 로직을 사용해야 한다면, 커스텀 훅을 간단히 호출해서 사용할 수 있다. 즉, 유연하게 수정하거나 확장할 수 있는 여지가 더 크다.

또한, React 상태 관리, 사이드 이펙트 로직 이외의 유틸 함수는 따로 유틸 함수로 분리하면 된다. 즉, 프레젠테이셔널-컨테이너 패턴은 커스텀 훅 패턴으로 충분히 대체가 될 뿐만 아니라, 유지 보수와 확장성에 있어서도 커스텀 훅 패턴이 훨씬 좋은 선택이 된다.

💡 요약하자면

커스텀 훅 패턴은 본질적으로 UI와 비즈니스 로직을 완전히 분리하는 패턴이 아니다. React의 상태 관리와 사이드 이펙트 처리 로직을 여러 컴포넌트에서 재사용할 수 있게 만드는 패턴이라고 보는 것이 더 정확하다. 비즈니스 로직을 완전히 분리하려면 커스텀 훅만으로는 부족하고, 유틸 함수와 같은 구조를 활용해야 한다. 하지만 유틸 함수를 분리하는 것은 위에서도 설명했듯 어렵지 않기 때문에 프레젠테이셔널-컨테이너 패턴은 커스텀 훅 패턴으로 대체된다.


참고문헌

0개의 댓글