Hooks - useState

흑우·2023년 8월 9일

useState

참조하기

useState

function MyComponent() {
  const [age, setAge] = useState(28);
  const [name, setName] = useState('Taylor');
  const [todos, setTodos] = useState(() => createTodos());
  • 컴포넌트의 최상위 레벨에서 useState를 호출하여 state 변수를 선언하세요.
  • 배열 구조 분해를 사용하여 [something, setSomething]과 같은 state 변수의 이름을 지정하는 것이 일반적입니다.

매개변수

  • initialState : 초기에 state를 설정할 값입니다. 값은 모든 데이터 타입이 허용되며, 이 인자는 초기 렌더링 이후에는 무시됩니다.
  • 함수를 initialState로 전달하면 이를 초기화 함수로 취급합니다. React는 컴포넌트를 초기화할 때 초기화 함수를 호출하고, 그 반환값을 초기 state로 저장합니다.
    • 순수 함수여야 합니다.
    • 인자를 받지 않아야 합니다.
    • 반드시 어떤 값을 반환해야 합니다.

반환값

  • useState는 두 개의 값을 가진 배열을 반환합니다.
    • 첫 번째 : 현재 state입니다. 첫 번째 렌더링 중에는 전달한 initialState와 일치합니다.
    • 두 번째 : state를 다른 값으로 업데이트하고 리렌더링을 촉발할 수 있는 set 함수입니다.

주의사항

  • useState는 훅이므로 컴포넌트의 최상위 레벨이나 직접 만든 훅에서만 호출할 수 있습니다.
    • 반복문이나 조건문 안에서 호출하고 싶다면새 컴포넌트를 생성하고 state를 그 안으로 옮기세요
  • Strict Mode에서 React는 순수 함수인지 확인하기 위해 초기화 함수를 두 번 호출합니다. 초기화 함수가 순수하다면 동작에 영향을 미치지 않습니다. 호출 중 하나의 결과는 무시됩니다.

setSomething(nextState)

매개변수

  • nextState: state가 될 값입니다.
  • 함수를 nextState로 전달하면 업데이터 함수로 취급됩니다.
    • 함수는 순수해야 합니다.
    • 대기중인 state를 유일한 인수로 사용해야 합니다.
    • 다음 state를 반환해야 합니다.
  • React는 업데이터 함수를 대기열에 넣고 컴포넌트를 리렌더링 합니다.
  • 다음 렌더링 중에 React는 대기열에 있는 모든 업데이터를 이전 state에 적용하여 다음 state를 계산합니다.

반환값

  • set 함수는 반환값이 없습니다.

주의사항

  • set 함수는 다음 렌더링에 대한 state 변수만 업데이트 합니다. set 함수를 호출한 후에도 state 변수에는 이전 값이 담겨 있습니다.
  • 사용자가 제공한 새로운 값이 Object.is에 의해 현재 state와 동일하다고 판단되면, React는 컴포넌트와 그 자식들을 리렌더링하지 않습니다.
  • React는 state 업데이트를 일괄처리합니다. 모든 이벤트 핸들러가 실행되고 set 함수를 호출한 후에 화면을 업데이트 합니다.
  • 렌더링 도중 set 함수를 호출하는 것은 현재 렌더링 중인 컴포넌트 내에서만 허용됩니다. React는 해당 출력을 버리고 즉시 새로운 state로 다시 렌더링을 시도합니다. 이전 렌더링의 정보를 저장하는 데 사용할 수 있습니다.
  • Strict Mode에서 React는 의도치않은 불순물을 찾기 위해 업데이터 함수를 두 번 호출합니다.

사용법

컴포넌트에 state 추가하기

function MyComponent() {
  const [age, setAge] = useState(42);
  const [name, setName] = useState('Taylor');
  • useState는 정확히 두 개의 요소가 있는 배열을 반환합니다.
    • 현재 state로, 처음에 제공한 초기 state로 설정됩니다.
    • 상호작용이 반응하여 다른 값으로 변경할 수 있는 set 함수입니다.
  • 화면의 내용을 업데이트하려면 다음 state로 set 함수를 호출합니다.
function handleClick() {
  setName('Robin');
  console.log(name); // 아직 "Taylor"입니다!              
}

이전 state를 기반으로 state 업데이트하기

function handleClick() {
  setAge(age + 1); // setAge(42 + 1)
  setAge(age + 1); // setAge(42 + 1)
  setAge(age + 1); // setAge(42 + 1)
}
  • handleClick이 실행되었을 때 age가 +3 될 것이라고 예상하지만 실제로는 +1로 동작합니다.
  • set 함수는 비동기적으로 발동하기 때문에 이미 실행 중인 코드에서는 state 변수가 업데이트되지 않기 때문입니다.
function handleClick() {
  setAge(a => a + 1); // setAge(42 => 43)
  setAge(a => a + 1); // setAge(43 => 44)
  setAge(a => a + 1); // setAge(44 => 45)
}
  • 해당 문제를 해결하기 위해 state대신 setAge에 업데이터 함수를 전달할 수 있습니다.
  • 업데이터 함수는 대기 중인 state를 가져와서 다음 state를 계산합니다.
  • React는 업데이터 함수를 큐에 넣습니다. 그러면 다음 렌더링 중에 동일한 순서로 호출합니다.
  • 대기 중인 다른 업데이트가 없으면 React는 결국 45를 현재 state로 저장합니다.
  • 업데이터 함수의 인수는 age의 a와 같이 state 변수 이름의 첫 글자 또는 prevAge 같은 명확한 이름으로 지정해야 합니다.
  • 동일한 이벤트 내에서 여러 업데이트를 수행하는 경우에 유용합니다.

객체 및 배열 state 업데이트

  • React의 state는 읽기 전용으로 간주되므로 기존 객체를 변이하지 않고, 교체를 해야 합니다.
// X
form.firstName = 'Taylor'; 

// O
setForm({ 
  ...form,
  firstName: 'Taylor'
});

중첩 객체 업데이트

function handleTitleChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        title: e.target.value
      }
    });
  }
  • 중첩된 객체를 업데이트하기 위해서는 업데이트 하려는 객체의 복사본을 만들어야하며, 위쪽으로 올라갈 때마다 해당 객체를 "포함하는" 모든 객체에 대한 복사본을 만들어야 합니다.

