React

[React] Hook 함수형 컴포넌트

teamnova 2025. 6. 30. 10:58
728x90

안녕하세요!

리액트를 통해 개발을 하다 보면 useState, useEffect 같은 단어들을 자주 접하셨을 텐데요,

이들은 모두 '훅(Hook)'이라는 리액트의 기능을 담당하는 함수들입니다.

 

훅(Hook)이 등장하기 전: 클래스 컴포넌트의 복잡함

훅이 등장하기 전, 리액트에서 '상태(state)'를 관리하거나 '생명주기(lifecycle)' 기능을 사용하려면 반드시 클래스 컴포넌트(Class Component) 사용해야 했습니다.

 

import React, { Component } from 'react';

class CounterClass extends Component {
  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>
        <button onClick={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 반복문, 중첩된 함수 내부에서는 훅을 호출하면 안 됩니다. 항상 컴포넌트 함수의 가장 바깥쪽(최상위 레벨)에서만 호출해야 합니다.

 

// ❌ 잘못된 사용 예시
function MyComponent({ condition }) {
  if (condition) {
    const [value, setValue] = useState(''); // if문 안에서 useState 호출
  }
  // ...
}

// ✅ 올바른 사용 예시
function MyComponent({ condition }) {
  const [value, setValue] = useState(''); // 항상 최상위 레벨에서 useState 호출
  if (condition) {
    // ... 훅이 아닌 일반적인 로직은 조건문 안에 있어도 괜찮습니다.
  }
  // ...
}

 

 

2. 리액트 함수에서만 훅 호출: 훅은 리액트 함수형 컴포넌트나 커스텀 훅(Custom Hook) 안에서만 호출해야 합니다. 

일반 자바스크립트 함수 안에서는 훅이 작동하지 않습니다.

 

// ✅ 올바른 사용 예시
function MyComponent() {
  const [count, setCount] = useState(0); // 컴포넌트 안에서 useState 호출
  // ...
}

// ❌ 잘못된 사용 예시 (일반 JS 함수)
function myUtilityFunction() {
  const [count, setCount] = useState(0); // 컴포넌트 밖에서 useState 호출
}

 

대표적인 기본 훅 예제

useState - 컴포넌트의 상태 관리

import React, { useState } from 'react';

function Counter() {
  // 1. useState(초기값)를 호출하여 상태 변수와 상태 변경 함수를 얻습니다.
  //    [현재상태값, 상태를변경하는함수] 형태의 배열을 반환합니다.
  const [count, setCount] = useState(0); // count라는 상태는 0으로 초기화됩니다.

  const increment = () => {
    // setCount 함수를 사용하여 상태를 업데이트합니다.
    // 이전 상태값을 기반으로 새로운 상태 값을 계산할 때는 함수 형태로 전달하는 것이 안전합니다.
    setCount(prevCount => prevCount + 1);
  };

  return (
    <div>
      <p>현재 숫자: {count}</p>
      <button onClick={increment}>
        숫자 증가시키기
      </button>
    </div>
  );
}

export default Counter;

 

useState(0)를 호출하여 count라는 상태 변수를 만들고, 초기값을 0으로 설정했습니다. setCount 함수를 호출하면 count 상태가 업데이트되고, 리액트는 자동으로 컴포넌트를 다시 렌더링하여 변경된 count 값을 화면에 보여줍니다.

 

예제:

 

useEffect - 부수 효과(Side Effect) 처리

useEffect는 컴포넌트가 렌더링된 후 또는 특정 상태가 변경된 후에 어떤 작업을 수행하도록 설정할 때 사용합니다. 흔히 클래스 컴포넌트의 componentDidMount, componentDidUpdate, componentWillUnmount 생명주기 메서드의 역할을 통합하여 처리합니다.

 

import React, { useState, useEffect } from 'react';

function DocumentTitleChanger() {
  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 추가

  const increment = () => {
    setCount(prevCount => prevCount + 1);
  };

  return (
    <div>
      <p>클릭 횟수: {count}</p>
      <button onClick={increment}>
        클릭하여 카운트 증가
      </button>
    </div>
  );
}

export default DocumentTitleChanger;

 

예제

 

useEffect(() => { ... }, [count])는 count 값이 변경될 때마다 콜백 함수 내의 document.title을 업데이트합니다.
콜백 함수 안에서 return () => { ... } 형태로 **클린업 함수(cleanup function)**를 반환할 수 있습니다. 이 함수는 컴포넌트가 언마운트될 때나, useEffect의 의존성 배열에 있는 값이 변경되어 effect가 다시 실행되기 전에 호출됩니다. 이벤트 리스너 제거 등에 유용합니다.
만약 의존성 배열을 빈 배열 []로 전달하면, useEffect는 컴포넌트가 처음 마운트될 때 딱 한 번만 실행되고 그 이후로는 실행되지 않습니다. (클래스 컴포넌트의 componentDidMount와 유사한 용도)