Updated:

주요 Hooks

useEffect

import { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  // 컴포넌트 마운트 시 또는 userId가 변경될 때 실행
  useEffect(() => {
    setLoading(true);
    
    fetch(`https://api.example.com/users/${userId}`)
      .then(response => response.json())
      .then(data => {
        setUser(data);
        setLoading(false);
      })
      .catch(error => {
        console.error('Error fetching user:', error);
        setLoading(false);
      });
      
    // 클린업 함수 (언마운트 시 또는 의존성 변경 전에 실행)
    return () => {
      console.log('Cleaning up...');
      // 예: 타이머 정리, 구독 취소 등
    };
  }, [userId]); // 의존성 배열
  
  if (loading) return <div>Loading...</div>;
  if (!user) return <div>User not found</div>;
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
    </div>
  );
}

코멘트: useEffect는 함수형 컴포넌트에서 side effects를 수행하기 위한 Hook입니다. 의존성 배열에 따라 다양한 생명주기 메서드를 대체할 수 있습니다:

  • 빈 배열([])을 전달하면 componentDidMount와 유사하게 마운트 시 한 번만 실행
  • 의존성 배열에 값을 넣으면 해당 값이 변경될 때마다 실행 (componentDidUpdate와 유사)
  • 클린업 함수를 반환하면 componentWillUnmount와 유사하게 언마운트 시 실행

useEffect 사용 패턴

// 1. 마운트 시 한 번만 실행 (componentDidMount)
useEffect(() => {
  console.log('Component mounted');
  // 초기 데이터 로딩, 이벤트 리스너 등록 등
  
  return () => {
    console.log('Component will unmount');
    // 정리 작업
  };
}, []);

// 2. 특정 값이 변경될 때마다 실행 (componentDidUpdate)
useEffect(() => {
  console.log('count changed to:', count);
}, [count]);

// 3. 모든 렌더링 후 실행 (의존성 배열 생략)
useEffect(() => {
  console.log('Component rendered');
});

코멘트: useEffect의 의존성 배열을 올바르게 관리하는 것이 중요합니다. 빈 배열을 사용하면 마운트 시에만 실행되고, 배열을 생략하면 모든 렌더링 후에 실행됩니다. 의존성 배열에 포함된 값이 변경될 때만 효과가 다시 실행됩니다.

useRef

import { useRef, useEffect } from 'react';

function AutoFocusInput() {
  // DOM 요소에 접근하기 위한 ref
  const inputRef = useRef(null);
  
  // 마운트 시 input에 포커스
  useEffect(() => {
    inputRef.current.focus();
  }, []);
  
  return <input ref={inputRef} type="text" />;
}

function Timer() {
  // 렌더링을 트리거하지 않고 값을 저장하기 위한 ref
  const timerIdRef = useRef(null);
  const [count, setCount] = useState(0);
  
  const startTimer = () => {
    if (timerIdRef.current) return; // 이미 타이머가 실행 중이면 무시
    
    timerIdRef.current = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
  };
  
  const stopTimer = () => {
    clearInterval(timerIdRef.current);
    timerIdRef.current = null;
  };
  
  // 컴포넌트 언마운트 시 타이머 정리
  useEffect(() => {
    return () => {
      if (timerIdRef.current) {
        clearInterval(timerIdRef.current);
      }
    };
  }, []);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={startTimer}>Start</button>
      <button onClick={stopTimer}>Stop</button>
    </div>
  );
}

코멘트: useRef는 두 가지 주요 용도가 있습니다:

  1. DOM 요소에 직접 접근할 때 (focus, 측정 등)
  2. 렌더링을 트리거하지 않고 값을 저장할 때 (타이머 ID, 이전 값 등)

ref.current의 변경은 렌더링을 유발하지 않으므로 렌더링 사이클과 관련 없는 값을 저장하는 데 적합합니다.

useMemo와 useCallback

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

