리액트를 통해 개발을 하다 보면 useState, useEffect 같은 단어들을 자주 접하셨을 텐데요,
이들은 모두 '훅(Hook)'이라는 리액트의 기능을 담당하는 함수들입니다.
훅(Hook)이 등장하기 전: 클래스 컴포넌트의 복잡함
훅이 등장하기 전, 리액트에서 '상태(state)'를 관리하거나 '생명주기(lifecycle)' 기능을 사용하려면 반드시 클래스 컴포넌트(Class Component) 사용해야 했습니다.
importReact, { Component } from'react';
classCounterClassextendsComponent {
constructor(props) {
super(props);
// state는 constructor 안에서 this.state로 초기화
this.state= {
count:0
};
}
// componentDidMount: 컴포넌트가 처음 마운트된 후 실행
componentDidMount() {
document.title=`클릭 수: ${this.state.count}`;
}
// componentDidUpdate: 컴포넌트가 업데이트된 후 실행
componentDidUpdate() {
document.title=`클릭 수: ${this.state.count}`;
}
// 이벤트 핸들러에서 state 변경은 this.setState 사용
handleCount= () => {
this.setState({ count:this.state.count+1 });
};
render() {
return (
<div>
<p>클릭 수: {this.state.count}</p>
<buttononClick={this.handleCount}>
클릭하세요
</button>
</div>
);
}
}
해당 클래스 컴포넌트는 아래와 같은 단점을 가지고 있습니다.
1. 많은 코드 사용 : constructor, super(props), this.state, this.setState, this.handleCount 등 불필요해 보이는 코드가 많았습니다.
2. this 키워드의 사용: 자바스크립트의 this 바인딩 문제로 인해 개발자들이 어려움을 겪었습니다. 3. 로직 재사용의 어려움: 여러 컴포넌트에서 동일한 로직(예: API 호출, 구독)을 재사용하기 위해 HOC(Higher-Order Components)나 Render Props 패턴을 사용해야 했는데, 이는 코드의 중첩을 심화시켜 가독성을 해치기도 했습니다.
그럼 Hook 이란 무엇일까요?
함수형 컴포넌트 안에서 state와 생명주기(lifecycle) 같은 리액트의 기능들을 사용할 수 있게 해주는 함수들의 모음입니다.
위의 설명된 글 내용처럼 복잡함과 어려움을 해결하기 위해 리액트 16.8 버전부터 hook이 도입되었습니다.
Hook을 사용하기 위해서는 지켜야할 규칙이 있습니다.
1. 최상위 레벨에서만 훅 호출: if 문, for 반복문, while 반복문, 중첩된 함수 내부에서는 훅을 호출하면 안 됩니다. 항상 컴포넌트 함수의 가장 바깥쪽(최상위 레벨)에서만 호출해야 합니다.
// ❌ 잘못된 사용 예시
functionMyComponent({ condition }) {
if (condition) {
const [value, setValue] =useState(''); // if문 안에서 useState 호출
}
// ...
}
// ✅ 올바른 사용 예시
functionMyComponent({ condition }) {
const [value, setValue] =useState(''); // 항상 최상위 레벨에서 useState 호출
const [count, setCount] =useState(0); // 컴포넌트 안에서 useState 호출
// ...
}
// ❌ 잘못된 사용 예시 (일반 JS 함수)
functionmyUtilityFunction() {
const [count, setCount] =useState(0); // 컴포넌트 밖에서 useState 호출
}
대표적인 기본 훅 예제
useState - 컴포넌트의 상태 관리
importReact, { useState } from'react';
functionCounter() {
// 1. useState(초기값)를 호출하여 상태 변수와 상태 변경 함수를 얻습니다.
// [현재상태값, 상태를변경하는함수] 형태의 배열을 반환합니다.
const [count, setCount] =useState(0); // count라는 상태는 0으로 초기화됩니다.
constincrement= () => {
// setCount 함수를 사용하여 상태를 업데이트합니다.
// 이전 상태값을 기반으로 새로운 상태 값을 계산할 때는 함수 형태로 전달하는 것이 안전합니다.
setCount(prevCount=>prevCount+1);
};
return (
<div>
<p>현재 숫자: {count}</p>
<buttononClick={increment}>
숫자 증가시키기
</button>
</div>
);
}
exportdefaultCounter;
useState(0)를 호출하여 count라는 상태 변수를 만들고, 초기값을 0으로 설정했습니다. setCount 함수를 호출하면 count 상태가 업데이트되고, 리액트는 자동으로 컴포넌트를 다시 렌더링하여 변경된 count 값을 화면에 보여줍니다.
예제:
useEffect - 부수 효과(Side Effect) 처리
useEffect는 컴포넌트가 렌더링된 후 또는 특정 상태가 변경된 후에 어떤 작업을 수행하도록 설정할 때 사용합니다. 흔히 클래스 컴포넌트의 componentDidMount, componentDidUpdate, componentWillUnmount 생명주기 메서드의 역할을 통합하여 처리합니다.
importReact, { useState, useEffect } from'react';
functionDocumentTitleChanger() {
const [count, setCount] =useState(0);
// 1. useEffect(콜백함수, 의존성배열)
// 콜백함수: 컴포넌트가 렌더링될 때마다 실행될 로직
// 의존성배열: 이 배열 안의 값이 변경될 때마다 콜백함수를 다시 실행합니다.
// 빈 배열([])을 전달하면 컴포넌트가 처음 마운트될 때 딱 한 번만 실행됩니다.
useEffect(() => {
// 컴포넌트가 렌더링된 후에 실행될 로직
document.title=`클릭 횟수: ${count}`;
console.log('useEffect 실행됨!');
// 클린업 함수: 컴포넌트가 언마운트되거나 effect가 다시 실행되기 전에 실행될 로직
// 주로 이벤트 리스너 제거, 구독 해제 등에 사용됩니다.
return () => {
console.log('cleanup 실행됨!');
// 예시: document.title을 이전 상태로 복구하거나 불필요한 작업을 정리할 수 있습니다.
};
}, [count]); // count 값이 변경될 때마다 effect를 다시 실행하도록 의존성 배열에 count 추가
constincrement= () => {
setCount(prevCount=>prevCount+1);
};
return (
<div>
<p>클릭 횟수: {count}</p>
<buttononClick={increment}>
클릭하여 카운트 증가
</button>
</div>
);
}
exportdefaultDocumentTitleChanger;
예제
useEffect(() => { ... }, [count])는 count 값이 변경될 때마다 콜백 함수 내의 document.title을 업데이트합니다. 콜백 함수 안에서 return () => { ... } 형태로 **클린업 함수(cleanup function)**를 반환할 수 있습니다. 이 함수는 컴포넌트가 언마운트될 때나, useEffect의 의존성 배열에 있는 값이 변경되어 effect가 다시 실행되기 전에 호출됩니다. 이벤트 리스너 제거 등에 유용합니다. 만약 의존성 배열을 빈 배열 []로 전달하면, useEffect는 컴포넌트가 처음 마운트될 때 딱 한 번만 실행되고 그 이후로는 실행되지 않습니다. (클래스 컴포넌트의 componentDidMount와 유사한 용도)