React

[React] Hooks

yo09 2025. 1. 12. 16:29

React Hooks란?

React Hooks는 함수형 컴포넌트에서도 React의 상태 관리 및 생명주기(lifecycle) 기능을 사용할 수 있게 해주는 기능이다. 이전에는 클래스 컴포넌트에서만 가능했던 이러한 기능들을 함수형 컴포넌트에서도 활용할 수 있도록 만들어졌다. React 16.8에서 처음 도입되었다.

 

[주요 장점]

  1. 코드 재사용성: 반복적인 로직을 간결하게 처리 가능.
  2. 가독성 향상: 함수형 컴포넌트에서의 간단한 구문 덕분에 코드가 더 명확함.
  3. 복잡한 로직 처리 가능: 상태 관리와 비동기 작업 등을 간단하게 구현할 수 있음.

 

주요 Hooks와 활용법

1. useState - 상태 관리

useState는 컴포넌트의 상태를 관리하는 데 사용된다. 상태는 컴포넌트에서 데이터의 현재 값을 의미하며, 변경되면 컴포넌트가 다시 렌더링된다.

const [state, setState] = useState(initialState)
  • initialState: 상태의 초기값. 숫자, 문자열, 배열, 객체 등 어떤 값이든 설정 가능.
  • state: 현재 상태 값.
  • setState: 상태 값을 업데이트하는 함수. 이 함수가 호출되면 컴포넌트가 다시 렌더링된다.

 

활용 예제

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0); // 초기값은 0

  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  );
}

export default Counter;
  • useState(0)은 초기 상태를 0으로 설정한다.
  • setCount를 호출하면 count 값이 변경되고, 컴포넌트가 다시 렌더링된다.

 


 

 

2. useEffect - 사이드 이펙트 관리

useEffect는 사이드 이펙트(side effect)를 처리하는 데 사용된다. 예를 들어, 데이터를 가져오거나(AJAX 통신), 이벤트 리스너를 추가/제거하거나, DOM을 조작하는 등의 작업이 포함된다. 외부 데이터 연동 시, 일단 초기 렌더링을 하여 화면을 띄우고 로딩 처리 이후 다시 부분 업데이트를 할 때 유용하다.

useEffect(() => {
  // 실행할 코드
  return () => {
    // 정리(cleanup) 작업
  };
}, [dependencies]); // 의존성 배열
  • dependencies: 이 배열 안의 값이 변경될 때만 useEffect가 실행된다. 빈 배열([])을 사용하면 한 번만 실행된다.

 

활용 예제

import { useEffect, useState } from "react";

function TimerComponent() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    // 1. 타이머 설정: 1초마다 count를 증가시키는 함수 실행
    const timer = setInterval(() => {
      setCount((prevCount) => prevCount + 1); // 이전 상태값에 1 추가
    }, 1000);

    // 2. clean-up 함수: 컴포넌트가 사라질 때 실행
    return () => {
      clearInterval(timer); // 타이머를 정리하여 메모리 누수를 방지
    };
  }, []); // 빈 배열: 초기 렌더링 시 딱 한 번 실행

  return <div>타이머: {count}초</div>;
}

export default TimerComponent;
  • 초기 렌더링 시 실행
    • useEffect는 컴포넌트가 화면에 나타난 후(mount) 실행.
    • setInterval을 사용해 1초마다 count 상태를 증가시키는 타이머를 설정.
  • 타이머 정리 (clean-up)
    • return 뒤의 함수는 컴포넌트가 화면에서 제거될 때(unmount) 실행.
    • 이 함수에서 clearInterval(timer)로 타이머를 중지해 불필요한 타이머 실행을 방지.
  • 의존성 배열 ([])
    • useEffect의 두 번째 인자로 []를 전달하면, useEffect는 컴포넌트가 처음 렌더링될 때 한 번만 실행.

 

 

1. 기본 동작: 렌더링마다 실행

useEffect는 컴포넌트가 렌더링될 때마다 실행된다. 이는 의존성 배열을 생략한 경우에 해당한다,

예시

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

function App() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log('useEffect 실행!'); // 상태 변경으로 인해 렌더링될 때마다 실행
  });

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={() => setCount(count + 1)}>Increase</button>
    </div>
  );
}

결과

  • 버튼을 클릭하면 count가 변경되고 컴포넌트가 다시 렌더링.
  • useEffect는 렌더링될 때마다 실행된다. (console.log 출력)

 

2. 의존성 배열([]) 사용: 실행 시점 제어

useEffect에 의존성 배열을 추가하면, 배열에 지정한 값이 변경될 때만 실행되도록 제어할 수 있다.

예시

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

function App() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');

  useEffect(() => {
    console.log('count가 변경되었어요!');
  }, [count]); // count가 변경될 때만 실행

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={() => setCount(count + 1)}>Increase Count</button>

      <h1>Name: {name}</h1>
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
    </div>
  );
}

