커스텀훅 vs 고차 컴포넌트
커스텀 훅과 고차 컴포넌트는 무엇일까?
리액트로 개발을 하다보면 효율적인 코드관리를 위하여 따로 hook 을 분리를 하거나 컴포넌트를 분리를 하는 경우가 매우 자주 있을것이다.
이때, 커스텀 훅 혹은 고차 컴포넌트로 분리를 할 수 있는데 커스텀 훅과 고차 컴포넌트는 무엇일까??
커스텀 훅
사용자 정의 훅 혹은 커스텀 훅이라고 불리는 이 커스텀 훅 이라는건 정확히 무엇일까??
리액트 hook의 기본 원칙
- 훅의 이름만 use로 시작합니다. 이를 통해 use로 시작하면 hook이구나 바로 알 수 있죠
- 훅을 사용할 수 있는 곳은, 함수 컴포넌트 내부 또는 custom hook 내부에서만 사용할 수 있습니다.
- hook의 이름을 직접 지정해줄 수 있으므로, 사용하기에 따라 가독성을 좋게 만들어주기도 합니다.
예시
import { useState, useEffect } from 'react';
function useWindowSize() {
const [windowSize, setWindowSize] = useState({
width: undefined,
height: undefined,
});
useEffect(() => {
function handleResize() {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
}
window.addEventListener('resize', handleResize);
handleResize(); // 초기 크기 설정
return () => window.removeEventListener('resize', handleResize);
}, []); // 빈 배열은 컴포넌트 마운트 시 한 번만 실행됨을 의미
return windowSize;
}
export default useWindowSize;
이러한 리액트의 훅의 규칙을 따르면서 React에서 상태 로직을 컴포넌트 간에 재사용할 수 있게 해주는 기능입니다. 이를 통해 중복 코드를 줄이고 로직을 더 깔끔하게 관리할 수 있습니다.
고차 컴포넌트
그럼 커스텀 훅과 함께 또 하나의 효율적인 코드 관리법 중 하나인 고차 컴포넌트란 무엇일까요??
고차컴포넌트(HOC, HigherOrderComponent)는
컴포넌트자체의 로직을 재시용히키위한방법이다. 사용자정의훅은 리액트훅을 기반으로히므로 리액트에서만사용 할 수 있는 기술이지만 고차컴포넌트는 고차 함수의 일종으로, 자바스크립트 일급 객체, 함수의 특징을 이용함으로 리액트가 아닌 자바스크립트 환경에서 사용이 가능합니다.
고차 함수의 사전적인 정의를 살펴보면
‘함수를 인수로받거나결과로 반환하는 함수'
라고 적혀있다
예시
import React, { useState, useEffect } from 'react';
function withLoading(WrappedComponent) {
return function WithLoadingComponent({ isLoading, ...props }) {
const [loading, setLoading] = useState(isLoading);
useEffect(() => {
setLoading(isLoading);
}, [isLoading]);
if (loading) {
return <div>로딩 중...</div>;
}
return <WrappedComponent {...props} />;
};
}
// 사용 예시
function MyComponent({ data }) {
return <div>{data}</div>;
}
const MyComponentWithLoading = withLoading(MyComponent);
// 부모 컴포넌트에서 사용
function ParentComponent() {
const [isLoading, setIsLoading] = useState(true);
const [data, setData] = useState(null);
useEffect(() => {
// 데이터 로딩을 시뮬레이션
setTimeout(() => {
setData("로딩된 데이터");
setIsLoading(false);
}, 2000);
}, []);
return <MyComponentWithLoading isLoading={isLoading} data={data} />;
}
export default ParentComponent;
이 withLoading HOC는 다음과 같이 작동합니다
withLoading 함수는 컴포넌트를 인자로 받습니다.
새로운 컴포넌트를 반환하며, 이 컴포넌트는 isLoading prop을 받습니다.
로딩 중일 때는 "로딩 중..." 메시지를 표시합니다.
로딩이 완료되면 원래의 컴포넌트를 렌더링합니다.
또한, 꼭 지켜져야 하는 규칙은 아니지만(ESlint 에 걸리지 않는다) with로 시직하는 이름을 시용해야 한다.
그럼 어떠한 차이가 있을까??
구현 방식
커스텀 훅: 함수로 구현되며, React의 내장 훅을 사용합니다.
HOC: 컴포넌트를 인자로 받아 새 컴포넌트를 반환하는 함수입니다.
사용 방법
커스텀 훅: 컴포넌트 내부에서 직접 호출합니다.
HOC: 컴포넌트를 감싸는 방식으로 사용합니다.
로직 공유
커스텀 훅: 상태 로직을 공유하기 쉽습니다.
HOC: 주로 프로퍼티 조작이나 렌더링 로직을 공유합니다.
구성(Composition)
커스텀 훅: 여러 훅을 쉽게 조합할 수 있습니다.
HOC: 여러 HOC를 중첩하면 복잡해질 수 있습니다.
디버깅
커스텀 훅: 일반 함수처럼 디버깅하기 쉽습니다.
HOC: 컴포넌트 계층이 깊어져 디버깅이 어려울 수 있습니다.
성능
커스텀 훅: 일반적으로 더 좋은 성능을 보입니다.
HOC: 불필요한 중첩으로 인해 성능 저하가 발생할 수 있습니다.
예제 코드로 한번 살펴보자
import React, { useState, useEffect } from 'react';
// 커스텀 훅 예시
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return width;
}
// 커스텀 훅을 사용하는 컴포넌트
function WindowWidthComponent() {
const width = useWindowWidth();
return <div>Window width is: {width}</div>;
}
// HOC 예시
function withWindowWidth(WrappedComponent) {
return class extends React.Component {
state = { width: window.innerWidth };
componentDidMount() {
window.addEventListener('resize', this.handleResize);
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
}
handleResize = () => {
this.setState({ width: window.innerWidth });
};
render() {
return <WrappedComponent width={this.state.width} {...this.props} />;
}
};
}
// HOC를 사용하는 컴포넌트
const WindowWidthComponentWithHOC = withWindowWidth(({ width }) => (
<div>Window width is: {width}</div>
));
// 사용 예
function App() {
return (
<div>
<h2>Using Custom Hook:</h2>
<WindowWidthComponent />
<h2>Using HOC:</h2>
<WindowWidthComponentWithHOC />
</div>
);
}
export default App;
이 예제에서 볼 수 있듯이:
커스텀 훅 (useWindowWidth)은 간단하게 사용할 수 있고, 컴포넌트 내부에서 직접 호출됩니다.
HOC (withWindowWidth)는 컴포넌트를 감싸는 형태로 사용되며, 새로운 컴포넌트를 생성합니다.
커스텀 훅은 일반적으로 더 간결하고 유연하며, 함수형 프로그래밍 패러다임과 잘 어울립니다.
반면 HOC는 클래스 컴포넌트와 함께 사용될 때 더 자연스러우며, 레거시 코드에서 자주 볼 수 있습니다.
그럼 언제 뭘써야할까?
모던 리액트 딥다이브를 읽어보면서 생각해본 결과
커스텀 훅과 고차 컴포넌트(HOC)를 언제 사용하면 좋을지에 대한 내 생각을 적어보겠습니다
커스텀 훅 사용이 좋은 경우
- 상태 로직 재사용: 여러 컴포넌트에서 동일한 상태 관리 로직을 사용해야 할 때
- 부수 효과 관리: API 호출, 이벤트 리스너 등의 부수 효과를 관리할 때
- 간단한 로직 추상화: 복잡한 로직을 간단한 인터페이스로 추상화하고 싶을 때
- 함수형 컴포넌트와 함께 사용: 최신 React 패러다임에 맞춰 개발할 때
- 컴포지션이 필요한 경우: 여러 로직을 조합해야 할 때
예시:
function useAPI(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(url)
.then(response => response.json())
.then(data => {
setData(data);
setLoading(false);
});
}, [url]);
return { data, loading };
}
// 사용
function MyComponent() {
const { data, loading } = useAPI('https://api.example.com/data');
// ...
}
HOC 사용이 좋은 경우:
- 프로퍼티 주입: 컴포넌트에 추가적인 프로퍼티를 주입해야 할 때
- 렌더링 하이재킹: 컴포넌트의 렌더링 결과를 조작해야 할 때
- 상태 추상화: 특정 상태 로직을 여러 컴포넌트에 적용해야 할 때
- 크로스 커팅 관심사: 로깅, 인증 등 여러 컴포넌트에 공통으로 적용되는 기능을 구현할 때
- 클래스 컴포넌트와 함께 사용: 레거시 코드를 다룰 때
예시:
function withAuth(WrappedComponent) {
return class extends React.Component {
state = { isAuthenticated: false };
componentDidMount() {
this.checkAuth();
}
checkAuth() {
// 인증 로직...
this.setState({ isAuthenticated: true });
}
render() {
if (!this.state.isAuthenticated) {
return <div>Please log in</div>;
}
return <WrappedComponent {...this.props} />;
}
};
}
// 사용
const ProtectedComponent = withAuth(MyComponent);
선택 기준:
코드 복잡도:
- 간단한 로직이면 커스텀 훅
- 복잡한 렌더링 로직이 필요하면 HOC
재사용성:
- 상태 로직만 재사용하려면 커스텀 훅
- 렌더링 로직까지 재사용하려면 HOC
성능:
- 성능이 중요하면 커스텀 훅 (불필요한 중첩 방지)
디버깅 용이성:
- 디버깅이 중요하면 커스텀 훅 (컴포넌트 트리가 단순해짐)
정도 인것 같다