중첩 배열 업데이트

function handleAddTodo(title) {
    setTodos([
      ...todos,
      {
        id: nextId++,
        title: title,
        done: false
      }
    ]);
  }

function handleChangeTodo(nextTodo) {
    setTodos(todos.map(t => {
      if (t.id === nextTodo.id) {
        return nextTodo;
      } else {
        return t;
      }
    }));
  }

😀 초기 state 다시 생성하지 않기

function TodoList() {
  const [todos, setTodos] = useState(createInitialTodos());
  // ...
  • createInitialTodos()의 결과는 초기 렌더링에 사용되지만, 여전히 모든 렌더링에서 이 함수를 호출하게 됩니다.
  • 이는 큰 배열을 생성하거나 값비싼 계산을 수행하는 경우 낭비가 될 수 있습니다.
function TodoList() {
  const [todos, setTodos] = useState(createInitialTodos);
  // ...
  • 이 문제를 해결하려면, useState에 초기화 함수로 전달하세요.
    - 함수를 호출한 결과인 createInitialTodos()가 아니라 함수 자체인 createInitialTodos를 전달하면, React는 초기화 중에만 함수를 호출합니다. (이건 저도 몰랐던 부분이네요)

😀 key로 state 재설정하기

  • 목록을 렌더링할 때 key 속성을 자주 접하게 됩니다. 하지만 key 속성은 다른 용도로도 사용됩니다.
  • 컴포넌트에 다른 key를 전달하여 컴포넌트의 state를 재설정할 수 있습니다.
  • 이 예제에서는 React 버튼이 version state 변수를 변경하고, 이를 Form에 key로 전달합니다. key가 변경되면 React는 Form 컴포넌트를 처음부터 다시 생성하므로 state가 초기화 됩니다.
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>
    </>
  );
}

