본문 바로가기

React/Basic

React Hook - useState

* 목차

 - Intro

 - Example

 - Extension 1. functional update

 - Extension 2. object update

 - Extension 3. create initial state by function

 - Extension 4. Resetting state with a key

 - 마치며

 

* 혹시 React Hook 자체가 개념이 잡혀있지 않으신 분들은 이 글을 먼저 읽어주세요

 

React Hook 알아보기

* 목차 - Intro - Hook이란? - React 18에서 제공하는 Hook의 종류 - 마치며 Intro * react18.2.0 version를 기준으로 설명합니다. React의 특징으로는 3가지를 뽑을 수 있습니다. jsx 문법 Hook 가상 DOM 이 3가지에 대

tyoon9781.tistory.com

 


Intro

이번 시간에는 React Hook 중에서 가장 기본인 useState에 대해서 알아보도록 하겠습니다.

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

 

useState는 데이터 생명주기를 관리하는 Hook으로, 보통 3단계로 작성하여 사용하게 됩니다.

 

  1. 컴포넌트에 const [data, setData] = useState({initialData});로 useState를 활용한 data 선언
  2. setData(data => {newData})로 data를 변경할 함수 작성
  3. <button onClick={changeData}>Change Data</button> 으로 html tag에 data를 변경할 함수 연결

 

기본적인 useState는 이렇게 3단계로 작성하게 됩니다. 실제로 간단한 예제를 작성해 봅시다.

 


Example

위에서 언급한 4가지를 천천히 작성해 봅시다.

 

1. 먼저 useState를 사용할 수 있도록 react library에서 불러옵니다.

import { useState } from 'react';

 

2. useState로 data의 lifecyle을 관리할 수 있도록 작성합니다. data의 이름은 count입니다.

export default function UseStateComponent(){
    const [count, setCount] = useState(0)
}

 

const [count, setCount] = useState(0) 형태를 보시면 useState가 [count, setCount]를 return 하는 것을 알 수 있습니다. 이때 count는 data의 값을 참조하기 위한 용도이고, setCount는 data의 값을 변경하기 위한 용도입니다. 따라서 다음과 같은 동작은 불가능합니다.

// count를 직접 조작할 수 없다.
// count = count + 1

 

대신에 data값을 변경하려면 다음과 같이 할 수 있습니다.

// count에 새로운 값 대입
setCount(count+1);

// 함수형 업데이트 방식
setCount(count => count+1);

 

둘 다 count를 1씩 증가하는 동작입니다. 하지만 함수형 업데이트 방식을 사용한다면 업데이트 간의 의존성이 있는 경우

함수형 업데이트는 의존성에 묻히지 않고 제대로 업데이트를 수행합니다. 따라서 특이한 이유가 없다면 함수형 업데이트 방식으로 작성하도록 합시다. 이 예제는 나중에 보여드리도록 하겠습니다.

 

3.  setCount를 사용할 함수를 만들어 줍시다. increment와 decrement 함수를 만들겠습니다.

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

  const decrement = () => {
    setCount(count => count - 1);
  };

 

이렇게 함수를 작성하고 나면 이제 이 함수를 web page에서 작동할 수 있도록 해야 겠지요? 버튼에 연결하도록 합시다.

 

4. Component에 버튼을 만들고 onClick attribute에 함수를 연결해 줍시다. onClick에 increment()를 입력하는 일은 없도록 합시다.

  return (
    <div>
      <h2>Count: {count}</h2>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </div>
  );

 

그러면 이제 useState의 예제 코드가 완성되었습니다. 완성된 코드는 다음과 같습니다.

import { useState } from "react";

export default function UseStateComponent() {
  const [count, setCount] = useState(0);

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

  const decrement = () => {
    setCount((count) => count - 1);
  };

  return (
    <div>
      <h2>Count: {count}</h2>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </div>
  );
}

 

그리고 App.js에 이 component를 부르고 실행해 봅시다.

Increment, Decrement가 잘 작동합니다.

 

여기까지 기본기를 알아보았습니다. 이제는 useState를 좀 더 다양하게 다뤄보도록 하겠습니다.

 


Extension 1. Functional update

Distance라는 data를 만들어서 count를 +1, -1이 아닌 더욱 다양한 숫자로 다룰 수 있도록 해보겠습니다.

import { useState } from "react";