결과

  • count가 변경될 때만 useEffect가 실행된다.
  • name이 변경되더라도 useEffect는 실행되지 않는다.

 

3. 빈 배열([]): 최초 1회만 실행

의존성 배열이 빈 배열([])인 경우, 컴포넌트가 마운트(처음 렌더링) 될 때만 실행된다. 이후 상태가 변경되더라도 실행되지 않는다.

예시

import React, { useEffect } from 'react';

function App() {
  useEffect(() => {
    console.log('컴포넌트가 마운트되었습니다!');
  }, []); // 빈 배열: 최초 1회만 실행

  return <h1>Welcome to the App!</h1>;
}

결과

  • 컴포넌트가 처음 렌더링될 때만 console.log가 출력된다.
  • 상태 변경이나 재렌더링이 발생해도 실행되지 않는다.

 


 

3. useContext - 전역 상태 관리

useContext는 React의 Context API와 함께 사용되어 컴포넌트 트리에서 데이터를 쉽게 전달한다.

useContext는 Context를 구독하고, 해당 값에 접근하는 데 사용하는 훅이다. 이를 통해 Prop Drilling(중간 단계의 컴포넌트를 통해 데이터를 전달하는 과정)을 피할 수 있다.

 

 

파일 구성

src/
├── App.js
├── ThemeContext.js
├── components/
│   ├── Toolbar.js
│   ├── ThemeButton.js

1. ThemeContext.js

Context를 생성하는 파일이다. 전역 상태를 관리하기 위한 컨텍스트를 정의한다.

// ThemeContext.js
import { createContext } from "react";

// 1. ThemeContext 생성
const ThemeContext = createContext();

export default ThemeContext;

2. App.js

애플리케이션의 최상위 컴포넌트이다. ThemeContext.Provider를 사용해 Context 값을 하위 컴포넌트로 전달한다.

// App.js
import React, { useState } from "react";
import ThemeContext from "./ThemeContext"; // ThemeContext 가져오기
import Toolbar from "./components/Toolbar"; // Toolbar 컴포넌트 가져오기