😀 이전 렌더링에서 얻은 정보 저장하기

  • 보통은 이벤트 핸들러에서 state를 업데이트 합니다. 하지만 드물게 렌더링에 대한 응답으로 state를 조정해야 하는 경우도 있습니다.
    • 예를 들어, props가 변경될 때 state 변수를 변경하고 싶을 수 있습니다.
  • 대부분의 경우 이 기능은 필요하지 않습니다.
    • 필요한 값을 현재 props나 다른 state에서 모두 계산할 수 있는 경우, 중복되는 state를 모두 제거하세요.
    • 너무 자주 재계산하는 것이 걱정된다면, useMemo hook을 사용하면 도움이 됩니다.
    • 전체 컴포넌트 트리의 state를 재설정하려면 컴포넌트에 다른 key를 전달하세요.
    • 가능하다면 이벤트 핸들러의 모든 관련 state를 업데이트하세요.
  • 이 중 어느 것에도 해당하지 않는 희귀한 경우에는, 컴포넌트가 렌더링되는 동안 set 함수를 호출하여 지금까지 렌더링된 값을 기반으로 state를 업데이트하는 데 사용할 수 있는 패턴이 있습니다.
  • CountLabel 컴포넌트는 전달된 count props를 표현합니다. 카운터가 마지막 변경 이후 증가 또는 감소했는지를 표시하고 싶다고 가정해 보겠습니다.
  • 이를 추적하기 위해 prevCount state 변수를 추가합니다. 그리고 trend라는 또 다른 state 변수를 추가하여 count의 증가 또는 감소 여부를 추적합니다.
  • prevCount와 count를 비교해서, 같지 않은 경우 prevCount와 trend를 모두 업데이트합니다.
export default function CountLabel({ count }) {
  const [prevCount, setPrevCount] = useState(count);
  const [trend, setTrend] = useState(null);
  if (prevCount !== count) {
    setPrevCount(count);
    setTrend(count > prevCount ? 'increasing' : 'decreasing');
  }
  return (
    <>
      <h1>{count}</h1>
      {trend && <p>The count is {trend}</p>}
    </>
  );
}
  • 렌더링하는 동안 set 함수를 호출하는 경우, 그 set 함수는 prevCount !== count와 같은 조건 안에 있어야 하며, 조건 내부에 setPrevCount(count)와 같은 호출이 있어야합니다.
  • 그렇지 않으면 리렌더링을 반복하다가 결국 깨질 것입니다. 또한 이 방식은 오직 현재 렌더링 중인 컴포넌트의 state만을 업데이트 할 수 있습니다.
  • 렌더링 중에 다른 컴포넌트의 set 함수를 호출하는 것은 에러입니다. 마지막으로, 이 경우에도 set 함수 호출은 여전히 변이가 아닌 state 업데이트여야 합니다. 순수 함수의 다른 규칙을 지켜야합니다.
  • 이 패턴은 일반적으로 피하는 것이 좋습니다. 하지만 Effect에서 state를 업데이트하는 것보다는 낫습니다.
  • 렌더링 도중 set 함수를 호출하면 React는 컴포넌트가 return문으로 종료된 직후, 자식을 렌더링하기 전에, 해당 컴포넌트를 리렌더링합니다.
  • 이렇게 하면 자식 컴포넌트를 두 번 렌더링할 필요가 없습니다.
  • 나머지 컴포넌트 함수는 계속 실행되고 결과는 버려집니다.
  • 조건이 모든 훅 호출보다 아래에 있으면 이른 return을 통해 렌더링을 더 일찍 다시 시작할 수 있습니다.

문제 해결

1. state를 업데이트 했지만 로그에는 계속 이전 값이 표시됩니다.

function handleClick() {
  console.log(count);  // 0

  setCount(count + 1); // 1로 리렌더링 요청
                       
  console.log(count);  // 아직 0입니다! 
                       

  setTimeout(() => {
    console.log(count); // 여기도 0이고요!
                        
  }, 5000);
}
  • set 함수를 호출해도 실행 중인 코드의 state는 변경되지 않습니다.
  • 이유는 state가 스냅샷처럼 동작하기 때문입니다. state를 업데이트하면 새로운 state 값으로 다른 렌더링을 요청하지만 이미 실행 중인 이벤트 핸들러의 count 변수에는 영향을 미치지 않습니다.
  • 다음 state를 사용해야 하는 경우에는, set 함수에 전달하기 전에 변수를 저장해야 합니다.
