ReactNextCentral

State를 스냅샷으로 사용

Published on
이 문서에서는 리액트에서 상태(State)를 스냅샷으로 사용하는 방법에 대해 설명합니다. 리액트에서 상태를 업데이트하면 새로운 렌더링이 트리거되며, 이러한 동작을 자세히 살펴봅니다.
Table of Contents

상태 변수는 읽고 쓸 수 있는 일반적인 JavaScript 변수처럼 보일 수 있습니다. 하지만 상태는 더 이상의 스냅샷과 비슷한 동작을 합니다. 상태를 설정하면 이미 가지고 있는 상태 변수가 변경되는 것이 아니라, 리렌더링을 트리거합니다.

배울 내용

  • 상태를 설정하는 것이 어떻게 리렌더링을 트리거하는지
  • 상태가 언제와 어떻게 업데이트되는지
  • 상태를 설정한 후에 즉시 업데이트되지 않는 이유
  • 이벤트 핸들러가 상태의 "스냅샷"에 접근하는 방법

상태 설정이 리렌더링을 트리거합니다

사용자 인터페이스를 클릭과 같은 사용자 이벤트에 직접 반응하여 변경된다고 생각할 수 있습니다. 리액트에서는 이러한 개념과 약간 다르게 동작합니다. 이전 페이지에서 상태 설정이 리액트에게 리렌더링을 요청한다는 것을 보았습니다. 즉, 인터페이스가 이벤트에 반응하려면 상태를 업데이트해야 합니다.

이 예제에서 "send"를 누를 때 setIsSent(true)는 리액트에게 UI를 리렌더링하도록 지시합니다.

Edit laughing-benji-yxnodj

버튼을 클릭할 때 일어나는 일은 다음과 같습니다.

  1. onSubmit 이벤트 핸들러가 실행됩니다.
  2. setIsSent(true)isSent를 true로 설정하고 새로운 리렌더링을 큐에 추가합니다.
  3. 리액트는 새로운 isSent 값을 기반으로 컴포넌트를 다시 렌더링합니다.

상태와 렌더링 사이의 관계를 좀 더 자세히 살펴보겠습니다.

렌더링은 시간에 대한 스냅샷을 취합니다

렌더링(Rendering)은 리액트가 함수인 컴포넌트를 호출하는 것을 의미합니다. 해당 함수에서 반환하는 JSX는 시간에 따른 UI의 스냅샷과 유사합니다. 해당 스냅샷에서는 프롭스, 이벤트 핸들러 및 로컬 변수가 모두 계산되며 해당 렌더링의 상태를 사용하여 계산됩니다.

사진이나 동영상 프레임과 달리 반환하는 UI "스냅샷"은 상호작용할 수 있습니다. 이는 이벤트 핸들러와 같은 로직을 포함하며 입력에 대한 응답으로 어떤 작업이 수행되어야 하는지를 지정합니다. 리액트는 화면을이 스냅샷과 일치하도록 업데이트하고 이벤트 핸들러를 연결합니다. 결과적으로 버튼을 누르면 JSX에서 지정한대로 클릭 핸들러가 트리거됩니다.

리액트가 컴포넌트를 다시 렌더링할 때:

  1. 리액트가 함수를 다시 호출합니다.
  2. 함수는 새로운 JSX 스냅샷을 반환합니다.
  3. 리액트는 반환된 스냅샷에 맞게 화면을 업데이트합니다.

컴포넌트의 상태로서, 상태는 함수가 반환한 JSX의 스냅샷이 아닌 일반 변수와 같이 함수가 반환된 후에 사라지는 것이 아닙니다. 상태는 실제로 리액트 자체에 "살아있는" 것처럼 함수 외부의 장소에 "거주"합니다. 리액트가 컴포넌트를 호출할 때 해당 렌더링을 위해 상태의 스냅샷을 제공합니다. 컴포넌트는 해당 렌더링의 상태 값을 사용하여 프롭스와 이벤트 핸들러가 포함된 JSX의 스냅샷을 반환합니다.

이 동작 방식을 보여주기 위해 작은 실험을 해보겠습니다. 이 예제에서 "3+" 버튼을 클릭하면 카운터가 세 번 증가할 것으로 예상할 수 있습니다. 왜냐하면 setNumber(number + 1)이 세 번 호출되기 때문입니다.

"3+" 버튼을 클릭할 때 어떤 일이 일어나는지 살펴보세요:

Edit dry-forest-7os05q

버튼을 클릭할 때 number가 한 번만 증가하는 것을 알 수 있습니다!

상태 설정은 다음 렌더링을 위해만 변경합니다. 첫 번째 렌더링에서 number는 0이었습니다. 이것은 렌더링의 onClick 핸들러에서도 setNumber(number + 1)이 호출된 후에도 number의 값이 여전히 0인 이유입니다.

<button onClick={() => {
  setNumber(number + 1);
  setNumber(number + 1);
  setNumber(number + 1);
}}>+3</button>

