
0. 리액트 훅이란?
리액트가 제공하는 미리 만들어둔 도구로,
함수형 컴포넌트에서 상태(state)와 리액트의 다양한 기능을
간단한 함수 호출로 사용할 수 있게 해주는 함수입니다.
훅(hook)이 왜 생겨났을까?
리액트 16.8 이전에는 클래스 컴포넌트에서만 상태(state)나 생명주기(lifecycle) 기능을 사용할 수 있었습니다.
이 방식에는 다음과 같은 한계가 존재했습니다.
- this 바인딩 문제 : 클래스 메서드에서 this가 의도치 않게 바뀌어 버리는 문제가 잦음
- 상태 로직이 분산 : 하나의 기능과 관련된 코드가 componentDidMount, componentDidUpdate, componentWillUnmount 등에 흩어짐
- 재사용성 부족 : 클래스 간 상태 로직을 재사용하려면 HOC, render props 등 복잡한 패턴 필요
상태 관리 예시 : 클래스 방식
import React, { Component } from 'react';
class Counter extends Component {
// 1. state : 클래스의 속성으로 정의
state = {
count: 0
};
// 2. 상태 변경 : setState 메서드 사용
increase = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
return (
<div>
<p>카운트: {this.state.count}</p>
<button onClick={this.increase}>+1</button>
</div>
);
}
}
- this.state로 읽고, this.setState()로 변경
- this 바인딩 문제와 긴 코드로 인한 가독성 저하 발생
그렇다면 훅 등장 이후 어떻게 바뀌었을까요 ?
import React, { useState } from 'react';
function Counter() {
// count : 현재 상태 값
// setCount : 상태 변경 함수
const [count, setCount] = useState(0); // 초기값 0
const increase = () => setCount(count + 1);
return (
<div>
<p>카운트 : {count}</p>
<button onClick={increase}>+1</button>
</div>
);
}
export default Counter;
- this 없이 간결한 코드
- 관련 있는 로직을 하나의 함수 안에서 묶어 관리 용이
| 훅 사용 전 (클래스) | 훅 사용 후 (함수형) |
| state = { count: 0} | useState(0) |
| this.state.count | count |
| this.setState({ count: ... }) | setCount(...) |
| this 바인딩 필요 | this 대신 화살표 함수 사용 |
훅을 사용하면 클래스 문법 없이도 상태 관리가 가능하고,
짧고 직관적인 코드로 로직을 재사용하고 구성할 수 있습니다.
1. useState란?
리액트 훅 중 가장 기본이 되는 훅인 useState는
함수형 컴포넌트에서 변하는 값(상태)을 저장하고 관리하는 기능을 제공합니다.
값이 변하면 컴포넌트가 다시 렌더링 되어 UI에 반영됩니다.
기본 문법
const [state, setState] = useState(initialValue);
- state : 현재 상태 값 (읽기 전용)
- setState : 상태를 변경하는 함수 (호출 시 렌더링 발생)
- initialValue : 초깃값 (첫 렌더에서만 사용)
2. 기본 동작 원리
(1) 상태 저장
: state는 컴포넌트 내부의 리액트 상태 저장소에 기록됩니다.
(2) 상태 변경
: setState를 호출하면 리액트가 변경 예약을 걸고, 불필요한 렌더링을 방지하기 위해 최적화합니다.
(3) UI 갱신
: 다음 렌더링 주기에 새로운 state 값이 UI에 반영됩니다.
3. 예시
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0); // 초기값 0
const increase = () => setCount(count + 1);
return (
<div>
<p>카운트: {count}</p>
<button onClick={increase}>+1</button>
</div>
);
}

4. 알아야 할 핵심 개념
4-1. 상태 변경은 비동기적
setCount(count + 1);
console.log(count); // 여전히 이전 값일 수 있음
이유 : 리액트는 성능 최적화를 위해서 여러 상태 변경을 묶어서(batching) 처리
setCount(prev => prev + 1);
해결 : 이전 값 기반으로 변경 시 업데이터 함수 사용
4-2. 불변성 유지
- 객체/배열 상태를 다룰 때는 원본을 수정하지 말고 새로운 값을 만들어야 합니다.
// 잘못된 예시
todos.push('리액트공부');
setTodos(todos);
// 올바른 예시
setTodos(prev => [...prev, '리액트공부']);
이유 : 리액트는 참조 비교(얕은 비교, shallow compare)로 변경 여부를 판단합니다.
-> 참조가 바뀌어야 감지 가능 !
4-3. 초기값은 첫 렌더에만 적용
- useState(initialValue)의 초기값은 컴포넌트 첫 실행 시만 반영됩니다.
- props에 따라 상태 초기화하려면 useEffect로 처리합니다.
4-4. 지연 초기화(Lazy Initialization)
- 초기값 계산이 무거울 때, 함수 형태로 전달하면 첫 렌더에서만 실행합니다.
const [data, setData] = useState(() => heavyCalculation());
-> 첫 렌더에서만 실행되고 이후에는 재계산하지 않음
4-5. 값 덮어쓰기
- setState는 병합이 아니라 교체
- 객체 상태를 업데이트할 때는 기존 값 복사 후 변경
setUser(prev => ({ ...prev, age: 25 }));
5. 자주 쓰는 패턴 알아보기
5-1. 토글
const [isOpen, setIsOpen] = useState(false);
const toggle = () => setIsOpen(prev => !prev);
5-2. 폼 입력
const [form, setForm] = useState({ name: '', email: '' });
const handleChange = e => {
const { name, value } = e.target;
setForm(prev => ({ ...prev, [name]: value }));
};
5-3. 배열 관리
const [items, setItems] = useState([]);
setItems(prev => [...prev, newItem]); // 추가
setItems(prev => prev.filter(item => item.id !== id)); // 삭제
마무리
마지막으로 놓치기 쉬운 부분을 한번 더 살펴보며 마무리하겠습니다.
1. 훅은 컴포넌트 최상위에서만 호출 (조건문이나 반복문 안 X)
2. 동일 값 setState 시 렌더링이 되지 않는다 -> 리액트는 참조 비교로 최적화하기 때문
3. 배열, 객체 상태를 한꺼번에 묶기보다 분리해서 관리 -> 불필요한 렌더를 방지하기 위해서
'FRONTEND > React' 카테고리의 다른 글
| 리액트 훅 | useCallback에 대해 알아보기 (8) | 2025.08.12 |
|---|---|
| 리액트 훅 | useEffect에 대해 알아보기 (5) | 2025.08.11 |
| 부모 요소 패딩 값에 영향을 받지 않고 전체 너비 구분선 그리기 (0) | 2025.04.04 |
| 모던 리액트 Deep Dive | 1.1 자바스크립트와 동등 비교 (0) | 2025.03.09 |
| 리액트 훅(React Hook) 이해하기 (1) | 2025.01.19 |