function ExpensiveCalculation({ list, filter }) {
  // 계산 결과를 메모이제이션 (의존성이 변경될 때만 재계산)
  const filteredList = useMemo(() => {
    console.log('Filtering list...');
    return list.filter(item => item.includes(filter));
  }, [list, filter]); // list나 filter가 변경될 때만 재계산
  
  // 함수를 메모이제이션 (의존성이 변경될 때만 새 함수 생성)
  const handleItemClick = useCallback((item) => {
    console.log('Item clicked:', item);
    // 처리 로직...
  }, []); // 의존성이 없으므로 컴포넌트가 리렌더링되어도 함수는 유지됨
  
  return (
    <ul>
      {filteredList.map(item => (
        <li key={item} onClick={() => handleItemClick(item)}>
          {item}
        </li>
      ))}
    </ul>
  );
}

코멘트:

  • useMemo는 계산 비용이 많이 드는 값을 메모이제이션하여 불필요한 재계산을 방지합니다.
  • useCallback은 함수를 메모이제이션하여 불필요한 재생성을 방지합니다. 특히 자식 컴포넌트에 props로 함수를 전달할 때 유용합니다.
  • 두 Hook 모두 성능 최적화를 위한 것이므로 모든 계산이나 함수에 사용할 필요는 없습니다. 실제로 성능 문제가 있는 경우에만 사용하는 것이 좋습니다.

useReducer

import { useReducer } from 'react';

// 리듀서 함수 정의
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 };
    case 'SET':
      return { count: action.payload };
    default:
      throw new Error(`Unsupported action type: ${action.type}`);
  }
}

function Counter() {
  // [현재 상태, 액션을 디스패치하는 함수] = useReducer(리듀서 함수, 초기 상태)
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });
  
  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
      <button onClick={() => dispatch({ type: 'RESET' })}>Reset</button>
      <button onClick={() => dispatch({ type: 'SET', payload: 10 })}>Set to 10</button>
    </div>
  );
}

코멘트: useReducer는 복잡한 상태 로직을 관리할 때 useState의 대안으로 사용됩니다. Redux와 유사한 패턴으로 상태 업데이트 로직을 컴포넌트 외부로 분리할 수 있습니다. 특히 다음과 같은 경우에 유용합니다:

  • 여러 하위 값을 포함하는 복잡한 상태 객체
  • 이전 상태에 의존하는 상태 업데이트
  • 상태 업데이트 로직이 복잡한 경우

커스텀 Hooks

// 커스텀 Hook 정의
function useLocalStorage(key, initialValue) {
  // 초기 상태 설정
  const [storedValue, setStoredValue] = useState(() => {
    try {
      // localStorage에서 값 가져오기
      const item = window.localStorage.getItem(key);
      // 저장된 값이 있으면 파싱, 없으면 초기값 사용
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(error);
      return initialValue;
    }
  });
  
  // 값을 설정하고 localStorage에 저장하는 함수
  const setValue = (value) => {
    try {
      // 함수로 전달된 경우 처리
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      // 상태 업데이트
      setStoredValue(valueToStore);
      // localStorage에 저장
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(error);
    }
  };
  
  return [storedValue, setValue];
}

// 커스텀 Hook 사용
function App() {
  const [name, setName] = useLocalStorage('name', 'Guest');
  
  return (
    <div>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
        placeholder="Enter your name"
      />
      <p>Hello, {name}!</p>
    </div>
  );
}

코멘트: 커스텀 Hook은 로직을 재사용 가능한 함수로 추출하는 방법입니다. 이름이 “use”로 시작하는 함수로, 내부에서 다른 Hook을 사용할 수 있습니다. 커스텀 Hook을 사용하면 컴포넌트 간에 상태 로직을 공유할 수 있으며, 코드를 더 모듈화하고 테스트하기 쉽게 만들 수 있습니다.

Categories:

Updated:

Leave a comment