로직 재사용의 완성 - 나만의 Custom Hook 만들기
리액트 공식문서 기반의 리액트 입문기
들어가며
지금까지 우리는 리액트의 다양한 훅들을 살펴보면서 컴포넌트 내부에서 상태를 관리하고, 생명주기에 맞춰 사이드 이펙트를 처리하는 방법을 익혀왔습니다. 특히 5-8편에서는 Effect의 재실행을 줄이기 위한 심화 전략들을 통해 컴포넌트의 안정성과 성능을 향상시키는 방법을 배웠습니다.
하지만 개발을 진행하다 보면 여러 컴포넌트에서 동일하거나 비슷한 로직을 반복적으로 작성해야 하는 상황에 마주치곤 합니다. 예를 들어, 토글 기능을 구현하거나 특정 입력 필드의 상태를 관리하는 로직은 여러 곳에서 필요할 수 있습니다. 이런 반복적인 로직은 코드의 양을 늘리고, 유지보수를 어렵게 만들며, 새로운 기능을 추가할 때마다 비슷한 코드를 다시 작성해야 하는 번거로움을 초래할 수 있습니다.
리액트는 이러한 문제를 해결하기 위해 Custom Hook이라는 강력한 기능을 제공합니다. Custom Hook은 컴포넌트 로직을 재사용 가능한 함수로 추출하여, 여러 컴포넌트에서 동일한 상태 로직을 공유하고 관심사를 분리할 수 있도록 돕습니다.
이번 편에서는 Custom Hook이 무엇인지, 어떻게 만드는지, 그리고 왜 Custom Hook을 사용해야 하는지에 대해 기본적인 내용들을 함께 살펴보려 합니다.
Custom Hook의 특징: 로직 재사용의 완성
Custom Hook은 이름 그대로 개발자가 직접 만드는 특별한 자바스크립트 함수입니다. 이 함수는 리액트에서 제공하는 useState, useEffect, useContext와 같은 기본적인 훅들을 내부에 포함하여 특정 로직을 추상화하고 재사용할 수 있도록 설계됩니다.
Custom Hook은 컴포넌트의 렌더링 로직과는 독립적으로 상태 관련 로직을 관리하며, 이를 필요한 컴포넌트에서 불러와 사용할 수 있도록 합니다. 이는 마치 특정 기능을 하는 작은 라이브러리를 직접 만들어 사용하는 것과 비슷하다고 볼 수 있습니다.
Custom Hook의 핵심적인 특징은 다음과 같습니다.
- 로직 재사용: 여러 컴포넌트에서 동일한 상태 관련 로직을 중복 없이 재사용할 수 있게 됩니다. 이는 코드의 양을 줄이고, 일관된 동작을 보장하는 데 도움이 될 수 있습니다.
- 관심사 분리: 컴포넌트는 UI 렌더링에 집중하고, 상태 관리나 사이드 이펙트와 같은 로직은 Custom Hook으로 분리할 수 있습니다. 이는 컴포넌트 코드를 더욱 깔끔하고 이해하기 쉽게 만드는 데 기여할 수 있습니다.
- 상태 독립성: 여러 컴포넌트에서 동일한 Custom Hook을 사용하더라도, 각 컴포넌트 인스턴스는 자신만의 독립적인 상태를 가지게 됩니다. 이는 Custom Hook이 상태 자체를 공유하는 것이 아니라,
useState와 같은 리액트 내장 훅의 로직을 재사용하고, 리액트가 각 컴포넌트 인스턴스에 고유한 상태 저장 공간을 할당하여 관리하기 때문입니다. 따라서 한 컴포넌트의 상태 변경이 다른 컴포넌트에 영향을 주지 않습니다. - 유연성과 확장성: Custom Hook은 일반적인 자바스크립트 함수이기 때문에, 필요한 만큼 조합하거나 확장하여 복잡한 로직도 체계적으로 관리할 수 있습니다. 이는 애플리케이션의 규모가 커질수록 더욱 빛을 발할 수 있습니다.
리액트로 Custom Hook 만들기: 기본 구조와 규칙
Custom Hook을 만드는 것은 생각보다 어렵지 않습니다. 몇 가지 간단한 규칙만 지킨다면 누구나 자신만의 Custom Hook을 만들 수 있습니다.
Custom Hook 작성 규칙
use로 시작하는 이름: Custom Hook은 반드시use로 시작하는 이름으로 지어야 합니다 (예:useToggle,useCounter). 이 규칙은 리액트 린터가 훅의 규칙을 올바르게 적용하고, 개발자들이 어떤 함수가 훅인지 쉽게 알아볼 수 있도록 돕습니다.- 다른 훅 호출: Custom Hook 내부에서는
useState,useEffect,useContext와 같은 다른 훅들을 호출할 수 있습니다. 이 점이 일반적인 자바스크립트 함수와 Custom Hook을 구분하는 가장 중요한 특징 중 하나입니다. - 컴포넌트 또는 다른 Custom Hook 내에서 호출: Custom Hook은 리액트 함수 컴포넌트의 최상위 레벨 또는 다른 Custom Hook 내에서만 호출되어야 합니다. 조건문, 반복문, 중첩된 함수 내부에서는 호출할 수 없습니다.
간단한 Custom Hook 예제: useToggle
이제 간단한 useToggle Custom Hook을 만들어보겠습니다. 이 훅은 특정 상태(boolean 값)를 토글하는 기능을 제공합니다.
// useToggle.js
import { useState } from 'react';
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = () => {
setValue(currentValue => !currentValue);
};
return [value, toggle];
}
export default useToggle;코드 설명:
useToggle함수는initialValue라는 선택적 매개변수를 받아 토글 상태의 초기값을 설정합니다. 기본값은false입니다.useState훅을 사용하여value라는 상태와setValue함수를 생성합니다.toggle함수는 현재value의 반대값으로 상태를 업데이트하는 역할을 합니다.- 마지막으로,
useToggle훅은 현재value와toggle함수를 배열 형태로 반환합니다. 이는useState훅이[state, setState]형태로 값을 반환하는 것과 유사합니다.
useToggle Custom Hook 활용 예제
이제 useToggle Custom Hook을 컴포넌트에서 어떻게 활용하는지 살펴보겠습니다.
// App.jsx
import React from 'react';
import useToggle from './useToggle'; // 위에서 정의한 useToggle 훅을 불러옵니다.
function App() {
const [isLightOn, toggleLight] = useToggle(false); // isLightOn은 false로 시작합니다.
const [isVisible, toggleVisibility] = useToggle(true); // isVisible은 true로 시작합니다.
return (
<div>
<h2>Custom Hook 예제: useToggle</h2>
{/* 첫 번째 토글 */}
<p>전등 상태: {isLightOn ? '켜짐' : '꺼짐'}</p>
<button onClick={toggleLight}>전등 {isLightOn ? '끄기' : '켜기'}</button>
<hr />
{/* 두 번째 토글 */}
{isVisible && <p>이 문장은 보였다 안 보였다 합니다.</p>}
<button onClick={toggleVisibility}>문장 {isVisible ? '숨기기' : '보이기'}</button>
</div>
);
}
export default App;코드 설명:
App컴포넌트에서는useToggle훅을 두 번 호출하여 각각isLightOn과isVisible이라는 독립적인 토글 상태를 관리합니다.useToggle훅이 반환하는isLightOn(또는isVisible) 값으로 UI를 렌더링하고,toggleLight(또는toggleVisibility) 함수를 버튼의onClick이벤트 핸들러로 사용하여 상태를 변경합니다.- 각각의
useToggle호출은 완전히 독립적인 상태를 가지기 때문에, 한 토글의 상태를 변경해도 다른 토글의 상태에는 영향을 주지 않습니다.
이처럼 Custom Hook을 사용하면 반복되는 로직을 깔끔하게 추상화하고, 여러 컴포넌트에서 쉽게 재사용할 수 있습니다. 이는 코드의 가독성을 높이고, 개발 효율성을 크게 향상시키는 데 도움이 될 수 있습니다.
Custom Hook 사용 시 고려사항
Custom Hook은 로직 재사용과 관심사 분리라는 강력한 이점을 제공하지만, 모든 상황에서 Custom Hook이 최선의 해결책인 것은 아닙니다. Custom Hook을 사용하기 전에 다음과 같은 사항들을 고려해 볼 수 있습니다.
- 불필요한 추상화 지양: 특정 로직이 오직 하나의 컴포넌트에서만 사용되거나 매우 간단하여 쉽게 이해할 수 있는 경우, 굳이 Custom Hook으로 분리하여 추상화할 필요는 없습니다. 과도한 추상화는 오히려 코드의 복잡성을 높이고 유지보수를 어렵게 만들 수 있습니다. Custom Hook은 주로 여러 컴포넌트에서 반복적으로 사용되는 로직이나, 컴포넌트 내부에서 처리하기에 너무 복잡한 상태 관리 로직을 분리할 때 그 진가를 발휘합니다.
- 테스트 용이성 고려: Custom Hook으로 로직을 분리하면 해당 로직을 컴포넌트와 독립적으로 테스트하기 용이해지는 장점이 있습니다. 하지만 테스트가 불필요할 정도로 간단한 로직을 굳이 Hook으로 분리하는 것은 개발 오버헤드를 증가시킬 수 있습니다. Custom Hook을 설계할 때는 해당 로직이 얼마나 재사용될 것인지, 그리고 테스트의 필요성이 얼마나 큰지를 함께 고려하는 것이 좋습니다.
- 성능에 대한 오해 방지: Custom Hook을 사용한다고 해서 무조건적인 성능 향상이 보장되는 것은 아닙니다. 오히려 잘못된 의존성 관리나 불필요한 복잡성 추가는 성능 저하를 야기할 수도 있습니다. Custom Hook은 주로 로직의 구조화와 재사용성, 그리고 관심사 분리에 초점을 맞춘다는 점을 이해하고 사용해야 합니다.
Custom Hook은 리액트 개발의 생산성과 코드 품질을 높이는 데 중요한 역할을 하지만, 현명하게 사용하여 그 이점을 최대한으로 누리는 것이 중요합니다.
Hook의 재사용성 가이드라인
Custom Hook을 만들 때, 단순히 로직을 분리하는 것을 넘어 재사용성을 극대화하기 위한 몇 가지 전략을 고려해 볼 수 있습니다.
- 유연한 API 설계: Hook의 인자로 초기값이나 설정 객체를 받아 내부 로직의 동작을 커스터마이징할 수 있도록 설계합니다. 예를 들어,
useCounterHook을 만들 때 초기값뿐만 아니라 증가/감소 단위를 설정할 수 있는 옵션을 제공할 수 있습니다.- ex:
useCounter(0, { step: 5 })와 같이 초기값과 증가 단위를 함께 전달하여 유연하게 카운터를 조작할 수 있습니다.
- ex:
- 콜백 함수 활용: Hook 내부에서 비동기 작업이나 특정 이벤트 발생 시 실행될 콜백 함수를 인자로 받아 유연성을 높일 수 있습니다. 이는 Custom Hook이 특정 액션을 수행한 후 외부 컴포넌트에 결과를 알리거나 추가 작업을 요청할 때 유용할 수 있습니다.
- ex:
useFetch('/api/data', { onSuccess: (data) => console.log(data) })와 같이 데이터 페칭 성공 시 특정 로직을 실행하는 콜백을 전달할 수 있습니다.
- ex:
- 반환 값의 다양성: Hook이 반환하는 값은 단순히 상태 값과 상태 변경 함수에 그치지 않고, 관련 유틸리티 함수나 파생된 값, 그리고 로딩 및 에러 상태 등을 포함할 수 있습니다. 객체나 배열 형태로 여러 값을 반환하여 소비하는 컴포넌트가 필요한 값만 선택적으로 사용할 수 있도록 설계하는 것이 좋습니다.
- ex:
const { value, setValue, reset } = useInput('')와 같이 입력 값, 변경 함수, 초기화 함수 등을 객체 형태로 반환하여 필요에 따라 구조 분해 할당하여 사용할 수 있습니다.
- ex:
- 관심사 분리 명확화: 하나의 Custom Hook이 너무 많은 기능을 하려 하기보다, 단일 책임 원칙에 따라 하나의 명확한 관심사를 처리하도록 설계합니다. 복잡한 로직은 여러 Custom Hook으로 분리하고 조합하여 사용하는 것이 유지보수와 가독성 측면에서 더 유리할 수 있습니다.
- ex:
useForm이라는 훅에서 전체 폼 관리를 담당하되, 개별 입력 필드의 로직은useInput을, 유효성 검사 로직은useValidation과 같이 작은 단위의 Custom Hook들을 조합하여 사용할 수 있습니다.
- ex:
이러한 전략들은 Custom Hook이 다양한 상황에서 더욱 효과적으로 사용될 수 있도록 돕고, 개발 효율성을 한층 더 끌어올리는 데 기여할 수 있습니다.
useState와 useReducer: Custom Hook에서의 선택 가이드라인
Custom Hook을 개발할 때, 내부적으로 상태를 관리하기 위해 useState와 useReducer 중 어떤 훅을 사용할지 결정하는 것은 중요한 설계 고려사항입니다. 두 훅 모두 상태 관리 기능을 제공하지만, 각각의 특징과 장단점을 이해하고 적절하게 선택하는 것이 필요합니다.
useState의 적합성:- 간단한 상태: 단일 값이거나 독립적인 여러 상태를 관리할 때 적합합니다. 예를 들어, 토글(
boolean), 카운터(number), 간단한 입력 필드 값(string) 등은useState로 충분히 관리할 수 있습니다.- ex:
useToggle훅에서const [isOn, setIsOn] = useState(false);와 같이 단순히true/false값을 관리하는 경우입니다.
- ex:
- 간결한 코드: 상태 업데이트 로직이 단순하고 직관적일 때 코드가 간결해집니다. 별도의 리듀서 함수를 작성할 필요 없이
setState함수를 통해 직접 상태를 변경할 수 있습니다.- ex:
setIsOn(prev => !prev);와 같이 이전 상태에 기반하여 새로운 상태를 설정하는 간단한 로직이 해당합니다.
- ex:
- 간단한 상태: 단일 값이거나 독립적인 여러 상태를 관리할 때 적합합니다. 예를 들어, 토글(
useReducer의 적합성:- 복잡한 상태 로직: 여러 하위 상태가 서로 연관되어 있거나, 다음 상태가 이전 상태에 의존하며, 상태 업데이트 로직이 복잡할 때
useReducer가 빛을 발합니다. 특히 상태 전이(state transition)가 명확하게 정의될 때 유용할 수 있습니다.- ex: 여러 입력 필드를 가진 폼의 상태(
form: { username: '', email: '', password: '' })를 관리하거나, 스텝별 진행 상황을 가진 복잡한 위자드(wizard: { currentStep: 1, data: {} })와 같은 상태 로직을 관리하는 경우입니다.
- ex: 여러 입력 필드를 가진 폼의 상태(
- 중앙 집중식 상태 관리: 여러 상태 변경 로직을 하나의 리듀서 함수 내에서 관리함으로써, Custom Hook의 상태 관리 로직을 중앙 집중화하고 예측 가능하게 만들 수 있습니다. 이는 테스트 용이성을 높이고 버그 발생 가능성을 줄이는 데 도움이 될 수 있습니다.
- ex:
dispatch({ type: 'UPDATE_FIELD', field: 'username', value: 'newVal' })와 같이 액션 객체를 통해 상태를 일관된 방식으로 업데이트하고 관리할 수 있습니다.
- ex:
- 성능 최적화 가능성:
dispatch함수는 한 번 생성되면 변경되지 않으므로, 이를 하위 컴포넌트에props로 전달할 때 불필요한 리렌더링을 방지하는 데useCallback과 함께 활용될 수 있습니다.- ex:
const [state, dispatch] = useReducer(reducer, initialState);에서dispatch함수를useContext와 함께 사용하여 여러 하위 컴포넌트에 전달해도 컴포넌트의 불필요한 리렌더링을 유발하지 않는 경우입니다.
- ex:
- 복잡한 상태 로직: 여러 하위 상태가 서로 연관되어 있거나, 다음 상태가 이전 상태에 의존하며, 상태 업데이트 로직이 복잡할 때
결론적으로, Custom Hook의 상태 관리 전략은 해당 Hook이 다룰 로직의 복잡성과 재사용될 맥락을 충분히 고려하여 useState와 useReducer 중 더 적합한 훅을 선택하고, 필요에 따라 유연하게 전환하는 것이 현명한 접근 방식이라고 할 수 있습니다.
요약
이번 편에서는 반복되는 로직을 효과적으로 재사용하기 위한 리액트의 강력한 기능, Custom Hook의 기본적인 개념과 작성 방법에 대해 살펴보았습니다.
- Custom Hook의 본질: 리액트 훅을 조합하여 로직을 추상화하고 재사용 가능한 함수로 만드는 개발자의 도구입니다.
- 작성 규칙 및 패턴:
use로 시작하는 이름, 다른 훅 호출, 컴포넌트 최상위 또는 다른 Custom Hook 내에서 호출이라는 세 가지 핵심 규칙을 따릅니다. - 활용의 장점: 코드 재사용성 증대, 컴포넌트와 로직의 관심사 분리, 코드의 가독성 및 유지보수성 향상에 기여합니다.
- 상태 독립성: 여러 컴포넌트에서 동일한 Custom Hook을 사용해도 각각 독립적인 상태를 관리합니다.
- 사용 시 고려사항: 불필요한 추상화를 피하고, 로직의 재사용성과 복잡성, 테스트 용이성을 고려하여 신중하게 사용해야 합니다.