React - State

반응형

State(상태)는 리액트에서 컴포넌트 내부에서 관리되는 동적인 데이터를 의미한다.

사용자 입력, API 응답, 버튼 클릭 등과 같은 이벤트에 따라 변할 수 있는 값을 저장하는 공간으로,

state가 변경될 때 마다 리액트는 자동으로 컴포넌트를 재렌더링하여 UI를 최신 상태로 유지한다.


State 사용방법

▼ 함수형 컴포넌트에서 useState 사용하기

import { useState } from "react"

function Counter() {
  // count 상태(state)와 이를 변경할 수 있는 setCount 함수 선언
  const [count, setCount] = useState(0)

  return (
    <div>
      <h2>현재 카운트 : {count}</h2>
      <button onClick={() => setCount(count + 1)}>증가</button>
      <button onClick={() => setCount(count - 1)}>감소</button>
    </div>
  )
}

export default Counter

useState(0)은 count 라는 state 변수를 선언하고 초기값을 0으로 설정한다.

setCount(newValue)는 cunt 값을 변경하는 함수이며

버튼 클릭시 setCount(count + 1)을 호출하여 count 값을 증가,  감소 시킨다.

State의 특징

1. State는 개별 컴포넌트 내부에서 관리된다.

State는 각 컴포넌트 내부에서만 관리되는 값으로

다른 컴포넌트에 직접 접근하거나 수정할 수는 없지만 props를 통해 전달할 수 있다.

다음 예제를 살펴보자

import { useState } from "react";

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

  return (
    <div>
      <h3>카운트: {count}</h3>
      <button onClick={() => setCount(count + 1)}>증가</button>
    </div>
  );
}

export default function App() {
  return (
    <div>
      <h2>독립적인 두 개의 카운터</h2>
      <Counter />
      <Counter />
    </div>
  );
}

위 예제에서는 두 개의 카운터가 있지만 각각의 state는 독립적으로 관리되기 때문에

버튼을 누르면 각 카운터가 개별적으로 증가하는 것을 확인할 수 있으며 (state가 독립적이라는 증거)

하나의 Counter 컴포넌트가 변경되어도 다른 Counter의 값은 바뀌지 않는다.

 

2. State는 불변성이다.

state는 직접 수정할 수 없고 반드시 setState(클래스형) 또는

useState의 setter 함수(함수형)을 사용해야한다.

다음 예제를 살펴보자

 

▼ 잘못된 예제 

import { useState } from "react"

function Counter() {
  const [count, setCount] = useState(0)
  const handleClick = () => {
 // state를 직접 수정 - setCount(setter함수)를 사용하지 않으면 리액트가 변경을 감지 하지못함
     count = count + 1
  }
  return (
    <div>
      <h3>카운트 :{count}</h3>
      <button onClick={handleClick}>증가</button>
    </div>
  )
}

export default Counter

 

▼ 올바른 예제

import { useState } from "react"

function Counter() {
  const [count, setCount] = useState(0)
  const handleClick = () => {
    setCount(count + 1) //setCount는 useState의 setter 함수
  }
  return (
    <div>
      <h3>카운트 :{count}</h3>
      <button onClick={handleClick}>증가</button>
    </div>
  )
}
export default Counter


// 또는

import { useState } from "react"

function Counter() {
  const [count, setCount] = useState(0)
  return (
    <div>
      <h3>카운트 :{count}</h3>
      <button onClick={() => setCount(count + 1)}>증가</button>
    </div>
  )
}

export default Counter

 

3. state 업데이트는 비동기적으로 처리됨

리액트에서는 여러 개의 setState 호출이 있을 경우

최적화를 위해 한꺼번에 처리(batch)될 수 있다.

즉, 이전 state 값을 기준으로 상태를 업데이트하려면 prevState를 사용해야한다.

다음 예제를 살펴보자

 

▼ 잘못된 예제

import { useState } from "react"

function Counter() {
  const [count, setCount] = useState(0)
  const handleClick = () => {
    setCount(count + 1)
    setCount(count + 1)
    setCount(count + 1)
  }
  return (
    <div>
      <h3>카운트 :{count}</h3>
      <button onClick={handleClick}>증가</button>
    </div>
  )
}

export default Counter

위와 같이 setCount setter함수를 3번이나 호출했지만 

실제로는 count 값이 1만 증가한다.

리액트는 여러 setState 호출을 묶어서 처리(batching)하기 때문에

count 값이 변경된 것을 한 번만 반영한다.

 

▼ 올바른 예제

import { useState } from "react"

