Programming/react

[react] 함수 컴포넌트 Hooks 기능

x-coder 2025. 4. 13. 08:30

Hello. #{Somebody}

리액트 핵심, 훅(Hook)! 🚀

 

오늘은 리액트 함수 컴포넌트의 핵심이자, 개발을 훨씬 즐겁게 만들어주는 마법 같은 친구,

바로 훅(Hook)에 대해 알아보려 합니다. 😊

 


훅(Hook), 왜 갑자기 나타났을까요? 🤔

예전 리액트에서는 상태(state)를 관리하거나 생명주기(lifecycle) 기능을 사용하려면 꼭 클래스 컴포넌트를 사용해야 했습니다.

하지만 클래스 컴포넌트는 this 키워드의 혼란스러움, 코드 재사용의 어려움 등 몇 가지 불편한 점들이 있었죠.

이런 불편함을 해소하고, 더 간결하고 직관적인 방법으로 함수 컴포넌트에서도 리액트의 강력한 기능들을 사용할 수 있도록 등장한 것이 바로 훅(Hook)입니다.

훅 덕분에 우리는 함수 컴포넌트를 사용해서 상태 관리, 생명주기 관리 등을 할 수 있게 되었습니다.

 

상태 관리의 첫걸음, useState 🔢

useState는 컴포넌트가 기억해야 할 값, 즉 상태(state)를 함수 컴포넌트 안에서 사용할 수 있게 해줍니다.

useState(초기값)을 호출하면 [현재 상태값, 상태 업데이트 함수] 배열을 반환합니다.

상태 업데이트 함수가 호출되면 컴포넌트는 새로운 상태값으로 다시 렌더링됩니다.

import React, { useState } from 'react';

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

  return (
    <div>
      <p>클릭 횟수: {count}</p>
      {/* setCount를 호출하여 count 상태 업데이트 */}
      <button onClick={() => setCount(count + 1)}>클릭하세요!</button>
    </div>
  );
}
export default BasicCounter;
가장 기본적이면서도 가장 많이 사용되는 훅

 

부수 효과 처리는 내게 맡겨! useEffect ✨

컴포넌트가 렌더링된 후에 처리해야 하는 작업들, 예를 들어 API 호출, 구독 설정, 직접적인 DOM 조작 등을 부수 효과(Side Effect)라고 합니다. useEffect는 이러한 부수 효과들을 함수 컴포넌트에서 다룰 수 있게 해줍니다.

useEffect(콜백함수, [의존성 배열]) 형태로 사용하며, 의존성 배열에 따라 콜백함수의 실행 시점이 결정됩니다.

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

function DataFetcher() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    console.log('데이터 가져오기 시작!');
    // 예시: 실제로는 fetch나 axios 등을 사용합니다.
    setTimeout(() => { // 비동기 작업 시뮬레이션
      setData({ message: '짜잔! 데이터 도착!' });
      setLoading(false);
      console.log('데이터 가져오기 완료!');
    }, 1500);

    // 클린업(Clean-up) 함수: 컴포넌트가 사라지기 전이나, 다음 effect 실행 전에 호출됩니다.
    return () => {
      console.log('컴포넌트 언마운트 또는 업데이트 전 정리 작업');
      // 예: 구독 해제, 타이머 제거 등
    };
  }, []); // 의존성 배열이 비어있으므로, 마운트 시 딱 한 번만 실행됩니다.

  if (loading) {
    return <p>데이터를 불러오는 중...</p>;
  }

  return <p>{data ? data.message : '데이터 없음'}</p>;
}
export default DataFetcher;
의존성 배열을 어떻게 관리하는지가 useEffect 활용의 핵심

 

"Prop Drilling" 이제 그만! useContext 🌐

깊게 중첩된 컴포넌트 구조에서 최상위 컴포넌트의 상태를 최하위 컴포넌트까지 전달해야 할 때,

중간의 모든 컴포넌트가 props를 계속해서 넘겨줘야 하는 번거로움을 "Prop Drilling"이라고 합니다.

