💻 Frontend

1. React Hook, useState

category
💻 Frontend

useState

useState는 컴포넌트에 상태 변수를 추가할 수 있는 React Hook 입니다.
const [state, setState] = useState(initialState);
 

문법

1. useState(initialState)

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

파라미터

  • initialState: 원하는 초기 state 값. 모든 유형의 값이 될 수 있지만, 함수에는 특별한 동작이 있습니다. 이 인수는 초기 렌더링 후에 무시됩니다.
    • 함수를 initialState로 전달하면 초기화 함수로 처리됩니다. 순수해야 하고 인수를 사용하지 않아야 하며 무슨 값이든 반환해야 합니다. React는 컴포넌트를 초기화할 때 초기화 함수를 호출하고 반환 값을 초기 상태로 저장합니다.

반환값

useState는 정확히 두 개의 값이 있는 배열을 반환합니다:
  1. 현재 state. 첫번째 렌더링 중에 전달한 initalState와 일치합니다.
  1. state를 다른 값으로 업데이트하고 다시 렌더링을 트리거할 수 있는 set 함수입니다.

주의 사항

  • useState 는 hook 이므로 컴포넌트 또는 커스텀 hook의 최상위 레벨에서만 호출할 수 있습니다. 루프나 조건 내에서 호출할 수 없습니다. 필요한 경우 새 컴포넌트를 추출하고 해당 컴포넌트로 state를 이동합니다.
  • Strict 모드에서 React는 우발적인 불순물을 찾기 위해 초기화 함수를 두 번 호출합니다. 이것은 개발 전용 동작이며 실제 제품(서비스)에 영향을 미치지 않습니다. 초기화 함수가 순수한 경우(순수해야 함) 동작에 영향을 주지 않아야 합니다. 호출 중 하나의 결과는 무시됩니다.
 

2. set 함수, setSomething(nextState)

