[리액트 공식문서 읽기] MANAGING STATE - Reating to Input with State

JaeHong Jeong·2023년 9월 21일
post-thumbnail

Overview

리액트는 UI를 조작하는 선언적인 방법을 제공한다. UI의 개별 부분을 직접 조작하는 대신 컴포넌트가 가질 수 있는 다양한 상태를 설명하고 사용자 입력에 응답하여 상태 간에 전환을 한다. 이는 디자이너가 UI에 대해 생각하는 방법과 유사하다.

How declarative UI compares to imperative

UI 상호작용을 설계할 때 사용자 작업에 따라 UI가 어떻게 변경되는지 생각해 볼 수 있다. 사용자가 답변을 제출할 수 있는 약식을 고려해봐라.

  • 양식에 내용을 입력하면 ‘Submit’이 활성화된다.
  • ‘Submit’을 누르면 양식과 버튼이 모두 비활성화되고 스피너가 나타난다.
  • 네트워크 요청이 성공하면 양식이 숨겨지고 ‘Thank you’ 메시지가 나타난다.
  • 네트워크 요청이 실패하면 오류 메시지가 표시되고 양식이 다시 활성화된다.

명령형 프로그래밍에서 위의 내요은 상호 작용을 구현하는 방법과 직접적으로 일치한다. 방금 일어난 일에 따라 UI를 조작하려면 정확한 지침을 작성해야한다. 이에 대해 생각하는 또 다른 방법은 다음과 같다. 차를 타고 누군가 옆에 타고 가서 어디로 가야 하는지 차례대로 말해준다고 상상해봐라

그들은 당신이 어디로 가고 싶은지 모르고 단지 당신의 명령을 따른 뿐이다. (그리고 방향이 틀리면 결국 잘못된 위치에 있게 된다.) 스피너에서 버튼까지 각 요소에 “명령”을 내려 컴퓨터에 UI 업데이트 방법을 알려주어야 하기 때문에 필수라고 한다.

이 명령형 UI 프로그래밍 예에서는 양식이 리액트 없이 구축되었다. 브라우저 DOM만 사용한다.

// index.js

async function handleFormSubmit(e) {
  e.preventDefault();
  disable(textarea);
  disable(button);
  show(loadingMessage);
  hide(errorMessage);
  try {
    await submitForm(textarea.value);
    show(successMessage);
    hide(form);
  } catch (err) {
    show(errorMessage);
    errorMessage.textContent = err.message;
  } finally {
    hide(loadingMessage);
    enable(textarea);
    enable(button);
  }
}

function handleTextareaChange() {
  if (textarea.value.length === 0) {
    disable(button);
  } else {
    enable(button);
  }
}

function hide(el) {
  el.style.display = 'none';
}

function show(el) {
  el.style.display = '';
}

function enable(el) {
  el.disabled = false;
}

function disable(el) {
  el.disabled = true;
}

function submitForm(answer) {
  // Pretend it's hitting the network.
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (answer.toLowerCase() == 'istanbul') {
        resolve();
      } else {
        reject(new Error('Good guess but a wrong answer. Try again!'));
      }
    }, 1500);
  });
}

let form = document.getElementById('form');
let textarea = document.getElementById('textarea');
let button = document.getElementById('button');
let loadingMessage = document.getElementById('loading');
let errorMessage = document.getElementById('error');
let successMessage = document.getElementById('success');
form.onsubmit = handleFormSubmit;
textarea.oninput = handleTextareaChange;
// index.html

<form id="form">
  <h2>City quiz</h2>
  <p>
    What city is located on two continents?
  </p>
  <textarea id="textarea"></textarea>
  <br />
  <button id="button" disabled>Submit</button>
  <p id="loading" style="display: none">Loading...</p>
  <p id="error" style="display: none; color: red;"></p>
</form>
<h1 id="success" style="display: none">That's right!</h1>

<style>
* { box-sizing: border-box; }
body { font-family: sans-serif; margin: 20px; padding: 0; }
</style>

