(번역)State를 스냅샷으로 다루기 - NEW 리액트 공식문서

hongregii·2023년 3월 3일
0

새로운 리액트 공식문서 beta - State as a Snapshot

기존 공식문서 바탕으로 리액트를 정리하다 보니, 함수형 + hooks 패러다임에서 확 와닿지 않게 돼 있는 경우가 많았다.

그 중에서도 가장 눈에 걸리는 부분이 바로 state 였는데, 새로운 문서를 읽어보니 setState()의 비동기성을 이렇게 이해했어야 맞다는 것을 깨달아버렸다...

한국어 번역이 어서 나오기를 기대하면서, 일단 이 문서는 사견을 자제하고 그대로 번역해보겠다.

State as a Snapshot

State 변수가 읽고 쓸 수 있는 일반 JS 변수처럼 보일 수 있지만, State 는 변수보다는 스냅샷처럼 행동한다. setState()는 기존 State 변수를 바꾸는 게 아니라, 리렌더링을 트리거한다는 사실!

이번 문서에서는

  • setState() 가 어떻게 리렌더링을 트리거하는지
  • 언제, 어떻게 state가 업데이트되는지
  • 왜 state가 setState() 이후에 즉시 업데이트 되지 않는지
  • 이벤트 핸들러 함수가 state의 "스냅샷" 에 어떻게 접근하는지

를 배워보겠다.

setState()는 리렌더링을 발생시킨다.

클릭 과 같은 이벤트 직후에 UI가 곧바로 바뀐다고 생각했을 수 있다. 그러나, 리액트는 그것과는 조금 다르게 작동한다.
이전 페이지
에서 setState()가 리액트에게 리렌더링을 요청한다는 것을 배웠을 것이다. 다시 말하면, 인터페이스가 반응하게 하려면, state를 업데이트 해야 한다는 뜻이다!

다음 예시에서 "전송" 버튼을 누르면, setIsSent(true) 코드는 리액트에게 UI를 리렌더링 하라고 알려준다.

import { useState } from 'react';

export default function Form() {
  const [isSent, setIsSent] = useState(false);
  const [message, setMessage] = useState('Hi!');
  // isSent state가 true면 메시지 보내는 중 렌더링
  if (isSent) {
    return <h1>메세지 보내는 중!</h1>
  }
  return (
    <form onSubmit={(e) => {
      e.preventDefault();
      setIsSent(true);
      sendMessage(message);
    }}>
      <textarea
        placeholder="Message"
        value={message}
        onChange={e => setMessage(e.target.value)}
      />
      <button type="submit">전송</button>
    </form>
  );
}

function sendMessage(message) {
  // 메세지 전송하는 로직
}

마크다운에서 리액트 임포트를 어떻게 하는가.. 아무래도 못하겠지? velog의 한계... ㅠㅠ

버튼을 누르면 이렇게 됨 :

  1. form 엘리멘트의 onSubmit 이벤트 핸들러가 실행됨

  2. setIsSent(true) 코드는 isSent state를 true로 설정(set)하고, 새로운 렌더를 큐에 올려놓음

  3. 리액트는 새로운 isSent값에 따라 컴포넌트를 리렌더링

이제 state와 렌더링 간 관계를 더 자세히 보자.

렌더링은 특정 시점의 스냅샷을 가져온다

"렌더링" 이란 리액트가 컴포넌트를 불러온다는 뜻이다. 그런데 그 컴포넌트는 함수. 이 함수에서 return 되는 값은 JSX. 이 JSX는 특정 시점에서의 스냅샷과 같다. props, 이벤트 핸들러, 지역변수(*const, let)들은 렌더링 되는 시점의 state를 사용하여 계산된다는 것이다!