export default function UseStateComponent() {
  const [count, setCount] = useState(0);
  const [distance, setDistance] = useState(1);

  const incrementCount = () => {
    setCount((count) => count + distance);
  };

  const decrementCount = () => {
    setCount((count) => count - distance);
  };

  const incrementDistance = () => {
    setDistance((distance) => distance + 1);
  };

  const decrementDistance = () => {
    setDistance((distance) => distance - 1);
  };

  return (
    <div>
      <h2>Count: {count}</h2>
      <button onClick={incrementCount}>Increment Count</button>
      <button onClick={decrementCount}>Decrement Count</button>
      <h3>Amount: {distance}</h3>
      <button onClick={incrementDistance}>Increment Distance</button>
      <button onClick={decrementDistance}>Decrement Distance</button>
    </div>
  );
}

 

이 코드는 distance를 증감 시킨 후 increment, decrement로 distance만큼 증감시키는 component입니다.

distance에 따라 count의 증감크기가 달라진다.

 

 

그런데 갑자기 여기서 이벤트(?)를 한다고 Increment Count를 기존 Distance의 2배를 증가시키려 합니다. 그래서 incrementCount 함수를 이렇게 작성해 보았습니다. 

  const incrementCount = () => {
    setCount((count) => count + distance);
    setCount((count) => count + distance);
  };

 

솔직히 distance에 x2하는 것에 비해 말도 안 되는 비효율(?) 적이지만 그래도 잘 작동하는 것을 볼 수 있습니다.

distance는 10이지만 count는 20씩 증가한다.

 

그런데 만약에 setCount를 함수형 업데이트 방식이 아닌 값 setting으로 바꿨다면 어떻게 되었을까요?

  const incrementCount = () => {
    setCount(count + distance);
    setCount(count + distance);
  };

 

그렇다면 하나의 setCount만 동작하는 듯한 결과를 보여줍니다.

2배 이벤트(?)가 먹히지 않았다.

 

이유는 setCount는 비동기적으로 동작하기 때문입니다. 더욱 정확하게 얘기하면 setCount가 호출되는 순간 바로 실행되는 것이 아니라 Queue에 쌓이게 됩니다. 그러면 setCount가 2줄 연속으로 작성되어 있을 때 다음과 같이 동작하게 됩니다.

  // count = 0, distance = 12인 경우
  const incrementCount = () => {
    setCount(count + distance);	// count = 0 + 12 작업이 queue에 push됨
    setCount(count + distance); // count = 0 + 12 작업이 queue에 push됨
  };
  // queue에 있는 setCount를 업데이트 함.
  // count result : 12

 

이렇게 의도치 않은 상황이 생길 수 있기 때문에 왠만하면 함수형 업데이트를 추천합니다. 참고로 함수형 업데이트를 진행하면 다음과 같이 됩니다.

  // count = 0, distance = 12라 가정
  const incrementCount = () => {
    setCount((count) => count + distance); // (count => count + distance) 작업이 queue에 push됨
    setCount((count) => count + distance); // (count => count + distance) 작업이 queue에 push됨
  };
  // count = count + distance = 0 + 12 로 인해 count = 12
  // count = count + distance = 12 + 12로 인해 count = 24
  // count result : 24

 


Extension 2. Object Update

이번에는 useState가 단순히 숫자가 아닌 Object를 다뤄보도록 하겠습니다.

  const [user, setUser] = useState({ name: "testUser", id: "AAA", age: 20 });

 

그리고 return에 user를 표현하고 age를 바꿀 수 있는 button을 만들어 줍시다.

      <h2>
        Hello {user.name}! your age is {user.age}
      </h2>
      <button onClick={setAge}>Set Age by Count</button>

 

그리고 setAge는 다음과 같은 함수를 만들어 줍시다. javascript의 ...연산자(Spread Operator)를 활용해서 user의 age만을 수정하도록 함수를 작성했습니다.

  const setAge = () => {
    setUser((user) => ({
      ...user,
      age: count,
    }));
  };

 

전체 코드는 다음과 같습니다.

import { useState } from "react";

export default function UseStateComponent() {
  const [count, setCount] = useState(0);
  const [distance, setDistance] = useState(1);
  const [user, setUser] = useState({ name: "testUser", id: "AAA", age: 20 });

  const incrementCount = () => {
    setCount((count) => count + distance);
    setCount((count) => count + distance);
  };

  const decrementCount = () => {
    setCount((count) => count - distance);
  };

  const incrementDistance = () => {
    setDistance((distance) => distance + 1);
  };

  const decrementDistance = () => {
    setDistance((distance) => distance - 1);
  };

  const setAge = () => {
    setUser((user) => ({
      ...user,
      age: count,
    }));
  };

  return (
    <div>
      <h2>
        Hello {user.name}! your age is {user.age}
      </h2>
      <button onClick={setAge}>Set Age by Count</button>
      <h2>Count: {count}</h2>
      <button onClick={incrementCount}>Increment Count</button>
      <button onClick={decrementCount}>Decrement Count</button>
      <h3>Distance: {distance}</h3>
      <button onClick={incrementDistance}>Increment Distance</button>
      <button onClick={decrementDistance}>Decrement Distance</button>
    </div>
  );
}

 