UI를 명령적으로 조작하는 것은 격리된 예에서는 충분히 잘 작동하지만 더 복잡한 시스템에서는 관리하기가 기하급수적으로 더 어려워진다. 이와 같은 다양한 양식으로 가득 찬 페이지를 업데이트한다고 상상해 봐라. 새로운 UI 요소나 새로운 상호 작용을 추가하려면 모든 기존 코드를 주의 깊게 확인하여 버그가 발생하지 않았는지 확인해야 한다.(예: 표시하거나 숨기는 것을 잊어버린 경우.)

리액트는 이 문제를 해결하기 위해 만들어다.

리액트에서는 UI를 직접 조작하지 않는다. 즉, 컴포넌트를 직접 활성화, 비활성, 표시 또는 숨기지 않는다. 대신, 보여주고 싶은 것을 선언하면 리액트는 UI를 업데이트하는 방법을 알아낸다. 택시를 타고 운전사에게 방향을 정확히 알려주는 대신 가고 싶은 곳을 알려주는 것을 생각해봐라. 당신을 거기까지 데려다주는 것이 운전기사의 임무이며, 심지어 당신이 고려하지 않은 몇 가지 지름길을 알고 있을 수도 있다.

Thinking about UI declaratively

위에서 명령적으로 양식을 구현하는 방법을 살펴보았다. 리액트에서 생각하는 방법을 더 잘 이해하기 위해 아래에서 리액트에서 이 UI를 다시 구현하는 과정을 살펴보겠다.

  1. 컴포넌트의 다양한 시각적 상태를 식별한다.
  2. 상태 변화를 유발하는 요인을 결정한다.
  3. useState 를 사용하여 메모리의 상태를 표현한다.
  4. 필수적이지 않은 상태 변수를 제거한다.
  5. 이벤트 핸들러를 연결하여 상태 설정한다.

Step 1: Identify your component’s different visual state

컴퓨터 과학에서는 여러 “상태” 중 하나에 있는 “상태 머신”에 대해 들을 수 있다. 디자이너와 함께 작업한다면 다양한 “시각적 상태”에 대한 모형을 본 족이 있을 것이다. 리액트는 디자인과 컴퓨터 과학의 교차점에 있기 때문에 이 두가지 아이디어 모두 영감의 원천이다.

먼저, 사용자가 볼 수 있는 UI의 다양한 “상태”를 모두 시각화해야 한다.

  • Empty: 양식에 “Submit” 버튼이 비활성화되어 있다.
  • Typing: 양식에 “Submit” 버튼이 활성화되어 있다.
  • Submitting: 양식이 완전히 비활성화 되었다. 스피너가 표시 된다.
  • Success: 양식 대신 “Thank you” 메시지가 표시된다.
  • Error: 입력 상태와 동일하지만 추가 오류 메시지가 있다.

디자이너와 마찬가지로 논리를 추가하기 전에 다양한 상태에 대해 “모의”하거나 “모의”를 만들고 싶을 것이다. 예를 들어, 다음은 양식의 시각적 부분에 대한 모의이다. 이 모의는 기본값이 'empty'status 라는 prop에 의해 제어된다.

export default function Form({
  status = 'empty'
}) {
  if (status === 'success') {
    return <h1>That's right!</h1>
  }
  return (
    <>
      <h2>City quiz</h2>
      <p>
        In which city is there a billboard that turns air into drinkable water?
      </p>
      <form>
        <textarea />
        <br />
        <button>
          Submit
        </button>
      </form>
    </>
  )
}

해당 prop을 원하는 대로 부를 수 있으며 이름은 중요하지 않다. 성공 메시지가 나타나는지 확인하려면 status = 'empty'status = 'success' 로 편집해봐라. 모킹을 사용하면 로직을 연결하기 전에 UI를 빠르게 반복할 수 있다. 다음은 status prop에 의해 여전히 “제어”되는 동일한 컴포넌트의 보다 구체화된 프로토타입이다.

