[react] useState hook

Published: by Creative Commons Licence

참고

useState

useState

상태를 관리하는 훅으로 컴포넌트 최상단에서만 사용 가능하다. 기본적으로 반복문이나 조건문에서는 사용이 불가하지만 필요하다면, 새로운 컴포넌트를 만들고 상태를 그 컴포넌트로 전달하면 가능하다.

구문

const [something, setSomething] = useState(initialState)

  • initialState는 초기값, 모든 유형을 초기값으로 사용할 수 있다. 함수도 물론 사용 가능하지만 함수호출을 하는 것이 아닌 함수자체를 넘기거나 익명함수를 넘긴다. 초기값은 최초 렌더링 후에는 무시된다.
  • 반환값은 현재상태값과 상태값을 변경할 수 있는 set함수로 이루어진 배열이다.

set 함수

  • set함수는 setSomething(nextState) 형태로 상태값을 업데이트하고 다시 렌더링을 하게 한다. 인수 nextState값은 함수 포함 모든 유형이 가능하다.
  • set함수는 리렌더링에 대한 상태만 업데이트하는 함수이기 때문에, set함수 호출 직후에 실제 화면상의 상태값은 변경되지 않는다.
  • Object.is()로 새 상태값과 현재 상태값이 같으면 리액트는 리렌더링을 하지 않는다.
  • 리액트는 모든 이벤트 핸들러가 실행되고 해당 설정 함수를 호출된 후에야 한번에 화면을 업데이트한다.
  • 만약 이보다 먼저 화면을 업데이트 해야 한다면, flushSync를 통해 화면에 더 일찍 업데이트하도록 강제할 수 있다.
  • set함수 호출은 현재 컴포넌트 내에서만 허용된다.

상태값 변경 시점

import { useState } from 'react';

export default function Form() {
  const [name, setName] = useState('Taylor');
  const [age, setAge] = useState(42);

  function handleClick() {
    setAge(age + 1);
  }

  return (
    <>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
      />
      <button onClick={handleClick}>
        Increment age
      </button>
      <p>Hello, {name}. You are {age}.</p>
    </>
  );
}

나이를 변경하는 set함수를 3번 연속으로 실행한다고 해도 age가 바로 업데이트 되지 않아 원하는 결과를 얻을 수 없다. 이걸 해결하려면 상태값이 아닌 업데이트 함수를 인수로 넘겨야 한다.

  function handleClick() {
    setAge(age + 1); // setAge(42 + 1)
    setAge(age + 1); // setAge(42 + 1) 여전히 age는 42다. 이 순간에는 상태변수의 값은 없데이트 전이다. 
    setAge(age + 1); // setAge(42 + 1) 여전히 age는 42다. 이 순간에는 상태변수의 값은 없데이트 전이다. 
  }

업데이트 함수를 인수로 넘기면, 실제 리액트는 내부에서 이렇게 실행이 된다.

  1. 이벤트 핸들러 handleClick() 실행
  2. set 함수의 인수로 전달된 업데이트 함수(updater function)a => a + 1를 큐에 차례로 넣음
  3. 컴포넌트 리렌더링, 대기상태의 큐의 업데이트 함수 계산
    • 업데이트 함수는 대기상태값으로 다음 상태값을 계산한다. a => a + 1 에서 a가 대기상태값, a + 1의 계산결과가 다음 상태값이 된다.
  4. 큐에 넣은 업데이트 함수 동일한 순서대로 호출
  5. age 상태값 업데이트
  function handleClick() {
    setAge(a => a + 1); // setAge(42 + 1)
    setAge(a => a + 1); // setAge(43 + 1)
    setAge(a => a + 1); // setAge(44 + 1)
  }

실제 이런 경우는 많이 발생하지 않는다. 리액트가 클릭에 반응해 바로 상태를 업데이트 하기 때문이다. 핸들러 내에서 2개 이상의 상태를 업데이트 해야 하는 경우에는 업데이트 함수가 유용하다.

객체와 배열의 상태 변경

기본적으로 stateread-only다. 직접 변경하면 안된다. 따라서 객체와 배열의 상태는 직접 변경하면 안되고 복사본으로 대체를 해야 한다.

form.firstName = 'Taylor'; // 직접 수정은 안된다. 실제로 된다고 하더라도 리렌더링이 제대로 이루어 지지 않는다. 
setForm({
  ...form, // 기존 객체를 전개구문으로 복사
  firstName: 'Taylor' // 그 중  firstName만 변경한다. 
});

함수 초기값 세팅

useState의 초기값은 최초 한 번만 사용되고 그 이후에는 무시된다. 그런데 아래처럼 함수호출식을 인수로 넘기면 리렌더링을 할때마다 재호출로 인한 자원소모가 발생한다. 실제 값은 더 이상 사용하지 않는데도 말이다.

const [tasks, setTasks] = useState(getStore() || []); 

함수호출식이 아닌 함수자체(초기화함수, the initializer function)를 넘기면 이 문제는 해결된다. 인수로 넘겨받은 함수 자체는 최초 렌더링 시에만 호출되고 이후는 호출 되지 않는다.

const [tasks, setTasks] = useState(getTodoList);
function getTodoList() {
  return getStore() || [];
}

key로 상태 리셋하기

보통 리스트를 렌더링할때 key를 사용하지만 key를 이용해 컴포넌트의 상태를 리셋할 수 있다.
리셋버튼을 클릭하면 version상태가 변경된다. 그럼 리액트는 key상태값이 변화한 것을 감지하고 Form 컴포넌트를 다시 그리게 되고 name 상태는 다시 초기화된다.

import { useState } from 'react';

export default function App() {
  const [version, setVersion] = useState(0);

  function handleReset() {
    setVersion(version + 1);
  }

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

function Form() {
  const [name, setName] = useState('Taylor');

  return (
    <>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
      />
      <p>Hello, {name}.</p>
    </>
  );
}