[React] State와 Hooks(useState, useEffect...)



01


State

State는 컴포넌트의 상태를 나타내며, 변화 가능한 값을 관리하는 데 사용됩니다.

State는 직접 수정할 수 없고, setState를 통해 수정해야 합니다.

// 잘못된 예시
this.state = { name: '박서방' };

// 정상적인 사용법
this.setState({ name: '박서방' });

 

상태가 변경되면 컴포넌트가 다시 리렌더링 됩니다. 따라서 렌더링과 데이터 흐름에 필요한 값만 state에 포함시켜야 성능 저하를 막을 수 있습니다.

함수형 컴포넌트에서는 React 16.8부터 useState Hook을 사용하여 상태를 관리할 수 있습니다.


02

Lifecycle (생명주기)

React 컴포넌트는 생성, 업데이트, 제거라는 세 가지 주요 생명주기를 가집니다.

이 과정을 통해 컴포넌트의 동작 방식을 알 수 있습니다.

  • 컴포넌트가 생성될 때 필요한 메모리를 할당하고, 컴포넌트가 사라질 때는 소멸자를 통해 메모리를 반환합니다.
  • 이는 메모리 누수를 방지하고, 효율적인 성능을 유지하기 위해 필수적인 과정입니다.

라이프 사이클은 메모리 관리를 최적화하는 데 중요한 역할을 합니다.

3가지 생명주기

  1. 생성될 때 (Mount / 출생)
    - 컴포넌트가 처음 DOM에 렌더링 될 때 발생합니다. 이 시점에서 초기화 작업이 수행됩니다.

  2. 업데이트될 때 (Update / 인생)
    -  컴포넌트가 화면에 렌더링 된 후, props나 state가 변경되면 업데이트가 발생합니다. (리렌더링 될 때를 의미)

  3. 제거할 때 (UnMount / 사망)
    -  컴포넌트가 화면에서 사라지는 순간, 즉 DOM에서 제거될 때 발생합니다. (렌더링에서 제외되는 순간을 의미)
    -  이때는 메모리 정리, 타이머 제거, 이벤트 삭제, api호출을 취소 등 필요한 정리 작업을 할 수 있습니다.

라이플사이클 제어

컴포넌트의 라이프 사이클의 단계별로 컴포넌트들이 각각 다른 작업을 수행하도록 만드는 것

 


03

Hook

Hook함수형 컴포넌트에서 상태와 생명주기 기능을 사용할 수 있게 해주는 함수입니다.

함수형 컴포넌트에서 useState, useEffect 등 다양한 Hook을 사용하여 상태를 관리하거나, 컴포넌트의 생명주기를 처리할 수 있습니다.

* 이름 앞에 use를 붙여 hook이라는 것을 나타내 주어야 합니다.

Hook 사용 규칙 

1. Hook은 반드시 함수 컴포넌트이거나 커스텀훅(Custom Hook) 내부에서만 호출이 가능합니다.

 

2. 조건문이나 반복문 내에서는 사용할 수 없습니다.

// 조건문 내에서 사용하는 경우 (잘못된 예)
function App() {
	if(num === 1) {
		const [number, setNumber] = useState(0);
	}
}

// 반복문 내에서 사용하는 경우 (잘못된 예)
function App() {
  while (num < 9) {
   const [number, setNumber] = useState(0);
  }
}

 

3. 커스텀훅(Custom Hook)을 직접 만들어 사용가능합니다.

: 함수 키워드 앞에 use만 붙여주면 내부에서 커스텀훅으로 인식합니다.

 

4. ESLint 플러그인으로 규칙을 강제할 수 있습니다.

// ESLint 설정 파일
{
  "plugins": [
    "react-hooks"
  ],
  "rules": {
    "react-hooks/rules-of-hooks": "error",  // Hook 규칙 강제
    "react-hooks/exhaustive-deps": "warn"   // 의존성 배열 체크
  }
}

useState()

useState는 컴포넌트에서 상태를 관리할 수 있도록 도와주는 Hook입니다.

const [state, setState] = useState(initialState);

 

  • initialState: 상태의 초기 값
  • state: 현재 상태 값
  • setState: 상태 값을 업데이트하는 함수

 

<예시코드 01>

import React, { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>클릭 횟수 : {count}</p>
      <button onClick={() => setCount(count + 1)}> Click! </button>
    </div>
  );
}

실행결과

<예시코드 02>

import { useState } from 'react';
import './App.css';