사진이나 영화의 프레임과는 다르게, UI의 "스냅샷"은 상호작용이 가능하다. 이 스냅샷들에는 인풋에 따라 어떤 결과가 나오는지를 특정하는 이벤트 핸들러 같은 로직들이 포함되어있다. 리액트는 이 스냅샷에 맞도록 화면을 업데이트하고, 이벤트 핸들러를 연결한다.
그 결과, 버튼을 누르는 것이 JSX의 클릭 핸들러를 촉발하는 것.

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

  1. 리액트가 함수를 다시 호출함

  2. 함수가 새로운 JSX 스냅샷 을 리턴함

  3. 리액트는 이 새로 만들어진 스냅샷 에 맞도록 화면을 업데이트

    Illustrated by Rachel Lee Nabors - 이미지 출처는 공식문서

    리액트가 함수 재실행 스냅샷 계산 DOM 트리 업데이트

state는 컴포넌트의 메모리

함수 속 일반 변수는 함수가 리턴하면 사라진다. 그러나 state 는 리액트 그 자체에서 "살고 있다" - 함수 밖 리액트의 선반 속에 사는 것과 같다!
컴포넌트를 호출할 때, 리액트는 그 해당 렌더를 위한 state의 스냅샷을 준다. 컴포넌트는 그 렌더의 state 값을 이용하여 계산한 새로운 props와 이벤트 핸들러를 포함한 UI의 스냅샷을 리턴한다.

일러스트가 세 개 있는데 생략하겠음

실험 : setState() 한번에 세번 쓰면 ?

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
    // 버튼 한번 누르면 setState()를 세번 해보자!
        setNumber(number + 1);
        setNumber(number + 1);
        setNumber(number + 1);
      }}>+3</button>
    </>
  )
}

버튼을 한 번 누르면 어떻게 될까? setNumber(number + 1)가 세번 있으니까 number state가 0 → 3 이 되겠지?

놀랍게도 아니다. number는 0 → 1 로 바뀜.

setState()는 state를 다음 렌더링 을 위해서만 바꾼다.

첫 렌더에서 number는 0이었다. 그러니까 뭐다? 그 렌더의 onClick 이벤트핸들러 에서는 number가 계속 0이라는 말이다! setNumber()가 호출된 뒤에도.

// 아까 그 부분에서 버튼만 땀
<button onClick={() => {
  setNumber(number + 1);
  setNumber(number + 1);
  setNumber(number + 1);
}}>+3</button>

버튼의 click 핸들러 동작을 자세히 살펴보자.

  1. setNumber(number + 1) : number값이 0 이니까 → setNumber(0 + 1)

    • 리액트는 다음 렌더링에서 number1로 바꿀 준비함
  2. setNumber(number + 1) : number값이 0 이니까 → setNumber(0 + 1)

    • 리액트는 다음 렌더링에서 number1로 바꿀 준비함
  3. setNumber(number + 1) : number값이 0 이니까 → setNumber(0 + 1)

    • 리액트는 다음 렌더링에서 number1로 바꿀 준비함

이 렌더링에서의 number가 항상 0이니까, number값을 1로 세번 만들어주는 것이다. 이벤트 핸들러 실행이 끝나도, 3이 아니라 1이 화면에 나오는 이유.

코드를 더 뜯어보면, 이렇게 되는 셈. (이해가 됐으면 skip 해도 좋음. 공식문서 친절하군...)

// 처음 누르면
<button onClick={() => {
  setNumber(0 + 1);
  setNumber(0 + 1);
  setNumber(0 + 1);
}}>+3</button>

다음 렌더링에서 number1. 그 렌더 에서의 클릭 핸들러는 이렇다.

// 두번째 렌더에서 누르면 (= 세번째 렌더링 트리거)
<button onClick={() => {
  setNumber(1 + 1);
  setNumber(1 + 1);
  setNumber(1 + 1);
}}>+3</button>

시간에 따라 state는...

setState() 이후 alert 하면 어떻게 될까?

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 5);
    // setState() 이후 alert 하면?
        alert(number);
      }}>+5</button>
    </>
  )
}

위에서 잘 배웠으면, alert는 0을 띄울 것이라는 사실을 알 수 있을 것이다.

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

그렇다면 alert에 타이머를 붙인다면 ?????
리렌더링 이후 에 alert가 작동하게 된다면 ????
"0"이 나올까? "5"가 나올까?