export default function Form({
  // Try 'submitting', 'error', 'success':
  status = 'empty'
}) {
  if (status === 'success') {
    return <h1>That's right!</h1>
  }
  return (
    <>
      <h2>City quiz</h2>
      <p>
        In which city is there a billboard that turns air into drinkable water?
      </p>
      <form>
        <textarea disabled={
          status === 'submitting'
        } />
        <br />
        <button disabled={
          status === 'empty' ||
          status === 'submitting'
        }>
          Submit
        </button>
        {status === 'error' &&
          <p className="Error">
            Good guess but a wrong answer. Try again!
          </p>
        }
      </form>
      </>
  );
}

Step 2: Determine whate trigger thos state changes

두 경우 모두 UI를 업데이트하려면 상태 변수를 설정해야 한다. 개발 중인 양식의 경우 몇 가지 다른 입력에 응답하여 상태를 변경해야 한다.

  • 텍스트 입력(사람)을 변경하면 텍스트 상자가 비어 있는지 여부에 따라 Empty 상태에서 Typing 상태 또는 다시 상태로 전환되어야 한다.
  • 제출 버튼(사람)을 클릭하면 Submitting  상태로 전환되어야 한다.
  • 성공적인 네트워크 응답(컴퓨터)은 Success 상태로 전환되어야 한다.
  • 실패한 네트워크 응답(컴퓨터)은 일치하는 오류 메시지와 함께 상태로 Error 전환되어야 한다.

To help visualize this flow, try drawing each state on paper as a labeled circle, and each change between two states as an arrow. You can sketch out many flows this way and sort out bugs long before implementation.

이 흐름을 시각화하는 데 도움이 되도록 종이에 각 상태를 레이블이 지정된 원으로 그리고 두 상태 간의 각 변경 사항을 화살표로 그려봐라. 이런 방식으로 많은 흐름을 스케치하고 구현하기 오래 전에 버그를 분류할 수 있다.

Step 3:Represent the state in memory with useState

다음으로 [useState](https://react.dev/reference/react/useState) 를 사용하여 메모리에서 컴포넌트의 시각적 상태를 표현해야한다. 단순함이 핵심이다. 상태의 각 부분은 ‘움직이는 부분’이다. 그리고 가능한 적은 수의 ‘움직이는 조각’을 원한다. 복잡할수록 버그가 많아진다.

빈드시 존재해야 하는 상태부터 시작해라. 예를 들어 입력에 대한 answer 와 마지막 에러를 저장하려면 error 를 저장해야한다.

const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);

그런 다음 표시하려는 시각적 상태 중 하나를 나타내는 상태 변수가 필요하다. 일반적으로 이를 메모리에 표현하는 방법은 여러 가지가 있으므로 실험해 봐야한다.

최선의 방법을 즉시 생각하는 데 어려움을 겪고 있다면 가능한 모든 시각적 상태가 포함된다는 충분한 상태를 추가하는 것부터 시작해라.

const [isEmpty, setIsEmpty] = useState(true);
const [isTyping, setIsTyping] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [isError, setIsError] = useState(false);

첫 번째 아이디어가 최고는 아닐 수도 있지만 괜찮다. 상태 리팩토링은 프로세스의 일부다.

Step 4: Remove any non-essential state variables

상태 컨텐츠의 중복을 피하여 필수적인 것만 추적하려고 한다. 상태 구조를 리팩토링하는 데 약간의 시간을 투자하면 컴포텉를 더 쉽게 이해하고 중복을 줄이며 의도하지 않은 것을 피할 수 있다. 당신의 목표는 메모리의 상태가 사용자에게 보여주고 싶은 유효한 UI를 나타내지 않는 경우를 방지하는 것이다.(예를 들어, 오류 메시지를 표시하는 동시에 입력을 비활성화하고 싶지 않을 것이다. 그렇지 않으면 사용자가 오류를 수정할 수 없다.)

