처음에는 Hooks를 그저 함수형 컴포넌트에서 useState와 useEffect를 쓸 수 있게 해주는 '도구' 정도로만 생각했습니다. 하지만 이번에 Hooks가 클래스 컴포넌트의 근본적인 문제점을 해결하기 위해 탄생했다는 것을 알고 React를 바라보는 시각이 바뀌었습니다.
Hooks를 통해 얻은 가장 큰 깨달음은 컴포넌트의 역할 분리입니다. 과거에는 하나의 컴포넌트 안에 UI, 상태, 그리고 부수 효과(side effects) 로직이 모두 섞여 있었지만, 이제는 UI는 컴포넌트에, 상태 로직은 커스텀 훅에 분리하여 훨씬 깔끔하고 읽기 쉬운 코드를 작성할 수 있게 되었습니다.
이번 학습을 통해, 단순히 기능을 구현하는 것을 넘어 코드의 구조와 재사용성까지 고민하는 것이 얼마나 중요한지 깨달았습니다. 이제는 눈에 보이는 결과뿐만 아니라, 그 뒤에 숨겨진 코드의 구조와 재사용성에 대해 깊이 고민해보려고 합니다.
React Hooks가 등장하기 전, 함수형 컴포넌트는 오직 'props'를 받아 UI를 렌더링하는 순수 함수에 불과했습니다. 상태를 관리하거나 생명주기 로직을 다루기 위해서는 복잡하고 가독성이 떨어지는 클래스형 컴포넌트를 사용해야만 했었지만, Hooks의 등장과 함께 함수형 컴포넌트는 React의 모든 기능을 사용할 수 있는 강력한 존재가 되었습니다.
이 글에서는 Hooks가 왜 탄생했는지, Hooks의 핵심 개념과 주요 특징은 무엇인지, 그리고 실무에서 어떻게 활용되는지 작성해보겠습니다.
1. Hooks가 왜 필요했을까? (클래스 컴포넌트의 한계)
Hooks는 단순히 코드를 간결하게 만들기 위해 도입된 것이 아닙니다. 기존 클래스 컴포넌트가 가진 근본적인 문제점을 해결하기 위해 탄생했습니다.
1) this의 복잡성
JavaScript에서 this는 호출되는 시점에 따라 동적으로 바인딩되기 때문에 예측하기 어렵고 버그를 유발하기 쉽습니다. 특히 클래스 컴포넌트에서는 이벤트 핸들러를 바인딩하는 번거로운 작업이 필수적이었습니다.
2) 재사용성의 어려움
상태 관련 로직(Stateful Logic)을 재사용하기가 매우 어려웠습니다. 로직을 재사용하기 위해 High-Order Component(HOC)나 Render Props와 같은 복잡한 패턴을 사용해야 했는데, 이는 코드를 감싸는 컴포넌트가 많아지는 Wrapper Hell(래퍼 지옥) 현상과 낮은 가독성을 초래했습니다.
3) 생명주기 로직의 분산
componentDidMount, componentDidUpdate, componentWillUnmount 등 여러 생명주기 메서드에 관련된 로직이 흩어져 있어 코드를 이해하고 관리하기가 어려웠습니다. 예를 들어, 데이터 로딩과 이벤트 리스너를 한 번에 관리하고 싶어도 각각 다른 생명주기 메서드에 분리해서 작성해야 했습니다.
Hooks는 이러한 문제들을 함수형 컴포넌트의 장점을 유지하면서 해결하기 위해 도입되었습니다.
High-Order Component (HOC)란?
High-Order Component(HOC)는 컴포넌트 로직 재사용을 위한 React의 고급 패턴입니다. HOC는 '컴포넌트를 인자로 받아 새로운 컴포넌트를 반환하는 함수'를 의미합니다. 복잡한 로직을 HOC에 담아 여러 컴포넌트에 공통 기능을 부여할 수 있었습니다.
HOC의 작동 원리
HOC는 마치 '컴포넌트를 감싸서 추가 기능을 부여하는 래퍼(Wrapper)'와 같습니다. 예를 들어, 웹사이트의 사용자 인증 로직을 여러 페이지에 적용하고 싶을 때 HOC를 사용해 모든 컴포넌트를 감싸는 방식으로 처리할 수 있었습니다.
위 예시에서 withAuth HOC는 ProfilePage 컴포넌트를 감싸 로그인 여부를 확인하는 기능을 추가합니다.// 컴포넌트를 받아 인증 로직을 추가한 새로운 컴포넌트를 반환 function withAuth(WrappedComponent) { return function AuthenticatedComponent(props) { const isAuthenticated = checkUserAuthentication(); if (!isAuthenticated) { return <div>로그인이 필요합니다.</div>; } return <WrappedComponent {...props} />; }; } // HOC를 사용한 컴포넌트 const MyProfilePage = withAuth(ProfilePage);
HOC의 한계
HOC는 로직을 재사용하는 데 유용했지만, 여러 HOC를 중첩해서 사용하면 코드가 복잡해지는 문제가 발생했습니다.
// 여러 HOC를 중첩해서 사용한 경우 const MyProfilePage = withAuth(withLogging(withRouter(ProfilePage)));이처럼 컴포넌트를 여러 번 감싸는 구조를 Wrapper Hell이라고 합니다. 디버깅을 어렵게 하고, 컴포넌트 트리를 복잡하게 만들어 가독성을 크게 떨어뜨리는 좋지 않은 구조입니다. Hooks는 바로 이 문제를 커스텀 훅(Custom Hook)이라는 해결책으로 풀어낼 수 있습니다.
Hooks 알아보기
Hooks는 클래스 컴포넌트를 작성하지 않고도 React의 상태(state) 관리와 생명주기(lifecycle) 기능을 함수형 컴포넌트에서 사용할 수 있게 해주는 특별한 함수들입니다.
1) useState: 상태 관리의 핵심
가장 기본적인 Hook으로, 함수 컴포넌트에 상태(state)를 추가할 수 있게 합니다.
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
useState는 this.state와 this.setState를 대체하며, 상태 로직을 훨씬 간결하게 만듭니다.
2) useEffect: 함수 컴포넌트의 생명주기
useEffect는 컴포넌트가 렌더링될 때마다 특정 작업을 수행하도록 설정하는 Hook입니다. 데이터 가져오기, 구독 설정하기, DOM 직접 조작하기 등 부수 효과(Side Effects)를 처리하는 데 사용됩니다.
useEffect는 클래스 컴포넌트의 componentDidMount, componentDidUpdate, componentWillUnmount를 모두 대체할 수 있습니다.
import React, { useState, useEffect } from 'react';
function DataFetcher() {
const [data, setData] = useState(null);
// 컴포넌트가 마운트될 때 (처음 렌더링될 때) 한 번만 실행됩니다.
// 두 번째 인자로 빈 배열([])을 전달하면 부수 효과가 한 번만 발생합니다.
useEffect(() => {
fetch('https://api.example.com/data')
.then(response => response.json())
.then(json => setData(json));
}, []); // 의존성 배열
return <div>{data ? data.title : 'Loading...'}</div>;
}
3) Custom Hooks (재사용성)
Hooks의 가장 큰 장점은 로직의 재사용성을 개선할 수 있다는 점입니다. 여러 컴포넌트에서 공통으로 사용되는 상태 로직을 Custom Hook으로 만들어 재사용할 수 있습니다.
ex) 실시간으로 브라우저 창 크기를 가져오는 커스텀 훅
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,
});
}
// 이벤트 리스너를 추가하고 컴포넌트가 unmount될 때 제거
window.addEventListener('resize', handleResize);
handleResize(); // 초기 사이즈 설정
return () => window.removeEventListener('resize', handleResize);
}, []); // 빈 배열을 전달하여 컴포넌트 마운트 시 한 번만 실행
return windowSize;
}
export default useWindowSize;
import useWindowSize from './useWindowSize';
function MyComponent() {
const size = useWindowSize();
return (
<div>
<p>Window width: {size.width}</p>
<p>Window height: {size.height}</p>
</div>
);
}
useWindowSize 커스텀 훅을 사용하면, 창 크기를 가져오는 복잡한 로직을 여러 컴포넌트에서 중복 없이 깔끔하게 재사용할 수 있습니다. 이처럼 Hooks는 복잡한 패턴 없이도 컴포넌트 간 로직을 공유할 수 있게 함으로써 코드의 재사용성과 가독성을 크게 향상시켰습니다.
Hooks는 React 개발의 패러다임을 바꾼 중요한 기능이며, 이로 인해 함수형 컴포넌트가 React의 주류로 자리 잡게 되었습니다.
회고
저는 Hooks를 그저 함수형 컴포넌트에서 useState와 useEffect를 쓸 수 있게 해주는 '도구' 정도로만 생각했습니다. 하지만 이번에 Hooks가 클래스 컴포넌트의 근본적인 문제점을 해결하기 위해 탄생했다는 것을 알고 React를 바라보는 시각이 바뀌었습니다.
Hooks를 통해 얻은 가장 큰 깨달음은 컴포넌트의 역할 분리입니다. 과거에는 하나의 컴포넌트 안에 UI, 상태, 그리고 부수 효과(side effects) 로직이 모두 섞여 있었지만, 이제는 UI는 컴포넌트에, 상태 로직은 커스텀 훅에 분리하여 훨씬 깔끔하고 읽기 쉬운 코드를 작성할 수 있게 되었습니다.
이번 학습을 통해, 단순히 기능을 구현하는 것을 넘어 코드의 구조와 재사용성까지 고민하는 것이 얼마나 중요한지 깨달았습니다. 이제는 눈에 보이는 결과뿐만 아니라, 그 뒤에 숨겨진 코드의 구조와 재사용성에 대해 깊이 고민해보려고 합니다.
'📚 CS > React' 카테고리의 다른 글
| [React] useRef 알아보기 (0) | 2025.09.16 |
|---|---|
| [React] useEffect, useLayoutEffect 비교하기 (0) | 2025.09.14 |
| [React] 클래스형 컴포넌트 vs 함수형 컴포넌트 (0) | 2025.09.12 |
| [React] Virtual DOM & React Element (0) | 2025.09.10 |
| [React] React 렌더링 파이프라인 (0) | 2025.09.09 |