function App() {
  // useState를 사용하여 text 상태를 선언하고, 초기값을 빈 문자열로 설정합니다.
  const [text, setText] = useState('');

  function handleChange(event) {
    // input창에 사용자가 입력한 값을 받아오기
    // event : onChange
    setText(event.target.value);
  }

  return (
    <div className="container">
      <input type="text" value={text} onChange={handleChange} />
      <p>입력한 텍스트: {text}</p>
    </div>
  );
}

export default App;

실행 결과

 

*useState를 사용하여 값이 변경될 때마다 리 렌더링이 발생하나 성능에 큰 영향을 줄 정도는 아니라고 합니다.

 

" 사용 시 주의사항 "

  • 상태 값을 직접 수정하지 말고, 반드시 setState 함수로 상태를 업데이트해야 합니다.
  • useState는 컴포넌트 최상단에서만 호출해야 합니다. 조건문, 반복문, 중첩 함수 내에서 호출할 수 없습니다
// 잘못된 예시: 조건문 내에서 사용
function App() {
  if (num === 1) {
    const [number, setNumber] = useState(0);  // X
  }
}
// 잘못된 예시: 반복문 내에서 사용
function App() {
  while (num < 9) {
    const [number, setNumber] = useState(0);  // X
  }
}
  • 클래스형 컴포넌트에서는 사용할 수 없습니다.
  • useState를 사용할 때는 반드시 import 문을 통해 호출해야 합니다.
import { useState } from 'react'; // 반드시 중괄호로 감싸서 호출

useEffect()

리액트 컴포넌트의 부수 효과(Side Effect)를 제어하기 위해 사용하는 Hook입니다.

useEffect(() => {
  // 부수 효과 코드
}, [dependencies]);  // 의존성 배열
  • 의존성 배열에 있는 값이 변경될 때만 실행됩니다.

부수 효과(side effects)

부수 효과(side effects)는 부작용이라는 뜻으로 프로그래밍에서는 개발자가 의도치 않은 코드가 실행되면서 버그가 나타나면 사이드 이펙트가 발생했다고 표현합니다.

(리액트에서는 "부수적인 효과" , "파생되는 효과" 정도로 해석 가능합니다.)

 

부수 효과는 컴포넌트의 렌더링 결과에 영향을 주지 않으면서 수행해야 하는 작업입니다.

예를 들어 데이터 가져오기, 구독 설정, DOM 조작 등이 이에 해당합니다.

 

<사용 예시>

  • 의존성 배열에 값을 넣는 경우: 해당 값이 변경될 때마다 실행됩니다.
useEffect(() => { 이펙트 함수 }, [name]);
  • 의존성 배열이 빈 배열인 경우: 컴포넌트가 마운트 될 때와 언마운트될 때 한 번만 실행됩니다.
useEffect(() => { 이펙트 함수 }, []);
  • 의존성 배열을 생략하는 경우: 컴포넌트가 업데이트될 때마다 호출됩니다.
useEffect(이펙트 함수);

 

<예시코드 01>

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

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

  // componentDidMount, componentDidUpdate와 비슷하게 작동합니다.
  useEffect(() => {
    // 브라우저 API를 사용해서 document의 title을 업데이트 합니다.
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>클릭 횟수 : {count}</p>
      <button onClick={() => setCount(count + 1)}>Click!</button>
    </div>
  );
}

export default App;

 

 

<예시코드 02>

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

function App() {
  const [name, setName] = useState('김서방'); // 이름 상태
  const [inputValue, setInputValue] = useState(''); // input값을 관리하는 상태

  useEffect(() => {
    console.log('이름이 변경되었습니다.');
  }, [name]); // 의존성 배열에 name 값 : name값이 변경될 때에만 console.log가 실행됩니다.

  return (
    <div className="container">
      <input
        className="input"
        type="text"
        onChange={(e) => setInputValue(e.target.value)}>
      </input>
      <p className="name">{name}</p>
      <button
        onClick={() => {
          setName(inputValue);
        }}
      >
        이름 변경
      </button>
    </div>
  );
}

export default App;

실행결과

 

" 사용 시 주의사항 "

  • useEffect반드시 컴포넌트 최상단에서 호출해야 하며, 조건문이나 반복문 안에서 사용할 수 없습니다.
// 잘못된 예시
if (num === 1) {
  useEffect(() => { /* 수행할 작업 */ }, []);
}
  • useEffect는 반드시 import 문을 사용하여 호출해야 합니다.
import { useEffect } from 'react';

 

cleanup 함수

 

  • useEffect 안에서 return을 사용하면 컴포넌트가 언마운트될 때 정리 작업(cleanup)을 할 수 있습니다.
  • 이벤트 리스너나 타이머를 설정했다면, 컴포넌트가 사라질 때 이를 정리(cleanup) 해야 합니다.

 