const nextCount = count + 1;
setCount(nextCount);

console.log(count);     // 0
console.log(nextCount); // 1

2. state를 업데이트해도 화면이 바뀌지 않습니다.

obj.x = 10;  // 🚩 Wrong: mutating existing object
             // 🚩 잘못된 방법: 기존 객체를 변이
setObj(obj); // 🚩 Doesn't do anything
             // 🚩 아무것도 하지 않습니다.
  • React는 Object.is 비교 결과 다음 state가 이전 state와 같으면 업데이트를 무시합니다. 이는 보통 객체나 배열의 state를 직접 변경(변이)할 때 발생합니다.
// ✅ 올바른 방법: 새로운 객체 생성
setObj({
  ...obj,
  x: 10
});
  • 이를 해결하기 위해서는 항상 객체나 배열 state를 변이하는 대신 교체해야 합니다.

3. 에러가 발생했습니다: 리렌더링 횟수가 너무 많습니다.

  • 전형적으로 이는 렌더링 중에 state를 무조건적으로 설정하고 있음을 의미 하기 때문에, 컴포넌트가 렌더링, state 설정(렌더링 유발), 렌더링, state 설정(렌더링 유발) 등의 루프에 들어가는 것입니다. 이 문제는 이벤트 핸들러를 지정하는 과정에서 많이 나옵니다.
// 🚩 잘못된 방법: 렌더링 동안 핸들러 요청
return <button onClick={handleClick()}>Click me</button>

// ✅ 올바른 방법: 이벤트 핸들러로 전달
return <button onClick={handleClick}>Click me</button>

// ✅ 올바른 방법: 인라인 함수로 전달
return <button onClick={(e) => handleClick(e)}>Click me</button>

4. 초기화 함수 또는 업데이터 함수가 두 번 실행됩니다.

  • Strict Mode에서 React는 일부 함수를 한 번이 아닌 두 번 호출합니다. 이는 정상적인 현상이며, 이로 인해 코드가 손상되지 않아야 합니다.
  • Strict Mode는 컴포넌트를 순수하게 유지하는 데 도움이 됩니다. React는 호출 중 하나의 결과를 사용하고 다른 호출의 결과는 무시합니다.
  • 컴포넌트, 초기화 함수, 업데이터 함수가 순수하다면 이 동작은 로직에 영향을 미치지 않습니다.
  • 반면 의도치 않게 순수하지 않을 경우에는 실수를 알아차리는 데 도움이 됩니다.
setTodos(prevTodos => {
  // 🚩 실수: state 변이
  prevTodos.push(createTodo());
});

setTodos(prevTodos => {
  // ✅ 올바른 방법: 새로운 state로 교체
  return [...prevTodos, createTodo()];
});
  • 컴포넌트, 초기화 함수, 업데이터 함수는 순수해야 합니다

5. state의 값으로 함수를 설정하려고 하면 설정은 안되고 대신 호출됩니다.

  • state에 함수를 넣을 수는 없습니다.
  • useState에 함수를 전달하면 React는 초기화 함수로 여기고, setFn에 함수를 전달하면 업데이터 함수로 받아들입니다.
  • 따라서 이들을 호출해서 그 결과를 저장하려고 시도합니다. 정말로 함수를 저장하길 원하다면, 함수 앞에 () => 를 넣어야 합니다.
const [fn, setFn] = useState(someFunction);

function handleClick() {
  setFn(someOtherFunction);
}

const [fn, setFn] = useState(() => someFunction);

function handleClick() {
  setFn(() => someOtherFunction);
}

Reference

profile
흑우 모르는 흑우 없제~

1개의 댓글

comment-user-thumbnail
2023년 8월 9일

정리가 잘 된 글이네요. 도움이 됐습니다.

답글 달기