function Counter() {
  const [count, setCount] = useState(0)
  const handleClick = () => {
    setCount((prev) => prev + 1)
    setCount((prev) => prev + 1)
    setCount((prev) => prev + 1)
  }
  return (
    <div>
      <h3>카운트 :{count}</h3>
      <button onClick={handleClick}>증가</button>
    </div>
  )
}

export default Counter

위와 같이 prevState를 활용하여 버튼을 클릭하면 count 값이 한 번에 3씩 증가하게 된다.

prev(이전 상태 값)을 사용하면 리액트가 batch처리하더라도

올바르게 상태가 업데이트된다.

 


State와 Props 차이점

State는 컴포넌트 내부에서 관리하는 데이터이며 변경할 수가 있고

props는 부모에서 자식으로 전달되는 데이터이며 변경할 수 없는 차이가 있다.

또한 state는 동적인 데이터 관리인 반면 props는 정적인 데이터를 전달하고

state로 버튼을 클릭하면 카운터가 증가하듯

props로 구현하려면 부모가 자식컴포넌트에 데이터를 전달하게 로직을 구성해야하는 번거로움이 있다.

대충 정리하면 다음과 같다.

구분 State Props
데이터 변경 가능 여부 변경 가능 (setState, useState) 변경 불가능 (부모로부터 받은 값)
관리 위치 컴포넌트 내부에서 관리 부모 컴포넌트에서 전달
주요 역할 동적인 데이터 관리 정적인 데이터 전달

useState와 useReducer

컴포넌트가 복잡해지고 상태관리가 어려워질 때

useState 대신 useReducer를 사용할 수 있다.

예를들면 여러개의 state가 서로 의존적이거나

상태 업데이트 로직이 복잡할 때(if-else가 많아질때)

상태가 여러개이고 상태간의 의존성이 useReducer를 사용하는 것이 좋다.

 

▼ useState (간단한 상태관리)

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

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

 

▼ useReducer (복잡한 상태관리)

import { 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 };
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });

  return (
    <div>
      <p>카운트: {state.count}</p>
      <button onClick={() => dispatch({ type: "increment" })}>증가</button>
      <button onClick={() => dispatch({ type: "decrement" })}>감소</button>
    </div>
  );
}

export default Counter

useState에서 객체(object) 상태 관리하기

useState를 사용할 때 객체 상태 업데이트에 관하여 주의할 점이 있다.

다음 예시를 살펴보자

import { useState } from "react"

function UserProfile() {
  const [user, setUser] = useState({ name: "섭이", age: 20 })

  const updataNmae = () => {
    setUser({ name: "행섭" }) // age 값이 사라지는 현상 발생
  }
  return (
    <div>
      <h2>사용자 정보</h2>
      <p>이름: {user.name} </p>
      <p>나이:{user.age} </p>
      <button onClick={updataNmae}>이름 변경</button>
    </div>
  )
}

export default UserProfile

위 코드 처럼 setUser({name:행섭"})을 사용하면

기존 age 데이터가 사라지는 문제가 발생한다.

해결 방법은 다음과 같이 spread 연산자를 사용하여 기존 상태를 유지시켜야한다.

import { useState } from "react"

function UserProfile() {
  const [user, setUser] = useState({ name: "섭이", age: 20 })

  const updataNmae = () => {
    // 이름만 변경하면서 기존 객체의 데이터를 유지하는 함수
    setUser((prevUser) => ({ ...prevUser, name: "행섭" }))
  }
  return (
    <div>
      <h2>사용자 정보</h2>
      <p>이름: {user.name} </p>
      <p>나이:{user.age} </p>
      <button onClick={updataNmae}>이름 변경</button>
    </div>
  )
}

export default UserProfile

위와 같이 기존 user 객체를 유지하면서 name 값만 변경할 수 있게 spread 연산자를 사용하였고

그 결과 age 값이 사라지지 않고 유지된다.

 

또한여러 개의 속성을 동시에 변경할 수 있는데

다음과 같이 name과 age를 동시에 변경 시킬 수 있다.

import { useState } from "react"

function UserProfile() {
  const [user, setUser] = useState({ name: "섭이", age: 20, city: "Seoul" })

  const updataNmae = () => {
    setUser((prevUser) => ({ ...prevUser, name: "행섭", age: prevUser.age + 1 })) // 1증가

  }
  return (
    <div>
      <h2>사용자 정보</h2>
      <p>이름: {user.name} </p>
      <p>나이:{user.age} </p>
      <p>도시: {user.city}</p>
      <button onClick={updataNmae}>이름 변경</button>
    </div>
  )
}

export default UserProfile

 


useState에서 배열 업데이트 하기 (map, filter)

객체뿐만 아니라 배열을 state로 관리할 때도 기존 값을 유지하면서 업데이트 해야한다.

setState로 배열을 업데이트할 때는 map, filter,spread 연산자(...)등을 사용해야한다.

다음 예제를 살펴보자

 

▼ 리스트에서 특정 아이템의 값 변경하기

import { useState } from "react"

function TodoListi() {
  const [todos, setTodos] = useState([
    { id: 1, text: "리액트 공부하기", completed: false },
    { id: 2, text: "낮잠자기", completed: false },
    { id: 3, text: "게임하기", completed: false },
  ])

  // 특정 할일 완료 상태 변경
  const toogleTodo = (id) => {
    setTodos((prevTodos) =>
      prevTodos.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    )
  }
  return (
    <div>
      <h2>할일 목록</h2>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <spen style={{ textDecoration: todo.completed ? "line-through" : "none" }}>
              {todo.text}
            </spen>
            <button onClick={() => toogleTodo(todo.id)}>
              {todo.completed ? "취소" : "완료"}
            </button>
          </li>
        ))}

      </ul>
    </div>
  )
}

