Programming/React

[React] React 훅(Hooks)의 정의와 특징

보간 2025. 2. 2. 20:20

훅이란? 

함수형 컴포넌트에서 React의 상태(state)와 생명주기(lifecycle) 기능을 사용할 수 있게 해주는 함수


왜 훅(Hooks)이 필요했나?

컴포넌트 내부에 캡슐화 된 로직은 다른 컴포넌트와 공유가 쉽지 않았다. 결국 로직을 재사용하기 위해 React 앱에서 Render Props, HOC와 같은 복잡한 패턴을 사용해야 했다.

 

1. 함수의 한계: 상태 관리 불가능

함수는 순수한 로직을 재사용하기에 적합하지만, 상태를 내부적으로 관리할 수 없다. 

function Counter() {
  let count = 0; // 이 변수는 함수가 호출될 때마다 초기화됨

  const increment = () => {
    count += 1;
    console.log(count);
  };

  return increment;
}

const counter = Counter();
counter(); // 1
counter(); // 1 (매번 초기화됨)

클로저를 사용하면 상태를 유지할 수 있지만, React 컴포넌트와 같은 환경에서는 다음 문제가 있다.

  • React 컴포넌트는 상태가 변경되면 재렌더링된다. 클로저를 사용하면 상태는 유지되지만, React는 이를 감지하지 못해 재렌더링이 발생하지 않는다.
  • 여러 상태를 관리하려면 클로저를 중첩해서 사용해야 하며, 코드가 복잡해진다.
  • 각 컴포넌트마다 별도의 클로저를 생성해야 한다. 

 

2. 클래스 컴포넌트의 한계: 상태 관리 가능

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  increment = () => {
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.increment}>Increment</button>
      </div>
    );
  }
}
  • 코드가 복잡하고 길어진다.
  • 로직을 재사용하기 어렵다.
  • this 바인딩 문제*가 발생할 수 있다. 

 

*클래스 컴포넌트에서 메서드가 이벤트 핸들러로 전달될 때 this가 undefined가 되는 문제. 일일이 메서드에 this를 바인딩해주거나 화살표 함수를 사용해야 한다.

 

3. 고차 컴포넌트( HOC, Higher Order Component )의 한계 : 로직 재사용

고차 컴포넌트는 컴포넌트를 감싸서 로직을 재사용한다. 하지만 코드가 복잡하고 중첩 문제가 발생할 수 있다.

function withCounter(WrappedComponent) {
  return class extends React.Component {
    state = { count: 0 };

    increment = () => {
      this.setState({ count: this.state.count + 1 }); // *
    };

    render() {
      return (
        <WrappedComponent
          count={this.state.count}
          increment={this.increment}
          {...this.props}
        />
      );
    }
  };
}

function Counter({ count, increment }) {
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

export default withCounter(Counter);
  • 로직을 재사용할 수 있지만, 컴포넌트 계층이 복잡해진다.
  • 중첩된 HOC는 디버깅을 어렵게 만든다. 

*setState: React의 내장 메서드, 주로 클래스형 컴포넌트에서 상태(state)를 업데이트할 때 사용

 

4. 렌더 프로퍼티(Render Props)의 한계 

렌더 프로퍼티는 함수를 통해 로직을 재사용한다. 하지만 코드가 복잡하고 가독성이 떨어진다.

class CounterRenderer extends React.Component {
  state = { count: 0 };

  increment = () => {
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    // props로 전달된 render 함수를 호출하고, 현재 count와 increment 메서드를 인자로 전달
    return this.props.render(this.state.count, this.increment);
  }
}

function Counter() {
  return (
    <CounterRenderer
      render={(count, increment) => (
        <div>
          <p>Count: {count}</p>
          <button onClick={increment}>Increment</button>
        </div>
      )}
    />
  );
}
  • 로직을 재사용할 수 있다.
  • 코드가 복잡하고 가독성이 떨어진다.
  • 중첩된 렌더 프로퍼티는 관리하기 어렵다. 

 

5. React Hooks: 상태 관리 + 로직 재사용

Hooks는 함수형 컴포넌트에서 상태를 관리하고 로직을 재사용할 수 있게 해준다. 코드가 간결하고 이해하기 쉽다.

// React와 useState 훅을 임포트
import React, { useState } from 'react';

// useCounter라는 커스텀 훅*을 정의
function useCounter(initialValue = 0) {
//useState 훅을 사용해 상태를 관리
  const [count, setCount] = useState(initialValue);

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

  return { count, increment };
}

function Counter() {
  const { count, increment } = useCounter();

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}
  • 상태를 관리할 수 있다.
  • 로직을 재사용할 수 있다.
  • 코드가 간결하고 가독성이 좋다.
  • 클래스 컴포넌트의 복잡성을 피할 수 있다.

* 위 코드에서 정의한 커스텀 훅: useCounter 훅은 상태 관리와 관련된 로직을 캡슐화하여 재사용 가능하게 만듬


Hooks vs 클래스 / HOC / Render Props 비교 

특징 클래스  HOC  Render Props Hooks
상태 관리 가능 가능 가능 가능
로직 재사용 어려움 가능 가능 가능
코드 복잡도 높음 중간 중간 낮음
가독성 낮음 중간 중간 높음
중첩 문제 없음 있음 있음 없음
디버깅 어려움 어려움 어려움 쉬움

 


훅 사용 규칙

  • 훅은 컴포넌트의 최상위 레벨에서만 호출해야 한다.
  • 조건문, 반복문, 중첩 함수 내에서 훅을 호출하면 안 된다.

결론

  • 클래스 컴포넌트, HOC, 렌더 프로퍼티와 비교했을 때 Hooks는 코드의 간결성, 가독성, 재사용성 측면에서 우수하다. 
  • Hooks를 사용하면 함수의 한계인 상태 관리 문제를 해결하면서도, 함수의 장점인 코드 재사용성을 극대화할 수 있다.