useContext는 React.createContext로 생성된 Context 객체와 함께 사용되어, 이런 과정을 건너뛰고 필요한 컴포넌트에서 직접 전역적인 데이터에 접근할 수 있게 해줍니다. 마치 단체 메시지 방처럼, 필요한 사람에게 바로 정보를 전달하는 것과 같습니다.

import React, { useState, useContext, createContext } from 'react';

// 1. Context 생성
const UserContext = createContext(null);

function App() {
  const [user, setUser] = useState({ name: '손님', isLoggedIn: false });

  // 2. Provider로 감싸고 value 전달
  return (
    <UserContext.Provider value={{ user, setUser }}>
      <Header />
      <UserProfile />
      {/* ... 다른 컴포넌트들 */}
    </UserContext.Provider>
  );
}

function Header() {
  // 3. useContext로 Context 값 사용
  const { user } = useContext(UserContext);
  return <header>안녕하세요, {user.name}님!</header>;
}

function UserProfile() {
  const { user, setUser } = useContext(UserContext);

  const handleLogin = () => setUser({ name: '김리액트', isLoggedIn: true });
  const handleLogout = () => setUser({ name: '손님', isLoggedIn: false });

  return (
    <div>
      <p>로그인 상태: {user.isLoggedIn ? '로그인됨' : '로그아웃됨'}</p>
      {user.isLoggedIn ? (
        <button onClick={handleLogout}>로그아웃</button>
      ) : (
        <button onClick={handleLogin}>로그인</button>
      )}
    </div>
  );
}
export default App;

 

DOM 접근과 값 보존, useRef 📌

useRef는 두 가지 주요 사용 사례를 가집니다. 첫째, 특정 DOM 노드에 직접 접근해야 할 때 사용합니다(예: input 포커스). 둘째, 값이 변경되어도 컴포넌트를 리렌더링시키고 싶지 않은 '변수'(인스턴스 변수와 유사)를 관리할 때 사용합니다. useRef()는 .current 속성을 가진 객체를 반환하며, 이 속성을 통해 값이나 DOM 요소에 접근합니다.

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

function FocusInputAndTrackRenders() {
  const inputRef = useRef(null);
  const renderCountRef = useRef(0); // 리렌더링과 상관없이 렌더 횟수 추적
  const [text, setText] = useState(''); // 입력값 추적 (리렌더링 유발)

  useEffect(() => {
    // 마운트 시 포커스
    inputRef.current.focus();
  }, []);

  useEffect(() => {
    // 매 렌더링 시 카운트 증가 (하지만 이 증가가 리렌더링을 유발하진 않음)
    renderCountRef.current = renderCountRef.current + 1;
    console.log(`컴포넌트 렌더링 횟수: ${renderCountRef.current}`);
  }); // 의존성 배열 없으므로 매 렌더링 시 실행

  return (
    <div>
      <input
        ref={inputRef}
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="여기에 입력..."
      />
      <p>입력된 텍스트: {text}</p>
      <p>(이 컴포넌트는 {renderCountRef.current}번 렌더링되었습니다.)</p>
      <button onClick={() => inputRef.current.focus()}>입력창 포커스</button>
    </div>
  );
}
export default FocusInputAndTrackRenders;

 

비싼 계산 결과는 저장해두자! useMemo 🧠

컴포넌트가 렌더링될 때마다 복잡하고 시간이 오래 걸리는 계산을 반복적으로 수행해야 한다면 성능에 좋지 않겠죠?

useMemo는 바로 이럴 때 사용하는 훅입니다. 계산 결과값을 메모이제이션(memoization, 기억)해두고, 의존성 배열의 값이 변경되었을 때만 다시 계산을 수행합니다. 결과적으로 불필요한 계산을 건너뛰어 성능을 최적화할 수 있습니다.

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

// 매우 복잡하고 오래 걸리는 계산 함수라고 가정
const calculateComplexValue = (num) => {
  console.log('엄청 복잡한 계산 중...');
  let result = 0;
  for (let i = 0; i < num * 10000000; i++) {
    result += Math.sqrt(i); // 의미 없는 복잡한 연산
  }
  return result;
};