useState에 의해 반환된 set함수를 사용하면 state를 다른 값으로 업데이트하고, 다시 렌더링을 트리거할 수 있습니다. 다음 state를 직접 전달하거나 이전 state에서 계산하는 함수를 전달할 수 있습니다.
const [name, setName] = useState('Edward'); function handleClick() { // 1. 직접 상태 전달 setName('Taylor'); // 2. 이전 state에서 계산 setAge(a => a + 1);

파라미터

  • nextState: 원하는 상태 값입니다. 모든 타입의 값이 올수도 있지만 함수에는 특별한 동작이 있습니다.
    • 함수를 nextState로 전달하면 업데이트 함수로 처리 됩니다. 해당 함수는 순수해야하고 pending(보류)상태를 유일한 인수로 가져와야 하며 다음 state를 반환해야 합니다. React는 업데이트 기능을 대기열에 넣고 컴포넌트를 다시 렌더링합니다. 다음 렌더링 동안 React는 대기 중인 모든 업데이터를 이전 state에 적용하여 다음 state를 계산합니다.

반환값

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

주의 사항

  • set함수는 다음 렌더링에 대한 state변수만 업데이트 합니다. set함수를 호출한 후 state변수를 가져오면 여전히 호출 전 화면에 있던 이전 값을 얻게됩니다.
  • 공한 새 값이 Object.is 비교에 의해 결정된 현재 상태와 동일하면 React는 컴포넌트와 그 자식을 다시 렌더링하지 않습니다. 이것은 최적화입니다. 어떤 경우에는 React가 자식을 건너뛰기 전에 컴포넌트를 호출해야 할 수도 있지만 코드에 영향을 미치지 않아야 합니다.
  • React는 state 업데이트를 일괄 처리합니다. 모든 이벤트 핸들러가 실행되고, 해당 set함수를 호출한 후 화면을 업데이트합니다. 이렇게 하면 단일 이벤트 중에 여러 번 다시 렌더링 되는 것을 방지할 수 있습니다. 드문 경우지만 예를 들어 DOM에 액세스하기 위해 React가 화면을 더 일찍 업데이트하도록 강제해야 하는 경우 flushSync를 사용할 수 있습니다.
  • 렌더링 중에 set함수를 호출하는 것은 현재 렌더링 컴포넌트 내에서만 허용됩니다. React는 출력을 버리고 즉시 새 state로 다시 렌더링하려고 시도합니다. 이 패턴은 거의 필요하지 않지만 이전 렌더링의 정보를 저장하는데 사용할 수 있습니다.
  • Strict모드에서 React는 우발적인 불순물을 찾는 데 도움을 주기 위해 업데이트 기능을 두 번 호출합니다. 이것은 개발 전용 동작이며 실제 제품(서비스)에 영향을 미치지 않습니다. 초기화 함수가 순수한 경우(순수해야 함) 동작에 영향을 주지 않아야 합니다. 호출 중 하나의 결과는 무시됩니다.
 

사용법

컴포넌트에 상태 추가

컴포넌트의 최상위 레벨에서 useState를 호출하여 state 변수를 선언합니다.
import { useState } from 'react'; function MyComponent() { const [age, setAge] = useState(42); const [name, setName] = useState('Taylor'); // ...
배열 구조 분해를 사용하여 [something, setSomething]과 같은 state 변수의 이름을 지정합니다.
useState는 정확히 두 개의 값이 있는 배열을 반환합니다:
  1. current state는 초기에 사용자가 제공한 initial state(초기 상태)로 설정됩니다.
  1. 상호 작용에 대한 응답으로 다른 값으로 변경할 수 있는 set 함수입니다.
화면의 내용을 업데이트하려면 next state로 set 함수를 호출하십시오.
function handleClick() { setName('Robin'); }
React는 next state를 저장하고 새 값으로 구성 요소를 다시 렌더링하고 UI를 업데이트합니다.
 
🚨
set 함수를 호출해도 이미 실행 중인 코드의 현재 state는 변경되지 않습니다.
function handleClick() { setName('Robin'); console.log(name); // Still "Taylor"! }
다음 렌더링부터 시작하여 useState가 반환하는 항목에만 영향을 미칩니다.
 

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

age42세라고 가정합니다. 이 핸들러는 setAge(age + 1)세 번 호출합니다.
function handleClick() { setAge(age + 1); // setAge(42 + 1) setAge(age + 1); // setAge(42 + 1) setAge(age + 1); // setAge(42 + 1) }
그러나 클릭 한 번으로 45세가 아닌 43세가 됩니다! 이는 set 함수를 호출해도 이미 실행중인 코드의 수명 state 변수가 업데이트 되지 않기 때문입니다. 따라서 setAge(age + 1) 호출은 setAge(43)이 됩니다.
이 문제를 해결하려면 next state 대신 setAge업데이터 함수를 전달할 수 있습니다:
function handleClick() { setAge(a => a + 1); // setAge(42 => 43) setAge(a => a + 1); // setAge(43 => 44) setAge(a => a + 1); // setAge(44 => 45) }
여기서 a => a + 1은 업데이트 기능입니다. pending state를 가져와 next state를 계산합니다.
React는 업데이트 기능을 대기열에 넣습니다. 그런 다음 다음 렌더링 중에 동일한 순서로 호출합니다:
  1. a => a + 1은 pending state로 42를 수신하고 next state로 43을 반환합니다.
  1. a => a + 1은 pending state로 43을 수신하고 next state로 44를 반환합니다.
  1. a => a + 1은 pending state로 44를 수신하고 next state로 45를 반환합니다.
대기중인 다른 업데이트가 없으므로 React는 결국 현재 state로 45를 저장합니다.
규칙으로 state 변수 이름의 첫 글자를 따와 pending state 인수의 이름을 a for age와 같이 지정하는 것이 일반적입니다. 그러나 prevAge 또는 더 명확하다고 생각되는 것으로 지정할 수도 있습니다.
 

객체 및 배열 state 업데이트

객체와 배열을 state에 넣을 수 있습니다. React에서 state는 읽기 전용으로 간주되므로 기존 객체를 변경하기보다는 교체해야합니다. 예를 들어 state에 양식 객체가 있는 경우 변경하지 마세요:
// 🚩 이와 같이 객체의 state를 변경하지 마세요: form.firstName = 'Taylor';
// ✅ state를 새 객체로 바꾸세요. setForm({ ...form, firstName: 'Taylor' });
 

초기 state 재생성 방지

React는 초기 state를 한 번 저장하고 다음 렌더링에서 무시합니다.
function TodoList() { const [todos, setTodos] = useState(createInitialTodos()); // ...
createInitialTodos()의 결과는 초기 렌더링에만 사용되지만 여전히 모든 렌더링에서 이 함수를 호출하고 있습니다. 큰 배열을 만들거나 비용이 많이 드는 계산을 수행하는 경우 낭비가 될 수 있습니다.
이 문제를 해결하려면 useState초기화 함수를 대신 전달할 수 있습니다.
function TodoList() { const [todos, setTodos] = useState(createInitialTodos); // ...
호출 값으로 createInitialTodos()가 아니라 함수 자체인 createInitialTodos를 전달하고 있는 것을 유의해주세요. useState에 함수를 전달하면 React는 초기화 중에만 호출합니다.
React는 개발 단계에서 초기화 프로그램이 순수한지 확인하기 위해 두 번 호출할 수 있습니다.
해당 예제를 통해 차이점을 확인해보세요. 동작에 눈에 띄는 차이는 없지만 이 useState(createInitialTodos())코드는 덜 효율적입니다.
 

키로 상태 재설정

목록을 렌더링할 때(array.map) 종종 key 특성을 접하게 됩니다. 그러나 다른 용도로도 사용됩니다.
컴포넌트에 다른 키를 전달하여 컴포넌트의 상태를 재설정할 수 있습니다. 아래 예시에서 reset button은 form에 대한 key로 전달되는 version state값을 변경합니다.
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> </> ); }
reset 관련된 코드를 작성하지 않아도 button 클릭 시 Form 컴포넌트 내부에 있는 name 값이 초기화 되는 것을 확인하실 수 있습니다. button 클릭 시 version 값을 변경시키는데 이 과정에서 렌더링을 다시 하게 됩니다. 그래서 Form 컴포넌트를 새로 렌더링할때에는 초기값인 Taylor로 바뀌는 것이죠.
해당 내용은 공식문서에서 확인하실 수 있습니다.
 

이전 렌더링의 정보 저장

일반적으로 이벤트 핸들러에서 state를 업데이트합니다. 그러나 드문 경우지만 렌더링에 대한 응답으로 state를 조정해야 할 수 있습니다. 예를 들어 props가 변경될 때, state 변수를 변경해야 할 수 있습니다.
대부분의 경우 다음이 필요하지 않습니다.
  • 필요한 값을 현재 props 또는 다른 state에서 완전히 계산할 수 있는 경우 해당 중복 state를 모두 제거하세요. 너무 자주 다시 계산하는 것이 걱정된다면 useMemo Hook이 도움이 될 수 있습니다.
  • 전체 컴포넌트 트리의 state를 재설정하려면 컴포넌트에 다른 key를 전달하십시오.
  • 가능한 경우 이벤트 핸들러에서 모든 관련 state를 업데이트하십시오.
드문 경우이지만 컴포넌트가 렌더링되는 동안 set 함수를 호출하여 지금까지 렌더링된 값을 기반으로 state를 업데이트하는 데 사용할 수 있는 패턴이 있습니다.
예제로 확인해보죠. 이 CountLabel 컴포넌트는 props count을 표시합니다.
export default function CountLabel({ count }) { return <h1>{count}</h1> }
마지막 변경 이후 카운터가 증가했는지 감소했는지 표시하고 싶다고 가정해 보겠습니다. count prop은 이것을 알려주지 않습니다 — 이전 값을 추적해야 합니다. prevCount 상태 변수를 추가하여 추적하십시오.
카운트의 증가 또는 감소 여부를 유지하기 위해 trend라는 또 다른 state 변수를 추가합니다. prevCountcount를 비교하고, 같지 않으면 prevCounttrend를 모두 업데이트합니다.
이제 현재 count props와 마지막 렌더링 이후 어떻게 변경되었는지 모두 표시할 수 있습니다:
import { useState } from 'react'; 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 함수를 호출하는 경우 prevCount !== count와 같은 조건 내부에 있어야 하며 조건 내부에 setPrevCount(count)와 같은 호출이 있어야 합니다. 그렇지 않으면 컴포넌트가 충돌할 때까지 루프에서 다시 렌더링됩니다. 또한 이와 같이 현재 렌더링된 컴포넌트의 state만 업데이트할 수 있습니다. 렌더링 중에 다른 컴포넌트의 set 함수를 호출하면 오류가 발생합니다.
마지막으로, set 호출은 여전히 변형 없이 state를 업데이트해야 합니다. 이는 순수 함수의 다른 규칙을 위반할 수 있다는 의미는 아닙니다.
이 패턴은 이해하기 어려울 수 있으며 일반적으로 피하는 것이 가장 좋습니다. 그러나 effect에서 state를 업데이트하는 것보다 낫습니다. 렌더링 중에 set 함수를 호출하면 React는 컴포넌트가 return 문으로 종료된 직후와 자식을 렌더링하기 전에 해당 컴포넌트를 다시 렌더링합니다. 이렇게 하면 자식이 두 번 렌더링할 필요가 없습니다. 나머지 컴포넌트 함수는 계속 실행되며 결과는 버려집니다. 해당 조건이 모든 Hook 호출보다 낮은 경우 더 일찍 렌더링을 다시 시작하는 조기 return;을 추가할 수 있습니다.
 

문제 해결

state를 업데이트했지만, 로그에는 이전 값이 보입니다

set 함수를 호출해도 실행 중인 코드의 state는 변경되지 않습니다:
function handleClick() { console.log(count); // 0 setCount(count + 1); // 리렌더링 요청과 함께 1로 변경 console.log(count); // 아직 카운트 값이 0! setTimeout(() => { console.log(count); // 해당 코드 또한 0! }, 5000); }
이는 상태가 snapshot(스냅샷)처럼 작동하기 때문입니다. state를 업데이트하면 새 state 값으로 다른 렌더링이 요청되지만, 이미 실행 중인 이벤트 핸들러의 JavaScript 변수에는 영향을 미치지 않습니다.
next state를 사용해야 하는 경우 set 함수에 전달하기 전에 변수에 저장할 수 있습니다:
const nextCount = count + 1; setCount(nextCount); console.log(count); // 0 console.log(nextCount); // 1
 

state를 변경했는데 화면에는 반영이 되지 않아요

React는 Object.is 비교에 의해 결정된 대로 next state가 이전 state와 같으면 업데이트를 무시합니다. 이것은 일반적으로 state에서 객체 또는 배열을 직접 변경할 때 발생합니다.
obj.x = 10; // 🚩 Wrong: 기존 객체 변경 setObj(obj); // 🚩 아무것도 하지 않는다.
기존 obj 객체를 변경하고 setObj에 다시 전달했기 때문에 React는 업데이트를 무시했습니다. 이 문제를 해결하려면 개체와 배열을 변경하는 대신 교체하고 있는지 확인해야 합니다.
// ✅ Correct: 새로운 객체 생성하기 setObj({ ...obj, x: 10 });
 

다음 오류가 발생합니다. “리렌더링이 너무 많습니다."

다음과 같은 오류가 표시될 수 있습니다. “리렌더링이 너무 많습니다. React는 무한 루프를 방지하기 위해 렌더링 수를 제한합니다." 일반적으로 이는 렌더링 중에 state를 무조건 설정한다는 의미이므로 컴포넌트는 렌더링, state 설정(렌더링 유발), 렌더링, state 설정(렌더링 유발) 등의 루프에 들어갑니다. 해당 오류는 이벤트 처리기를 지정하는 실수로 인해 자주 발생합니다.
🚨
렌더링 발생 → setState → setState로 인한 렌더링 유발 → 렌더링 발생 → 무한 렌더링
// 🚩 Wrong: 렌더링 중에 처리기를 호출합니다. // onClick 속성 안에서 호출하였지만 즉시 함수를 호출하는 것으로 판단하여 해당 함수를 계속 호출함 return <button onClick={handleClick()}>Click me</button> // ✅ Correct: 이벤트 핸들러를 전달합니다. return <button onClick={handleClick}>Click me</button> // ✅ Correct: 인라인 함수를 전달합니다. return <button onClick={(e) => handleClick(e)}>Click me</button>
 

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

Strict 모드에서 React는 일부 함수를 한 번이 아닌 두 번 호출합니다:
function TodoList() { // 해당 컴포넌트는 모든 렌더링에 대해 두 번 실행됩니다. const [todos, setTodos] = useState(() => { // 이 초기화 함수는 초기화 도중 두 번 실행됩니다. return createTodos(); }); function handleClick() { setTodos(prevTodos => { // 이 업데이트 함수는 클릭할 때마다 두 번 실행됩니다. return [...prevTodos, createTodo()]; }); } // ...
이 개발 전용 동작은 컴포넌트를 순수하게 유지하는 데 도움이 됩니다. React는 호출 중 하나의 결과를 사용하고 다른 호출의 결과는 무시합니다. 컴포넌트, 초기화 및 업데이트 기능이 순수하다면 로직에 영향을 미치지 않습니다. 그러나 실수로 순수하지 않은 경우 실수를 알아차리는 데 도움이 됩니다.
예를 들어, 이 순수하지 않은 업데이트 함수는 state의 배열을 변경합니다:
setTodos(prevTodos => { // 🚩 Mistake: mutating state prevTodos.push(createTodo()); });
React가 업데이트 함수를 두 번 호출하기 때문에 todo가 두 번 추가된 것을 볼 수 있으므로 실수가 있음을 알 수 있습니다. 해당 예시에서는 배열을 변경하는 대신 교체하여 실수를 수정할 수 있습니다.
setTodos(prevTodos => { // ✅ Correct: replacing with new state return [...prevTodos, createTodo()]; });
이제 이 업데이트 함수는 순수하므로 추가로 호출해도 동작에 차이가 없습니다. 이것이 React가 두 번 호출하면 실수를 찾는 데 도움이 되는 이유입니다. 컴포넌트, 초기화 및 업데이트 함수만 순수해야 합니다. 이벤트 핸들러는 순수할 필요가 없으므로 React는 이벤트 핸들러를 두 번 호출하지 않습니다.
자세한 내용은 구성 요소를 순수하게 유지하기를 참조하세요.
 

state를 함수로 설정하려고 하는데, 그전에 호출됩니다.

const [fn, setFn] = useState(someFunction); function handleClick() { setFn(someOtherFunction); }
함수를 전달하고 있기 때문에 React는 someFunction이 초기화 함수이고 someOtherFunction이 업데이트 함수라고 가정하므로 이를 호출하고 결과를 저장하려고 합니다. 실제로 함수를 저장하려면 두 경우 모두 앞에 () =>를 넣어야 합니다.
그런 다음 React는 전달한 함수를 저장합니다:
const [fn, setFn] = useState(() => someFunction); function handleClick() { setFn(() => someOtherFunction); }
 

마무리

state값으로 nextState를 변경하는 것이랑 prevState로 nextState를 변경하는 차이점을 명확히 알게 되었다.
state를 업데이트하면 새 state 값으로 다른 렌더링이 요청되지만, 이미 실행 중인 이벤트 핸들러의 JavaScript 변수에는 영향을 미치지 않습니다. 이러한 사실로 인해 로그찍을 때 항상 늦게 반영된다는 것을 알게되었다.
개념을 공부하다보니 순수 함수의 개념이 무엇인지 궁금해졌다. 해당 내용도 블로그에 정리해봐야겠다!

References