ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [React] Hook 정복 도전기_useState, useEffect, useContext, useReducer
    SPA/React.js 2024. 11. 23. 20:36

    hook은 React 16.8 버전부터 추가된 요소로, hook을 활용함으로써 기존 Class 바탕의 코드를 작성할 필요없이 상태 값과 React의 여러 기능을 사용할 수 있다.

    우리가 늘 사용하던 방식으로만 사용하게 되는 hook에 대해 기초적인 정의부터 다시 파악함으로써 정복을 도전해보자.


    1. Hook이란?

    Hook은 함수형 컴포넌트에서 React의 상태(state)와 생명주기(lifecycle)를 다룰 수 있게 해준다.

    Hook의 주요 특징은 아래와 같다.

     

    • Hook은 클래스 컴포넌트에서는 사용할 수 없고 함수형 컴포넌트에서만 사용 가능하다.
    • Hook은 컴포넌트의 최상위 레벨에서만 호출해야 하기에 조건문이나 반복문 내부에서 사용할 수 없다.
    • 모든 Hook은 use로 시작하는 이름을 가진다.

     

    2. 주요 Hook

    React에서 주로 사용되는 Hook에 대해 알아보자.

    (1) useState

    컴포넌트에 상태를 추가할 때 사용하는 hook으로, 기본적인 사용 방법은 아래와 같다.

    import React, { useState } from 'react';
    
    function Counter() {
      const [count, setCount] = useState(0); // count 초기값은 0
    
      return (
        <div>
          <p>현재 카운트: {count}</p>
          <button onClick={() => setCount(count + 1)}>증가</button>
        </div>
      );
    }

     

    useState배열을 반환하며, 첫번째 값은 상태 변수, 두번째 값은 상태를 업데이트하는 함수이다.

    일반적으로 비구조화 할당을 통해 useState의 반환 값들을 받아오며 상태를 업데이트하는 함수set$상태변수명으로 정의한다.

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

     

    상태를 업데이트할 때는 count += 1과 같이 기존 상태를 직접 수정하지 않고 setState 함수를 호출함으로써 수정한다.

    <button onClick={() => setCount(count + 1)}>증가</button>

     

    (1-1) state를 직접 변경하면 안 되는 이유

    위 이유는 React에서는 상태를 불변(immutable)하게 유지하는 것이 중요하기 때문이다.

    상태를 직접 수정하면 메모리에서 상태의 이전 값과 새 값이 동일한 참조(refenece)를 가지게 되어 React의 상태가 변경되었는지 알 수 없게 된다.

    const [count, setCount] = useState(0);
    
    function incrementWrong() {
      count++; // 상태 직접 수정
      console.log(count); // 출력은 변경되지만, React는 렌더링하지 않음
    }

     

    그렇기에 위처럼 상태를 직접 변경하면 React가 내부적으로 상태 변화 여부를 감지하지 못해 컴포넌트를 재렌더링하지 않는 문제가 발생하게 된다.

     

    (2) useEffect

    컴포넌트의 사이드 이펙트(side effect)를 관리하기 위한 Hook으로,

    데이터 가져오기, DOM 업데이트, 타이머 설정 등에 주로 사용된다.

    import React, { useState, useEffect } from 'react'; 
    
    function Timer() { 
    	const [count, setCount] = useState(0); 
        
        useEffect(() => {
        	const interval = setInterval(() => { 
            	setCount((prevCount) => prevCount + 1); 
            }, 1000); 
            
        	return () => clearInterval(interval); // 컴포넌트가 언마운트될 때 클린업 
    	}, []); // 의존성 배열 
    	
        return <p>타이머: {count}초</p>; 
    }
     

     

    useEffect에서는 두번째 인자인 의존성 배열([ ])이 중요한데 특징은 아래와 같다.

    • 빈 배열이면 컴포넌트가 처음 렌더링될 때 한번만 실행된다.
    • 배열에 이 있으면 해당 값이 변경될 때마다 실행된다.
    • 배열이 없으면 모든 렌더링 후 실행된다.

     

    (2-1) Clenup

    그 외에도 정리(cleanup) 작업을 할 때에도 useEffect를 사용할 수 있다.

    컴포넌트가 제거될 때나 특정 조건에서 변경될 때 동작하도록 할 수 있는데 이런 Cleanup의 필요성은 아래와 같다.

     

    - 메모리 누수 방지

    : 컴포넌트가 언마운드된 후에도 이벤트 리스너, 타이머, 네트워크 요청 등이 계속 실행되면 불필요한 리소스가 소모된다.

    - 중복 동작 방지

    : 의존성 배열이 변경될 때 useEffect가 재실행되는데 이전 효과를 정리하지 않으면 중복된 작업이 실행될 수 있다.

    - 예상치 못한 동작 방지

    : WebSocket 연결이나 데이터 구독을 정리하지 않으면 이전 연결이 끊기지 않아 데이터가 중복 수신될 수 있다.

     

    useEffect(() => {
      // 효과 실행
      return () => {
        // 정리 작업
      };
    }, [dependencies]);

    이런 Cleanup 작업은 useEffect 내부에 return 안에 작성함으로써 동작하게 할 수 있다.

    React는 이 반환된 정리 함수를 아래 두 시점에서 호출한다.

    • 컴포넌트가 언마운트될 때
    • 효과가 재실행되기 직전

     

    (3) useContext

    useContext는 전역 상태 관리를 도와주는 Hook으로, Context API와 함께 사용된다.

     

    useContext를 사용하면 중첩된 컴포넌트에서도 props를 전달하지 않고 Context 데이터를 바로 사용할 수 있기 때문에

    props drilling을 방지할 수 있다.

     

    Context 객체는 아래와 같이 createContex생성하고

    useContext를 사용하면 Context의 값을 구독(subscribe)할 수 있다.

     

    순서대로 해당 과정을 살펴보자면,

    createContext로 Context 객체를 생성한다.

    import React, { createContext } from 'react';
    
    // Context 생성 및 기본값 설정
    const ThemeContext = createContext('light');
     
    Provider를 사용하여 Context의 값을 하위 컴포넌트에 전달한다.
    function App() {
      return (
        <ThemeContext.Provider value="dark">
          <Toolbar />
        </ThemeContext.Provider>
      );
    }
    
    function Toolbar() {
      return <ThemeButton />;
    }

     

    useContext를 사용하여 컴포넌트에서 Context 값을 읽는다.

    function ThemeButton() {
      const theme = React.useContext(ThemeContext); // Context 값 읽기
      return <button style={{ background: theme === 'dark' ? '#333' : '#fff' }}>Change Theme</button>;
    }

     

    useContext는 보통 전역 상태 관리 시 사용하기 때문에 테마 관리, 언어 설정, 유저 인증 상태 등에 대한 데이터 공유 시 사용한다.

    그러나 값이 자주 변경되는 경우에는 성능에 주의하여 적절하게 사용해야 한다.

     

    (4) useReducer

    복잡한 상태 로직을 처리할 때 유용하며, useState와 유사하지만

    상태 변경 로직을 컴포넌트 외부로 분리하여 더 구조화된 방식으로 상태를 관리할 수 있도록 도와준다.

     

    상태 변경 로직Reducer 함수로 작성하고 상태 업데이트dispatch를 통해 실행한다. 상태 변경 로직을 정의하는 함수

    const [state, dispatch] = useReducer(reducer, initialState);
    • reducer: 상태 변경 로직을 정의하는 함수
    • initialState: 상태의 초기값
    • state: 현재 상태
    • dispatch: 액션(action)을 트리거하는 함수

     

    (4-1) Reducer 함수

    Reducer 함수는 상태와 액션을 받아서 새로운 상태를 반환하는 순수 함수이다.

    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 };
        default:
          throw new Error('Unhandled action type');
      }
    }

     

    (4-2) initialState

    initialState상태의 초기값을 정의한다.

    const initialState = { count: 0 };

     

    (4-3) action

    action은 상태를 업데이트할 때 사용하는 객체로,

    보통 type 속성을 포함하며, 추가적으로 필요한 데이터를 전달한다.

    const action = { type: 'increment' };

     

    useReducer를 사용하는 방법은 아래와 같다.

    import React, { useReducer } from 'react';
    
    // Reducer 함수
    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 };
        default:
          throw new Error('Unhandled action type');
      }
    }
    
    // 초기 상태
    const initialState = { count: 0 };
    
    function Counter() {
      const [state, dispatch] = useReducer(reducer, initialState);
    
      return (
        <div>
          <p>Count: {state.count}</p>
          <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
          <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
          <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
        </div>
      );
    }
    
    export default Counter;

     

    useState와 useReducer는 상태 관리를 한다는 점에서 유사하지만

    구체적인 차이는 다음과 같다.

    특징 useState useReducer
    사용 간단성 단순 상태 관리에 적합 복잡한 상태 관리에 적합
    업데이트 로직 컴포넌트 내부에 상태 로직 포함 상태 로직을 Reducer로 분리
    상태 변경 방식 setState 함수로 직접 변경 dispatch로 액션 전달
    선호 상황 독립적인 단일 상태 관리 연관된 다중 상태 관리

     

    3. 커스텀 Hook

    기존에 제작되어있는 Hook 외에도 반복적인 로직을 재사용하기 위해 커스텀 Hook을 만들 수 있다.

    import React, { useState, useEffect } from 'react';
    
    function useFetch(url) { 
    	const [data, setData] = useState(null);
        useEffect(() => { 
        	fetch(url) 
            .then((response) => response.json()) 
            .then((data) => setData(data)); 
        }, [url]);
        
        return data;     
    } 
    
    function App() { 
    	const data = useFetch('https://jsonplaceholder.typicode.com/posts'); 
        
        return ( 
        	<div> 
            	<h1>Posts</h1>
                {data 
                ? data.map((post) => <p key={post.id}>{post.title}</p>) 
                : 'Loading...'} 
            </div> 
        );
    }

     


     

    useEffect, useContext와 같은 기본 Hook부터 useReducer, 커스텀 Hook까지 다양한 기능을 적절히 활용하면 생산성과 코드의 재사용성을 높일 수 있다.

     

    적절한 때에 알맞은 Hook을 사용하여 효율적인 코드를 작성할 수 있도록 하자.

Designed by Tistory.