function MemoExample() {
  const [number, setNumber] = useState(1);
  const [text, setText] = useState(''); // 이 상태가 변해도 위 계산은 다시 하지 않도록!

  // useMemo 사용: number가 변경될 때만 calculateComplexValue 함수 실행
  const complexValue = useMemo(() => {
    return calculateComplexValue(number);
  }, [number]); // number를 의존성 배열에 추가

  return (
    <div>
      <input
        type="number"
        value={number}
        onChange={(e) => setNumber(parseInt(e.target.value, 10))}
      />
      <p>복잡한 계산 결과: {complexValue}</p>
      <hr />
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="이 입력은 계산과 상관없어요"
      />
      <p>입력된 텍스트: {text}</p>
    </div>
  );
}
export default MemoExample;
useMemo는 값을 기억하는 훅

 

함수도 기억할 수 있어! useCallback useCallback 🗣️

useCallback은 useMemo와 비슷하지만, 이 아닌 함수 자체를 메모이제이션합니다.

이게 왜 필요할까요? 자바스크립트에서는 컴포넌트가 리렌더링될 때마다 내부에 선언된 함수들이 새로 생성됩니다.

만약 이 함수를 자식 컴포넌트의 prop으로 전달하고, 그 자식 컴포넌트가 React.memo 등으로 최적화되어 있다면, 부모가 리렌더링될 때마다 prop으로 받은 함수가 계속 새로운 참조값을 가지게 되어 불필요한 자식 컴포넌트 리렌더링을 유발할 수 있습니다.

useCallback은 의존성 배열의 값이 변경되지 않는 한, 동일한 함수 참조를 반환하여 이러한 문제를 방지합니다.

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

// React.memo로 감싸서 props가 변경되지 않으면 리렌더링되지 않도록 최적화
const ChildButton = React.memo(({ onClick, label }) => {
  console.log(`${label} 버튼 렌더링됨!`);
  return <button onClick={onClick}>{label}</button>;
});

function CallbackExample() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  // useCallback 사용: count1이 변경될 때만 새로운 함수 생성
  const incrementCount1 = useCallback(() => {
    setCount1(c => c + 1);
  }, []); // count1에 의존하지 않으므로 빈 배열 가능 (함수형 업데이트 사용)
          // 만약 count1을 직접 사용했다면 [count1] 필요

  // useCallback 미사용: CallbackExample이 리렌더링될 때마다 항상 새로 생성됨
  const incrementCount2 = () => {
    setCount2(c => c + 1);
  };

  console.log('CallbackExample 부모 렌더링됨');

  return (
    <div>
      <p>카운트 1: {count1}</p>
      <ChildButton onClick={incrementCount1} label="카운트 1 증가 (useCallback)" />
      <hr />
      <p>카운트 2: {count2}</p>
      <ChildButton onClick={incrementCount2} label="카운트 2 증가 (일반 함수)" />
    </div>
  );
}
// 개발자 도구 콘솔을 열고 각 버튼을 클릭해보세요.
// '카운트 2 증가' 버튼 관련 상태(count2)만 변경해도, '카운트 1 증가' 버튼은 리렌더링되지 않지만,
// '카운트 1 증가' 버튼 관련 상태(count1)만 변경해도, 일반 함수로 전달된 '카운트 2 증가' 버튼은 리렌더링됩니다.
// 이는 incrementCount2 함수가 매번 새로 생성되기 때문입니다.

export default CallbackExample;
useCallback(fn, deps)는 useMemo(() => fn, deps)와 동일하게 동작합니다.
즉, 함수를 위한 useMemo의 편의 문법이라고 생각할 수 있습니다.

 

복잡한 상태 로직은 useReducer에게! 🔄

useState만으로 관리하기에는 상태 로직이 너무 복잡해지거나, 여러 상태 값이 서로 연관되어 업데이트되어야 할 때 useReducer가 좋은 대안이 될 수 있습니다.