이 버튼의 클릭 핸들러가 리액트에게 요청하는 일은 다음과 같습니다.

  1. setNumber(number + 1): number가 0이므로 setNumber(0 + 1).
    • 리액트는 다음 렌더링에서 number를 1로 변경하도록 준비합니다.
  2. setNumber(number + 1): number가 0이므로 setNumber(0 + 1).
    • 리액트는 다음 렌더링에서 number를 1로 변경하도록 준비합니다.
  3. setNumber(number + 1): number가 0이므로 setNumber(0 + 1).
    • 리액트는 다음 렌더링에서 number를 1로 변경하도록 준비합니다.

setNumber(number + 1)을 세 번 호출했지만, 이 렌더링의 이벤트 핸들러에서 number의 값은 항상 0입니다. 따라서 이벤트 핸들러가 완료된 후 리액트는 number가 3이 아닌 1인 상태로 컴포넌트를 다시 렌더링합니다.

이를 시각화하는 방법은 상태 변수를 코드에서 해당 값으로 대체하는 것입니다. 이 렌더링에서 number 상태 변수가 0이기 때문에 이벤트 핸들러는 다음과 같이 보입니다.

<button onClick={() => {
  setNumber(0 + 1);
  setNumber(0 + 1);
  setNumber(0 + 1);
}}>+3</button>

다음 렌더링에서 number는 1이므로 해당 렌더링의 클릭 핸들러는 다음과 같이 보입니다.

<button onClick={() => {
  setNumber(1 + 1);
  setNumber(1 + 1);
  setNumber(1 + 1);
}}>+3</button>

따라서 버튼을 다시 클릭하면 카운터가 2로, 다음 클릭에서는 3으로 설정됩니다.

시간에 따른 상태

재미있었죠. 이 버튼을 클릭하면 경고창에 어떤 내용이 표시되는지 추측해보세요:

Edit cranky-breeze-qvkn25

이전과 같은 대체 방법을 사용하면 경고창이 "0"을 표시할 것으로 예상할 수 있습니다.

setNumber(0 + 5);
alert(0);

그러나 경고창에 타이머를 설정하여 컴포넌트가 다시 렌더링된 후에만 활성화되도록 할 경우에는 어떻게 될까요? "0"이 표시될까요 아니면 "5"가 표시될까요? 아래 예제에서 예상해보고 실행해보세요!

Edit small-mountain-9162ll

놀라셨나요? 대체 방법을 사용하면 사용자가 해당 상태로 상호작용할 때 스냅샷으로 예약된 상태가 경고 실행 시점에 사용됩니다.

setNumber(0 + 5);
setTimeout(() => {
  alert(0);
}, 3000);

리액트는 경고가 실행되는 시점에 상태가 변경되었을 수 있지만, 사용자가 상호작용하는 시점에서의 상태 스냅샷을 사용하여 예약되었습니다!

렌더링 내에서 상태 변수의 값은 변경되지 않습니다. 이벤트 핸들러의 코드가 비동기적인 경우에도 해당 렌더링의 onClick에서 number의 값은 계속 0입니다. 리액트가 컴포넌트를 호출하여 UI의 스냅샷을 취할 때, number의 값이 "고정"되었습니다.

이로 인해 이벤트 핸들러가 시간에 민감한 오류를 일으키지 않도록 도와줍니다. 다음과 같은 시나리오를 상상해보세요. 다음과 같은 양식이 있는데, 이 양식은 5초 후에 메시지를 전송합니다.

  1. "Send" 버튼을 눌러 "Hello"를 Alice에게 보냅니다.
  2. 5초 지연이 끝나기 전에 "To" 필드의 값을 "Bob"으로 변경합니다.

경고창이 어떤 내용을 표시할지 예측해보세요. "You said Hello to Alice"가 표시될까요? 아니면 "You said Hello to Bob"이 표시될까요? 예측한 후에 실행해보세요!

Edit nifty-burnell-r7e88v

리액트는 렌더링 내에서 상태 값이 "고정"됩니다. 코드가 실행되는 동안 상태가 변경되었는지 걱정할 필요가 없습니다.

하지만 다음 렌더링 이전에 최신 상태를 읽고 싶다면, 다음 페이지에서 다루는 상태 업데이터 함수를 사용해야 합니다!

요약

  • 상태를 설정하면 새로운 렌더링이 요청됩니다.
  • 리액트는 상태를 컴포넌트 외부에 저장합니다.
  • useState를 호출하면 리액트는 해당 렌더링의 상태 스냅샷을 제공합니다.
  • 변수와 이벤트 핸들러는 리렌더링 간에 "유지"되지 않습니다. 각 렌더링에는 고유한 이벤트 핸들러가 있습니다.
  • 각 렌더링(및 해당 내부 함수)은 항상 해당 렌더링에게 주어진 상태 스냅샷을 "보게 됩니다.
  • 상태를 이벤트 핸들러에서 사용할 때 상태를 마음속으로 대체하여 생각할 수 있습니다. 렌더링된 JSX를 생각하는 방식과 유사합니다.
  • 과거에 생성된 이벤트 핸들러는 해당 렌더링에서 생성된 상태 값을 가집니다.