useEffect(() => {
  // 부수 효과 작업
  return () => {
    // cleanup 작업 (예: 이벤트 리스너 삭제)
  };
}, [dependencies]);

useCallback()

useCallback은 메모이제이션을 통해 함수 재생성을 방지하는 Hook입니다.

매 렌더링마다 새로 생성되는 함수를 특정 조건에서만 재생성하도록 합니다.

const memoizedCallback = useCallback(() => {
  // 코드
}, [dependencies]);

 

메모이제이션(memoization)란?

메모이제이션은 함수 호출 결과를 캐시 하여 동일한 입력값으로 호출될 때 이전에 계산된 결과를 반환하는 기법입니다. 이로 인해 불필요한 함수 호출을 줄이고, 성능 최적화를 할 수 있습니다.

 

< 예시 코드 01>
import { useState, useCallback } from 'react';

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

  // useCallback을 사용하여 메모이제이션된 클릭 핸들러 함수
  const handleClick = useCallback(() => {
    setCount(count + 1); // 이전 상태를 사용하여 새로운 상태를 설정합니다.
  }, [count]); // count가 변경될 때마다 handleClick 함수가 다시 생성됩니다.

  return (
    <div className="container">
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment +</button>
    </div>
  );
}

export default Counter;

실행결과

" 사용 시 주의사항 "

  • useCallback은 컴포넌트 최상위 레벨에서만 호출할 수 있습니다.
  • 반복문이나 조건문 내에서 호출할 수 없습니다.

useMemo()

useMemo는 불필요한 계산을 방지하고 성능을 최적화하는 Hook입니다.

(이름 그대로 Memo를 해두었다가 나중에 다시 쓴다 로 생각하면 좋습니다.)

const memoizedValue = useMemo(() => calculateValue, [dependencies]);

 

 

<예시코드 01>

import { useMemo } from 'react';

function TodoList({ todosList, activeTab }) {
  const filteredTodos = useMemo(() => filterTodos(todosList, activeTab), [todosList, activeTab]);

  return (
    <div>
      {filteredTodos.map(todo => <div key={todo.id}>{todo.text}</div>)}
    </div>
  );
}

 

" 사용 시 주의사항 "

  • useMemo는 컴포넌트 최상위 레벨에서만 호출할 수 있습니다.
  • Strict Mode에서는 React가 실수로 발생한 오류를 찾기 위해 계산 함수를 두 번 호출합니다.
    이는 개발 환경에서만 동작합니다.

useReducer()

useReducer는 useState보다 복잡한 상태를 관리할 때 사용하는 Hook입니다.

주로 상태와 상태를 업데이트하는 함수를 결합하여 상태를 관리합니다.

 

(모든 useState는 useReducer로 대체 가능 => 상태 관리 코드를 컴포넌트 외부로 분리할 수 있음)
(Redux와 유사한 개념이지만, 더 간단하고 로컬 상태에만 적용됩니다.)

 

여기서 dispatch는 발송하다,급송하다의 뜻으로 상태변화가 있어야한다는 사실을 알리는,발송하는 함수입니다.

const [state, dispatch] = useReducer(reducer, initialState);

state → 현재 상태 (데이터)
dispatch → 상태를 변경하는 함수 (dispatch(action)으로 실행)
reducer(state, action) → 상태를 업데이트하는 함수
initialState → useReducer의 초기 상태 (ex: itemData)

 

function reducer(state, action) {
  switch (action.type) {
    case "CREATE":
      return [...state, action.data];  // 새로운 데이터 추가
    case "EDIT":
      return state.map((item) =>
        item.id === action.targetId
          ? { ...item, ...action.data }  // 특정 항목 수정
          : item
      );
    case "DELETE":
      return state.filter((item) => item.id !== action.targetId);  // 특정 항목 삭제
    default:
      return state; // 기본적으로 기존 상태 유지
  }
}

  state → 현재 상태 (예: itemData)
  action → dispatch()를 통해 전달된 객체 (예: { type: "DELETE", targetId: 2 })

 

dispatch({ type: "DELETE", targetId: 2 });

✔ dispatch()를 호출하면 reducer()가 실행됨
✔ reducer()는 action.type을 확인하여 state를 업데이트
✔ 업데이트된 state가 useReducer의 state로 저장됨
✔ UI가 새로운 state를 반영하여 다시 렌더링됨

 

 

 

🔥 이해하기 쉽게 정리하면?