useReducer는 리덕스(Redux) 같은 상태 관리 라이브러리에서 영감을 받은 훅으로, 상태 업데이트 로직을 컴포넌트 외부의 리듀서(reducer) 함수로 분리하여 관리합니다. seReducer(reducer 함수, 초기 상태)를 호출하면 [현재 상태, dispatch 함수] 배열을 반환합니다. 상태를 업데이트하려면 dispatch 함수에 액션(action) 객체를 전달하고, 리듀서 함수는 현재 상태와 액션을 받아 새로운 상태를 반환하는 방식으로 동작합니다.

import React, { useReducer } from 'react';

// 1. 초기 상태 정의
const initialState = { count: 0 };

// 2. 리듀서 함수 정의: 상태와 액션을 받아 새로운 상태 반환
function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    case 'RESET':
      return { count: 0 };
    case 'ADD_AMOUNT':
      return { count: state.count + action.payload }; // 액션에 데이터를 담아 전달 가능
    default:
      throw new Error('알 수 없는 액션 타입입니다.');
  }
}

function ReducerCounter() {
  // 3. useReducer 사용: 리듀서와 초기 상태 전달
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <p>현재 카운트: {state.count}</p>
      {/* 4. dispatch 함수로 액션 전달하여 상태 업데이트 요청 */}
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>증가</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>감소</button>
      <button onClick={() => dispatch({ type: 'RESET' })}>초기화</button>
      <button onClick={() => dispatch({ type: 'ADD_AMOUNT', payload: 5 })}>+5</button>
    </div>
  );
}
export default ReducerCounter;
useReducer는 상태 업데이트 로직을 한 곳에 모아 관리하므로 코드의 가독성과 유지보수성이 향상되고,
복잡한 시나리오에서 상태 업데이트 과정을 예측하기 쉽게 만들어줍니다.

 

 

리액트의 주요 훅들을 다시 한번 표로 정리해 보았습니다.📋

useState 함수 컴포넌트의 상태 관리 상태 변경 시 컴포넌트 리렌더링 유발 [상태값, 상태설정함수] (배열) 간단한 상태 관리, 독립적인 상태값
useEffect 부수 효과(Side Effect) 처리 의존성 배열로 실행 시점 제어, 클린업 함수 제공 undefined API 호출, 구독, 타이머, DOM 조작 등 비동기/부수 작업
useContext Prop Drilling 없이 전역적 데이터 공유 Provider와 함께 사용, Context 값 구독 Context의 value prop 전역 상태(테마, 사용자 정보 등) 공유
useRef DOM 요소 접근, 리렌더링 없는 값 저장 .current 속성, 값 변경 시 리렌더링 없음 { current: ... } (객체) DOM 직접 제어, 렌더링과 무관한 값 저장
useMemo 값(계산 결과) 메모이제이션 의존성 변경 시에만 값 다시 계산, 성능 최적화 메모이제이션된 값 복잡한 계산 결과 재사용, 불필요한 계산 방지
useCallback 함수 메모이제이션 의존성 변경 시에만 함수 재생성, 자식 컴포넌트 최적화 메모이제이션된 함수 자식 컴포넌트에 콜백 전달 시 불필요한 리렌더링 방지
useReducer 복잡한 상태 로직 관리 (useState 대안) 상태 업데이트 로직 분리(reducer), 중앙 집중 관리 [상태값, dispatch 함수] (배열) 여러 상태가 연관된 복잡한 로직, 상태 관리 중앙화

 


 

훅들을 적재적소에 활용한다면, 여러분의 리액트 코드는 더욱 간결해지고, 성능은 향상되며, 유지보수는 용이해질 것입니다.

 

음에는 어떤 훅을 언제 써야 할지 조금 헷갈릴 수도 있지만,

중요한 것은 '왜' 이 훅을 사용하는지 이해하고 문제 상황에 가장 적합한 훅을 선택하는 능력을 기르는 것입니다.

 

그럼 즐거운 개발 되시길 바랍니다!🚀😉 

 

 

 

 

Bye. #{Somebody}