function App() {
  const [theme, setTheme] = useState("light"); // 전역 상태로 사용할 theme와 setTheme

  return (
    // ThemeContext.Provider로 하위 컴포넌트에 값 전달
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

export default App;

3. components/Toolbar.js

중간 단계 컴포넌트이다. 실제로는 데이터를 사용하지 않지만, 하위 컴포넌트로 전달하는 역할을 한다.

// components/Toolbar.js
import React from "react";
import ThemeButton from "./ThemeButton"; // ThemeButton 컴포넌트 가져오기

function Toolbar() {
  return (
    <div>
      <ThemeButton />
    </div>
  );
}

export default Toolbar;

4. components/ThemeButton.js

실제 useContext를 사용하여 전역 상태를 소비하고 UI를 렌더링하는 컴포넌트이다.

// components/ThemeButton.js
import React, { useContext } from "react";
import ThemeContext from "../ThemeContext"; // ThemeContext 가져오기

function ThemeButton() {
  const { theme, setTheme } = useContext(ThemeContext); // useContext로 ThemeContext 값 가져오기

  return (
    <button
      onClick={() => setTheme(theme === "light" ? "dark" : "light")}
      style={{
        backgroundColor: theme === "light" ? "#fff" : "#333",
        color: theme === "light" ? "#000" : "#fff",
      }}
    >
      현재 테마: {theme}
    </button>
  );
}

export default ThemeButton;

실행 흐름 정리

  1. App.js에서 ThemeContext.Provider를 사용해 전역 상태(theme와 setTheme)를 전달한다.
  2. Toolbar.js는 중간 단계의 컴포넌트로, 실제로는 아무 작업도 하지 않지만 계층 구조를 유지한다.
  3. ThemeButton.js에서 useContext로 전역 상태를 가져와 UI를 렌더링하고, 버튼 클릭 시 테마 상태를 변경한다.

파일 간 관계도

js
  └── ThemeContext.Provider (전역 상태 제공)
        └── Toolbar.js
              └── ThemeButton.js (전역 상태 소비)

 

4. useReducer - 복잡한 상태 로직 관리

  • 리액트의 상태 관리 훅으로, 복잡한 상태 로직을 보다 체계적으로 관리할 수 있도록 도와준다.
  • useState와 비슷하지만, 상태 변경 로직을 Reducer 함수로 분리하여 여러 상태 변경을 더 명확하게 처리할 수 있다.
  • Redux와 같은 Flux 아키텍처의 핵심 개념인 Reducer와 동일한 원리를 사용한다.
const [state, dispatch] = useReducer(reducer, initialState);

useReducer의 반환값

  1. state: 현재 상태를 나타냄.
  2. dispatch: 상태를 업데이트하기 위해 호출하는 함수.

 

코드 예제

(1) counterReducer

  • 상태 업데이트 로직을 정의한 함수.
  • 예를 들어, 증가/감소/초기화 같은 동작을 설정한다.
export function counterReducer(state, action) {
  switch (action.type) {
    case "increment":
      return { count: state.count + 1 };
    case "decrement":
      return { count: state.count - 1 };
    case "reset":
      return { count: 0 };
    default:
      throw new Error(`Unhandled action type: ${action.type}`);
  }
}

(2) initialState

  • 상태의 초기값.
  • 예를 들어, 초기 카운트값은 0으로 설정된다.
export const initialState = { count: 0 };

(3) useReducer(counterReducer, initialState)

  • useReducer를 호출하면서 두 가지를 전달한다.
    1. counterReducer: 상태 변경 로직을 정의한 함수.
    2. initialState: 상태의 초기값.
  • 이 호출의 결과는 [state, dispatch]라는 배열 형태로 반환된다.
    • state: 현재 상태 (예: { count: 0 }).
    • dispatch: 상태를 업데이트하기 위한 함수.

 

예시: 버튼 클릭으로 카운트 상태를 변경하는 동작

function App() {
  const [state, dispatch] = useReducer(counterReducer, initialState);

  return (
    <div>
      <p>현재 카운트: {state.count}</p>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
      <button onClick={() => dispatch({ type: "reset" })}>Reset</button>
    </div>
  );
}

 

실행 흐름

  1. dispatch 호출
    • 버튼을 클릭하면 dispatch가 호출된다.
    • 예: dispatch({ type: "increment" }).
  2. Reducer 함수 실행
    • counterReducer 함수가 실행된다.
    • action.type이 "increment"이면 state.count + 1로 상태가 변경된다.
  3. state 업데이트
    • useReducer는 새로운 상태를 반환하고 컴포넌트를 다시 렌더링한다.

 

왜 사용하는지?

  • useState는 간단한 상태 관리에 적합하지만, 상태가 복잡하거나 여러 액션(증가, 감소, 초기화 등)을 처리할 때는 관리가 어려워진다.
  • useReducer는 상태 관리 로직을 명확히 분리하여 코드의 가독성을 높이고 유지보수를 쉽게 만든다.

 


 

5. useRef - DOM 요소 및 값의 참조 관리

useRef는 React에서 참조(Reference)를 관리하는 Hook이다. 주로 DOM 요소값을 참조할 때 사용되며, 컴포넌트가 리렌더링 되어도 값이 유지된다. useRef는 상태 변화와 달리 리렌더링을 발생시키지 않아서 성능 최적화나 DOM 요소에 직접 접근할 때 유용하다.

const myRef = useRef(initialValue);
  • myRef는 ref 객체로, current 속성에 값을 저장한다.
  • initialValue는 ref의 초기값이다. 주로 null로 설정되며, DOM을 참조하려면 null로 시작한다.

 

코드 예제 - DOM 요소 참조

import React, { useRef } from 'react';

export default function FocusInput() {
  const inputRef = useRef(null);

  const handleFocus = () => {
    // 버튼 클릭 시 input 요소에 포커스
    inputRef.current.focus();
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={handleFocus}>Focus the input</button>
    </div>
  );
}

 

 


 

6. useCallback - memoization된 함수 생성

useCallback은 주로 함수의 재생성을 방지하여 렌더링 성능을 개선하는 데 사용된다. useCallback을 사용하면 특정 함수가 컴포넌트가 리렌더링될 때마다 새로 생성되지 않고, 기존 인스턴스를 재사용하게 된다. 이렇게 하면 불필요한 함수 호출이나 컴포넌트 리렌더링을 방지할 수 있다.

const memoizedCallback = useCallback(callback, [dependencies]);
  • callback: 실행할 함수를 지정한다. 이 함수는 컴포넌트가 렌더링된 후에 호출된다.
  • dependencies (옵션): 배열 안에 넣은 값들이 변경될 때만 callback 함수가 재실행된다. 의존성 배열을 빈 배열 [ ]로 두면 컴포넌트가 처음 렌더링될 때만 실행되고, 이후에는 실행되지 않는다.

 

코드 예제

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

export default function Counter() {
  const [count, setCount] = useState(0);

  // 의존성 배열 제거
  const increment = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
  }, []); // 빈 배열이므로 함수는 처음 한 번만 생성됨

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increase</button>
    </div>
  );
}
  • 컴포넌트가 렌더링되면 increment 함수가 한 번 생성된다.
  • 버튼을 클릭할 때마다 increment 함수는 setCount를 호출한다.
  • setCount 내부에서 최신 상태(prevCount)를 사용하여 count를 업데이트한다.
  • 상태가 변경되면 컴포넌트는 리렌더링되지만, increment 함수는 재생성되지 않는다.