export default TodoListi

위와 같이 완료 버튼을 누르면 toogleTodo(n) 실행 -> id: n인 항목의 completed 값을 true로 변경되며

map()을 사용해 배열을 순회하면서 특정 id와 일치하는 항목의 상태를 변경한다

또한 spread 연산자를 활용하여 기존 객체의 다른 속성은 유지한 채 completed만 변경한다.


State의 비효율적인 업데이트 방지 (리랜더링(Re-render) 최적화)

1. React.memo를 사용하여 불필요한 렌더링 방지하기

다음 코드에서 setCount 버튼을 누르면

부모가 리렌더링 될 떄, 자식도 불필요하게 리렌더링되는 현상이 있다.

import { useState } from "react";

function Parent() {
  const [count, setCount] = useState(0)
  return (
    <div>
      <h2>부모 컴포넌트</h2>
      <button onClick={() => setCount(count + 1)}>부모 카운트 증가</button>
      <Child />
    </div>
  )
}

function Child() {
  console.log('자식 컴포넌트 렌더링');
  return <h3>자식컴포넌트 입니다.</h3>
}

export default Parent

 

해결 방법은 다음과 같이 React.memo를 사용한다.

import { memo, useState } from "react";

function Parent() {
  const [count, setCount] = useState(0)
  return (
    <div>
      <h2>부모 컴포넌트</h2>
      <button onClick={() => setCount(count + 1)}>부모 카운트 증가</button>
      <MemoizedChild />
    </div>
  )
}

function Child() {
  console.log('자식 컴포넌트 렌더링');
  return <h3>자식컴포넌트 입니다.</h3>
}

const MemoizedChild = memo(Child) // React.memo 사용하여 불필요한 렌더링 방지

export default Parent

이렇게 하면 Child는 props가 변경되지 않는 한 다시 렌더링되지 않으며

setCount를 눌러도 자식은 그대로 유지된다.

 

2) useCallback을 사용하여 함수가 불필요하게 재생성되지 않도록 방지

import { useState, useCallback, memo } from "react";

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

  // useCallback을 사용하지 않으면 부모가 리렌더링될 때마다 handleClick이 새로 생성됨
  const handleClick = useCallback(() => {
    console.log("handleClick 함수는 재사용됨!");
  }, []);

  return (
    <div>
      <h2>부모 컴포넌트</h2>
      <button onClick={() => setCount(count + 1)}>부모 카운트 증가</button>
      <Child handleClick={handleClick} />
    </div>
  );
}

const Child = memo(({ handleClick }) => {
  console.log("자식 컴포넌트 렌더링됨!");
  return <button onClick={handleClick}>자식 버튼</button>;
});

export default Parent;

 

위와 같이 useCallback을 사용하여 handleClick 함수가 

부모가 리렌더링될 때마다 새로 생성되지 않으며

Child가 불필요하게 리렌더링되지 않는다.

 

반응형

'React' 카테고리의 다른 글

React- React에서 인라인 스타일 사용하기  (0) 2025.02.28
React - React의 렌더링 방식 : State  (0) 2025.02.27
React - children props  (0) 2025.02.21
React - props  (0) 2025.02.20
React - 리액트 컴포넌트 (React Component)  (0) 2025.02.19