코드 요소 역할
useReducer(reducer, itemData) 상태(state)를 itemData로 초기화
state 현재 상태 (데이터)
dispatch(action) 상태를 변경하는 요청을 보냄
reducer(state, action) 요청(action)에 따라 새로운 상태를 반환
action.type 어떤 작업을 수행할지 결정 (ex: "DELETE", "EDIT")
action.data 새로운 데이터 (ex: { date: 1234, content: "새 일기" })
action.targetId 수정/삭제할 특정 항목의 ID

 

 

<예시코드 01>

import { useReducer } from 'react';

// reducer : 변환기
// -> 상태를 실제로 변화시키는 역할
function reducer(state, action) {
	console.log(state,action);
    //결과: {type: "INCREASE",data: 1}
    
    //반환되는 값이 새로운 state값으로 반영됨
  	switch(action.type){
    	case "INCREASE": return state + action.data;
        case "DECREASE": return state - action.data;
        default: return state;
}

function Counter() {
  //dispatch : 발송하다, 급송하다
  // -> 상태 변화가 있어야 한다는 사실을 알리는,발송하는 함수
  const [state, dispatch] = useReducer(reducer, 0);
  
  const onClickPlus = () => {
  	// 인수: 상태가 어떻게 변화되길 원하는지
    //-> 액션 객체
        dispatch({
            type: "INCREASE",
            data: 1
        })
	}
    
    const onClickMinus = () => {
        dispatch({
            type: "DECREASE",
            data: 1
        })
    }

  return (
    <div>
      <h1>Count: {state}</h1>
      <button onClick={onClickPlus}>+</button>
      <button onClick={onClickMinus}></button>
    </div>
  );
}

export default Counter;

 


" 사용 시 주의사항 "

  • useReducer는 컴포넌트 최상단이나 커스텀 Hook에서만 호출할 수 있습니다.  
  • 반복문이나 조건문 내에서는 사용할 수 없습니다.
  • Strict Mode에서는 reducer와 init 함수가 두 번 호출됩니다. 이는 개발 환경에서만 발생하며, reducer와 init 함수가 순수 함수라면 로직에 영향을 주지 않습니다.

useContext()

useContext는 컨텍스트를 사용하여 전역 상태를 관리할 수 있는 Hook입니다.

const contextValue = useContext(MyContext);

 

 

<예시코드 01>

import { useContext } from 'react';

function MyComponent() {
  const theme = useContext(ThemeContext);
  return <div>{theme}</div>;
}

 


" 사용 시 주의사항 "

  • useContext는 반드시 Context.Provider 내에서 사용해야 하며, 중복된 모듈로 인해 작동하지 않을 수 있습니다.

useRef()

useRef는 컴포넌트에서 특정 DOM 요소나 값을 참조할 때 사용하는 Hook입니다.

주로 DOM 요소에 직접 접근하거나 변하지 않는 값을 저장하는 데 사용됩니다.

const refContainer = useRef(초깃값);

 

  • initialValue: ref의 초기 값
  • refContainer.current: ref가 참조하는 값이나 DOM 요소

 

<예시코드 01>

import { useRef } from 'react';

function MyComponent() {
  const inputRef = useRef(null);

  const handleFocus = () => {
    inputRef.current.focus();  // DOM 요소에 접근하여 포커스를 설정
  };

  return (
    <div>
      <input ref={inputRef} />
      <button onClick={handleFocus}>Focus on input</button>
    </div>
  );
}

 

 


" 사용 시 주의사항 "

  • useRef는 값이 변경되어도 리렌더링을 발생시키지 않습니다.
  • DOM 조작이나 변하지 않는 값을 저장하는 데 적합합니다.
  • useRef는 컴포넌트가 리렌더링 될 때도 같은 ref 객체를 계속 사용합니다.
  • useRef의 초기값은 **null**로 설정되며, DOM 요소가 마운트 되지 않으면 null을 가집니다.

04

마무리

React의 Hooks는 함수형 컴포넌트에서 상태와 생명주기를 쉽게 관리할 수 있게 해 줍니다.

이를 통해 코드가 더 간결하고 효율적이며, 유지보수도 쉬워집니다.

 

useState, useEffect, useReducer, useContext 등의 Hooks를 잘 활용하면, 복잡한 상태 관리와 데이터 흐름을 더 직관적으로 처리할 수 있습니다. 다만, 사용 시 주의사항을 잘 지켜야 성능과 효율성을 극대화할 수 있습니다.

 

더 많은 정보는 React 공식 문서에서 확인할 수 있습니다.