[React] Hook이 대체 뭐야? (useState, useEffect)
리액트 훅(Hook)이란?
저번에 컴포넌트에 대해서 간단하게 설명했을 때 안한 얘기가 하나 있다.
컴포넌트는 크게 두 가지 유형으로 나뉜다.
클래스형 컴포넌트와 함수형 컴포넌트이다.
클래스형 컴포넌트는 예전부터 사용하던 방식으로, 이름처럼 ‘클래스’ 라는 문법으로 만든다. 기능은 많지만 코드가 조금 길고 복잡하게 느껴질 수 있다.
함수형 컴포넌트는 비교적 최근에 등장해 대세가 된 방식이다. ‘함수’ 형태로 훨씬 간결하고 직관적으로 컴포넌트를 만들 수 있다.
이 함수 컴포넌트는 마치 ‘주문을 받으면 특정 모양의 레고 블록(UI)을 만들어서 내놓는 간단한 기계’ 와 같다.
그런데 이 간단한 기계에는 몇 가지 치명적인 단점이 있다.
- 기억력이 없다. (Stateless) : 버튼을 몇 번 눌렀는지, 사용자가 입력창에 무엇을 썼는지 등을 전혀 기억하지 못한다.
- 외부 세상과 소통할 수 없다. : 만들어진 후에 외부 서버에서 데이터를 가져오거나, 시간이 지나면 특정 행동을 하도록 설정할 수가 없다.
이런 부족한 점을 보완하기 위해, 함수 컴포넌트에 ‘특별한 능력’을 갈고리(Hook)처럼 걸어서 사용할 수 있게 만든 것이 바로 리액트 훅이다.
- useState 훅 : 컴포넌트에 ‘기억력’ 을 부여하는 갈고리
- useEffect 훅 : ‘외부 세상과 소통하는 능력’ (Side Effects 처리)을 부여하는 갈고리
왜 훅(Hook)이 등장했을까?
훅이 등장하기 전, ‘기억력(state)’이나 ‘외부와의 소통(lifecyclce)’ 같은 기능들은 오직 클래스 컴포넌트만 사용할 수 있었다. 하지만 클래스 컴포넌트는 다음과 같은 문제점들을 가지고 있었다.
- 로직 재사용의 어려움 (Wrapper Hell)
- 컴포넌트 간에 상태 관련 로직을 재사용하기가 매우 까다롭다.이를 해결하기 위해 HOC(Higher-Order Components)나 Render Props 같은 복잡한 패턴을 사용해야 하는데, 이로 인해 컴포넌트 트리가 불필요하게 깊어지고, 코드 추적이 어려워지는 ‘Wrapper Hell’ 현상이 발생했다.
- 거대하고 복잡해지는 컴포넌트
- 하나의 클래스 컴포넌트 안에 관련 없는 여러 로직들이 componentDidMonut , componentDidUpdate 같은 생명주기 메소드에 섞어 들어갔다. 예를 들어, componentDidMount 안에는 API 데이터 호출, 이벤트 리스너 등록 등 서로 다른 기능의 코드들이 한데 뒤섞여 있었다. 반대로, 하나의 기능을 위한 코드는 componentDidMount 와 componentWillUnmount 에 흩어져 있어 코드를 이해하고 유지보수하기 어려웠다.
- 클래스의 장벽
- JavaScript의 this 키워드는 많은 개발자들을 혼란스럽게 만들었고, 이벤트 핸들러를 매번 바인딩하는 등 불필요한 코드가 많았다.
훅(Hook)은 이 문제들을 해결하기 위해 등장했다. 훅을 통해 개발자들은 함수 컴포넌트 내에서 상태 로직을 간결하게 재사용하고, 관련 있는 코드들을 하나로 묶어 관리하며, 복잡한 클래스 문법 없이도 React의 모든 기능을 활용할 수 있게 되었다.
핵심 훅(Hook) 알아보기: useState 와 useEffect
대표적인 훅인 useState 와 useEffect 에 대해서 알아보자.
useState : 상태(State)를 위한 훅
useState 는 함수 컴포넌트에 ‘기억력’, 즉, 상태(State)를 추가해주는 훅이다.
import { useState } from 'react';
function Counter() {
// useState 사용: count라는 '상태'를 생성. 초기값은 0
// count: 현재 상태값 (읽기 전용) (getter)
// setCount: 이 상태를 업데이트할 수 있는 함수 (setter)
const [countㄷ, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count+1) }>
Click me
</button>
</div>
);
}
- setCount 함수가 호출되어 count 상태가 변경되면, React는 이 Counter 컴포넌트가 리렌더링되는 것을 인지한다. 그리고 컴포넌트를 리렌더링하여 화면의 숫자를 업데이트한다.
useEffect 이 사이드 이펙트(Side Effect)를 위한 훅
useEffect 는 컴포넌트가 렌더링된 이후에 처리해야 하는 사이드 이펙트를 수행하기 위한 훅이다. 여기서 사이드 이펙트란, 렌더링 결과에 직접적인 영향을 주지 않는 모든 부수적인 작업들을 의미한다.
- 사이드 이펙트 예시:
- API를 통해 서버에서 데이터 가져오기 (Data Fetching)
- 외부 라이브러리 사용
- DOM 직접 조작 (예: 문서 제목 변경)
- 타이머 설정(setTimeout, setInterval)
- 이벤트 리스너 구독 및 해제
import { useState, useEffect } from 'react';
function DocumentTitleChanger() {
const [count, setCount] = useState(0);
// useEffect 사용
// 이 컴포넌트는 '문서의 제목을 바꾸는' 부수적인 임무를 가짐
useEffect( () => {
// 렌더링이 완료된 후에 이 코드가 실행됨
document.title = `You clicked ${count} times`;
}, [count]); // '의존성 배열': count가 변경될 때만 이 effect를 실행하라
return (
<button onClick={ () => setCount(count+1)}>
Update Title ({count})
</button>
);
}
- 의존성 배열: useEffect 의 두 번째 인자인 [count] 는 매우 중요하다. 이것은 React에게 “이 useEffect 안의 코드는 count 라는 값이 변경될 때만 다시 실행해줘!” 라고 알려주는 역할을 한다.
- [count] : count 가 바뀔 때마다 실행된다.
- [] (빈 배열) : 최초 렌더링 직후 단 한 번만 실행된다. (API 최초 호출 등에 사용)
- 생략 시 : 매 리렌더링마다 실행된다. (주의해서 사용해야함)
훅의 규칙 (Rules of Hooks)
훅은 매우 편리하지만, 올바르게 동작하기 위해 반드시 지켜야 할 두 가지 규칙이 있다.
- 최상위 레벨에서만 호출해야 한다.
- 반복문(for), 조건문(if), 중첩된 함수 안에서 훅을 호출해서는 안된다. 훅은 항상 컴포넌트의 최상위 레벨에서, 동일한 순서로 호출되어야 한다. 이는 React가 여러 useState 와 useEffect 호출 사이에서 올바른 상태를 유지하는 방식과 관련이 있다.
- 오직 리액트 함수 내에서만 호출해야 한다.
- 리액트 함수 컴포넌트나 커스텀 훅 안에서만 훅을 호출할 수 있다. 일반적인 JavaScript 함수 안에서는 호출할 수 없다.