다음은 상태 변수에 대해 물어볼 수 있는 몇가지 질문이다.

  • 이 상태가 역설을 일으키는가? 예를 들어 isTypingisSubmitting 은 둘 다 true일 수 없다. 역설은 일반적으로 상태가 충분히 제한되지 않음을 의미한다. 두 개의 부울 값에는 네 가지 조합이 가능하지만 유효한 상태에는 3개만 해당된다. “불가능”상태를 제거하려면 typing , submitting , success 세 가지 값 중 하나여야 하는 상태로 이를 결합할 수 있다.
  • 이미 다른 상태 변수에서 동일한 정보를 사용할 수 있을까? 또 다른 역설 : isEmptyisTyping 은 동시에 true 일 수 없다. 별도의 상태 변수로 만들면 동기화되지 않고 버그가 발생할 위험이 있다. 다행스럽게도 isEmpty 를 제거하고 대신 answer.length === 0 을 확인할 수 있다.
  • 다른 상태 변수의 역으로부터 동일한 정보를 얻을 수 있을까? 에러는 error !== null 을 대신 확인할 수 있으므로 isError 는 필요하지 않다.

이 정리 후에는 필수 상태 변수 3개(7개에서 줄음) 남는다.

const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
const [status, setStatus] = useState('typing'); // 'typing', 'submitting', or 'success'

기능을 손상시키지 않고는 어떤 것도 제거할 수 없기 때문에 이것이 필수적이다.

💡 DEEP DIVE

리듀서를 사용하여 “불가능한” 상태 제거

이 세 가지 변수는 이 양식의 상태를 충분히 잘 표현한다. 하지만 여전히 완전히 이해되지 않는 일부 중간 상태가 있다. 예를 들어 status'success' 인 경우 null이 아닌 error는 의미가 없다. 상태를 보다 정확하게 모델링하려면 이를 리듀서로 추출하면 된다. 리듀서를 사용하면 여러 상태 변수를 단일 객체로 통합하고 관련된 모든 로직을 통합할 수 있다.

Step 5: Connect the event handlers to set state

마지막으로 상태를 업데이트하는 이벤트 핸들러를 만든다. 다음은 모든 이벤트 핸들러가 연결된 최종 양식이다.

import { useState } from 'react';

export default function Form() {
  const [answer, setAnswer] = useState('');
  const [error, setError] = useState(null);
  const [status, setStatus] = useState('typing');

  if (status === 'success') {
    return <h1>That's right!</h1>
  }

  async function handleSubmit(e) {
    e.preventDefault();
    setStatus('submitting');
    try {
      await submitForm(answer);
      setStatus('success');
    } catch (err) {
      setStatus('typing');
      setError(err);
    }
  }

  function handleTextareaChange(e) {
    setAnswer(e.target.value);
  }

  return (
    <>
      <h2>City quiz</h2>
      <p>
        In which city is there a billboard that turns air into drinkable water?
      </p>
      <form onSubmit={handleSubmit}>
        <textarea
          value={answer}
          onChange={handleTextareaChange}
          disabled={status === 'submitting'}
        />
        <br />
        <button disabled={
          answer.length === 0 ||
          status === 'submitting'
        }>
          Submit
        </button>
        {error !== null &&
          <p className="Error">
            {error.message}
          </p>
        }
      </form>
    </>
  );
}

function submitForm(answer) {
  // Pretend it's hitting the network.
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      let shouldError = answer.toLowerCase() !== 'lima'
      if (shouldError) {
        reject(new Error('Good guess but a wrong answer. Try again!'));
      } else {
        resolve();
      }
    }, 1500);
  });
}

이 코드는 원래 명령형 예제보다 길지만 훨씬 덜 취약하다. 모든 상호작용을 상태 변경으로 표현하면 나중에 기존 상태를 손상시키지 않고 새로운 시각적 상태를 도입할 수 있다. 또한 상호작용 자체의 논리를 변경하지 않고도 각 상태에 표시되어야 하는 내용을 변경할 수 있다.

Recap

  • 선언적 프로그래밍은 UI를 세세하게 관리하는 것이 아니라 각 시각적 상태에 대한 UI를 설명하는 것을 의미한다.
  • 컴포넌트를 개발할 때
    1. 모든 시각적 상태를 식별한다.
    2. 상태 변경에 대한 인간 및 컴퓨터 트리거를 결정한다.
    3. useState 로 상태 모델링한다.
    4. 버그와 역설을 피하기 위해 필수적이지 않은 상태를 제거한다.
    5. 이벤트 핸들러와 연결하여 상태를 설정한다.
profile
반갑습니다.

0개의 댓글