그럼 다음과 같은 결과를 확인할 수 있습니다.

distance로 Count를 정하고 age도 알맞게 수정되는 모습을 확인할 수 있다.

 


Extension 3. Create Initial state by function

지금까지는 useState의 초기값을 직접 입력해서 넣었습니다. 하지만 function을 활용해서 List를 만들고 useState의 initial State를 넣는 방법도 가능합니다. 이 방법에 대해서 먼저 정답부터 말씀드리면 re-rendering을 방지하기 위해 useState로 초기화할 때 function()을 넣지 말고 function 그 자체를 넣어야 합니다.

 

아래와 같이 10개의 Todo List를 작성하는 함수를 만들었습니다. 이때 return되는 값을 useState 초기화 값으로 사용하고 싶을 때 어떻게 code를 작성하여 초기화할 수 있을까요?

const createTodoList = () => {
  const initialTodoList = [];
  for (let i = 0; i < 10; i++) {
    initialTodoList.push({ id: i, text: `todo number : ${i}` });
  }
  return initialTodoList;
};

 

바로 function을 실행하지 않고 직접 넣는 것으로 초기화 할 수 있습니다.

const [todoList, setTodoList] = useState(createTodoList);

 

그리고 이 todoList를 Component를 통해 출력해 보겠습니다. 그럼 전체 코드는 다음과 같이 됩니다.

import { useState } from "react";

const createTodoList = () => {
  const initialTodoList = [];
  for (let i = 0; i < 10; i++) {
    initialTodoList.push({ id: i, text: `todo number : ${i}` });
  }
  return initialTodoList;
};

export default function UseStateComponent() {
  const [todoList, setTodoList] = useState(createTodoList);

  return (
    <div>
      <ul>
        {todoList.map((item) => (
          <li key={item.id}>{item.text}</li>
        ))}
      </ul>
    </div>
  );
}

 

todoList가 제대로 초기화 되었는지 확인해 보겠습니다.

제대로 출력이 되었다.

 

* 의외로(?) useState에서 초기화로 다음과 같이 작성해도 제대로 동작하는 것처럼 보입니다. 하지만 아래와 같이 작성했다간 모든 랜더링에서 createTodoList가 실행됩니다. 정상동작처럼 보일 수 있어도 resource 낭비가 발생합니다.

const [todoList, setTodoList] = useState(createTodoList());

 


Extension 4. Resetting state with a key

위의 예제에서 list를 출력하는 예제를 다뤘었습니다. 보통은 key를 유지해야 react rendering에 최적화된다고 하지만, key는 다른 목적도 가지고 있습니다.

key를 바꾸면 component의 state를 reset 시킬 수 있습니다. 자주 의도되는 작업은 아니지만 component reset을 구현할 수 있는 좋은 방법 중 하나입니다.

 

Form component의 key를 useState로 관리하고, 버튼을 누르면 key의 값이 항상 바뀌도록 설정했습니다. 

import { useState } from "react";

const Form = () => {
  const [name, setName] = useState("AAA");
  return (
    <>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <p>Hello, {name}</p>
    </>
  );
};

export default function UseStateComponent() {
  const [keyBoolean, setKeyBoolean] = useState(true);

  const handleReset = () => {
    setKeyBoolean((keyBoolean) => !keyBoolean);
  };

  return (
    <div>
      <button onClick={handleReset}>Reset</button>
      <Form key={keyBoolean} />
    </div>
  );
}

 

input에 어떤 이름을 입력해도 Reset button을 누르면 이름이 초기화 된다.

 


마치며

useState는 React를 하면서 가장 기본적으로 다루게 되는 Hook입니다. 기본으로 바우는 것이기 때문에 단순 변수처럼 생각하고 지나갈 수 있는 부분입니다. 하지만 비동기 처리 방식과 Virtual DOM에 대한 개념이 잡히지 않은 상태에서 useState를 남용하면 나중에 원인을 알 수 없는 resource 낭비가 발생하기 쉽고 이는 Web Application의 품질에 잠재된 문제를 만들어 버립니다. useState의 다양한 예시를 확인하면서 정확한 사용법을 알아가시길 바랍니다. 감사합니다.

 


*reference

https://react.dev/reference/react/useState

 

useState – React

The library for web and native user interfaces

react.dev

'React > Basic' 카테고리의 다른 글

React Hook - useEffect  (0) 2023.08.02
React Hook - useReducer  (0) 2023.07.24
React Hook 알아보기  (0) 2023.07.23
Material UI - Tutorial  (0) 2023.07.09
'React' must be in scope when using JSX  (0) 2023.04.15