<button onClick={() => {
        
        //setState() 이후
        setNumber(number + 5);
  
        //setTimeOut 안에 alert
        setTimeout(() => {
          alert(number);
        }, 3000);
      }}>+5</button>

결과 : 화면이 5로 바뀐 다음 3초 뒤에 alert(0)이 나옴

뜯어보면 이렇다.

// number 값은 0
setNumber(0 + 5);
// number 값은 0
setTimeout(() => {
  alert(0);
}, 3000);

alert 가 작동하는 시점에 리액트에 저장된 state 값은 바뀌었을 지 모르나, 사용자가 버튼을 눌렀을 시점의 스냅샷 으로 alert 안 number가 계산된 것이다!

state 값은 한 렌더 안에서 절대 바뀌지 않는다.

이벤트 핸들러 코드가 비동기여도 마찬가지.
해당 렌더onClick안에서 setNumber(number+5)가 아무리 호출돼도 number값은 계속 0이다. 리액트가 컴포넌트를 호출함과 동시에 찍힌 스냅샷에서 값이 고정된 것이다!

타이밍 실수를 줄이는 예시를 보여주겠다. 다음은 5초 딜레이 뒤에 메시지를 보내는 form. 시나리오는 이렇다 :

  1. [전송] 버튼 눌러서 Alice에게 "안녕하세요"를 보낸다.
  2. 5초 딜레이가 끝나기 전, To state를 "Bob"으로 바꿔야 함.
import { useState } from 'react';

export default function Form() {
  const [to, setTo] = useState('Alice');
  const [message, setMessage] = useState('안녕하세요');

  function handleSubmit(e) {
    e.preventDefault();
    setTimeout(() => {
      alert(`You said ${message} to ${to}`);
    }, 5000);
  }

  return (
    
    // 전송버튼 누르면 handleSubmit
    <form onSubmit={handleSubmit}>
      <label>
        To:{' '}
        <select
          value={to}

      // select 태그 이벤트핸들러 안에 setState
          onChange={e => setTo(e.target.value)}>
          <option value="Alice">Alice</option>
          <option value="Bob">Bob</option>
        </select>
      </label>

     // 인풋창
       <textarea
        placeholder="Message"
        value={message}
        onChange={e => setMessage(e.target.value)}
      />
    
    // [전송] 버튼
    <button type="submit">전송</button>
    </form>
  );
}
  • state는 message, to
  • 인풋창에 입력하면 setMessage. default값 "안녕하세요"
  • select 창 선택하면 setTo
  • 전송버튼 누르면 handleSubmit 실행
  • handleSubmit 안에 state 출력하는 alert창

alert는 무엇을 출력할까? (= 버튼 눌렀을 때 state는 무엇을 출력할까)

리액트가 한 렌더링에서 state값들을 고정하기 때문에 코드가 돌아가는 중에 state값이 바뀌었는지 걱정하지 않아도 된다.

그런데 리렌더링 전에 최신 state를 읽고 싶으면 어떻게 할까?
그럴 때는 다음 장에서 설명할 state updater 함수를 사용하자

7줄 요약

  • state 설정은 새로운 렌더링을 요청한다
  • 리액트는 컴포넌트 밖에 state를 저장한다 (마치 선반처럼)
  • useState를 호출하면, 리액트가 해당 렌더링을 위한 state의 스냅샷을 준다.
  • 변수와 이벤트 핸들러는 리렌더링에서 살아남지 못한다. 모든 렌더링은 자신만의 이벤트 핸들러를 갖는다.
  • 모든 렌더 (그리고 렌더 속 함수들)은 항상 리액트가 해당 렌더 에 제공하는 상태의 스냅샷을 들여다본다.
  • 이벤트 핸들러에서 마음속으로 상태를 바꿔서 생각해볼 수 있다. 렌더링된 JSX를 상상하는 것과 같음
  • 과거에 만들어진 이벤트 핸들러는 그것들이 생성됐을 때의 state 값을 가지고 있다.
profile
잡식성 누렁이 개발자

0개의 댓글