Published on

리액트 공식문서 스터디 5-1 주차

Authors
  • avatar
    Name
    junyeol kim

State를 사용해 Input 다루기

React는 선언적인 방식으로 UI를 조작한다.

개별적인 UI를 직접 조작하는 것 대신 컴포넌트 내부에 여러 state를 선언하고 사용자 입력에 따라 state를 변경한다.


명령형 UI

명령형 UI 예시

  • 폼에 입력하면 제출 버튼이 활성화 된다.
  • 제출 버튼을 누르면 폼과 버튼이 비활성화 되고 스피너가 나타난다.
  • 네트워크 요청이 성공하면 폼은 가려지고, 감사합니다라는 메세지가 나타난다.
  • 네트워크 요청이 실패하면 오류 메세지가 보이고, 폼은 다시 활성화된다.

-> 명령형 UI란 하나부터 열까지 모든 UI요소에 어떻게 업데이트 해야하는지 명령을 내리는 것 이다.

-> 간단한 시스템에서는 상관없지만, 조금만 복잡해져도 유지보수 관점에서 매우 안좋은 형태이다.


선언형 UI

  • React는 이러한 명령형 UI의 문제를 해결하기 위해 만들어졌다.

  • React에서는 UI를 직접 조작할 필요도, 컴포넌트를 직접 활성화 하거나 비활성화하는 등 할 필요가 없다.

  • 단지, 무엇을 보여주고싶은지 선언만 하면 된다.


UI를 선언적인 방식을 생각하기

React에서 UI를 선언하는 구현과정

  1. 컴포넌트의 다양한 시각적 state를 확인하기
  2. 무엇이 state 변화를 트리거하는지 알아내기
  3. 메모리의 state를 useState로 표현하기
  4. 불필요한 state를 제거하기
  5. state 설정을 위해 이벤트 핸들러 연결하기

  1. 컴포넌트의 다양한 시각적 state 확인하기

  • React는 디자인과 컴퓨터 과학 사이에 있기 때문에 상태 기계와 시각적 state 두 아이디어에서 영감을 받아 만들어졌다.

  • 가장 먼저, 사용자가 볼 수 있는 UI의 모든 state를 시각화 해야한다.


state 시각화 예시

Empty: 폼은 비활성화된 “제출” 버튼을 가지고 있다.
Typing: 폼은 활성화된 “제출” 버튼을 가지고 있다.
Submitting: 폼은 완전히 비활성화되고 스피너가 보인다.
Success: 폼 대신에 “감사합니다” 메시지가 보인다.
Error: “Typing” state와 동일하지만 오류 메시지가 보인다.

  • 많은 시각적 state는 배열을 통해 보여줄 수 있고, 이를 스타일가이드 혹은 스토리북이라 부른다.

  1. 무엇이 state 변화를 트리거하는지 알아내기

  • 두 종류의 인풋 유형으로 state를 트리거 할 수 있다.

    • 휴먼 인풋 ( 이벤트 핸들러가 필요할 수 도 있음 )
    • 컴퓨터 인풋
  • 모든 인풋 유형은 UI를 업데이트하기 위해서는 state 변수를 설정해야 한다.


인풋 유형에 따른 state 변경 예시

example-state
  • 텍스트 인풋을 변경하면 (휴먼) 텍스트 상자가 비어있는지 여부에 따라 state를 Empty에서 Typing 으로 또는 그 반대로 변경해야한다.

  • 제출 버튼을 클릭하면 (휴먼) Submitting state를 변경해야한다.

  • 네트워크 응답이 성공적으로 오면 (컴퓨터) Success state를 변경해야한다.

  • 네트워크 요청이 실패하면 (컴퓨터) 해당하는 오류 메시지와 함께 Error state를 변경해야한다.


  1. 메모리의 state를 useState로 표현하기

  • useState를 이용하여 컴포넌트의 시각적 state를 표현해야한다.

  • 각각의 state는 적을수록 좋다. 많으면 버그를 일으킬 확률이 높다.

  • 모든 시각적 state를 커버할 수 있는 확실한 것을 먼저 선언한 후, 필요한 state를 선언한다.


  1. 불필요한 state 변수 제거하기

  • state가 사용자에게 유효한 UI를 보여주지 않는 것을 방지하는게, 좋은 리팩토링이다.

⭐️ 프로젝트를 진행하면서, 3가지 질문으로 불필요한 state를 제거해보자. ⭐️

  1. state가 역설을 일으키지는 않나요?
  2. 다른 state 변수에 이미 같은 정보가 담겨있진 않나요?
  3. 다른 변수를 뒤집었을 때 같은 정보를 얻을 수 있진 않나요?

Reducer- 좀 더 정확하게 모델링 할 수 있다. - 여러 State의 변수를 하나의 객체로 통합하고, 관련된 모든 로직을 합칠 수 있다.


State 구조 선택하기

State의 구조화를 잘하면, 수정과 디버깅이 즐거운? 컴포넌트를 만들 수 있다.

State 구조화 원칙

5가지 원칙

  1. 연관된 state 그룹화하기. 두 개 이상의 state 변수를 항상 동시에 업데이트한다면, 단일 state 변수로 병합하는 것을 고려하세요.
  1. State의 모순 피하기. 여러 state 조각이 서로 모순되고 “불일치”할 수 있는 방식으로 state를 구성하는 것은 실수가 발생할 여지를 만듭니다. 이를 피하세요.
  1. 불필요한 state 피하기. 렌더링 중에 컴포넌트의 props나 기존 state 변수에서 일부 정보를 계산할 수 있다면, 컴포넌트의 state에 해당 정보를 넣지 않아야 합니다.
  1. State의 중복 피하기. 여러 상태 변수 간 또는 중첩된 객체 내에서 동일한 데이터가 중복될 경우 동기화를 유지하기가 어렵습니다. 가능하다면 중복을 줄이세요.
  1. 깊게 중첩된 state 피하기. 깊게 계층화된 state는 업데이트하기 쉽지 않습니다. 가능하면 state를 평탄한 방식으로 구성하는 것이 좋습니다.

  1. 연관된 state 그룹화 하기

// before
const [x, setX] = useState(0)
const [y, setY] = useState(0)

// after
const [position, setPosition] = useState({ x: 0, y: 0 })
  • 두개의 state 변수가 항상 함께 변경된다면, 단일 state 변수로 통합하는 것이 좋다.

  • State 변수가 객체인 경우에는 다른 필드를 명시적으로 복사하지 않고 하나의 필드만 업데이트할 수 없다.


  1. state의 모순 피하기

const [isSending, setIsSending] = useState(false)
const [isSent, setIsSent] = useState(false)
  • 위 두 개의 state를 선언함으로써 불가능한 state를 허용한다.

  • 이 말은 setIsSending, setIsSent을 함께 호출하는 것을 고려하지 않는 경우 두 상태가 true가 되는 상황이 생긴다.

  • 그렇기에 컴포넌트가 복잡할수록 무슨일이 일어나는지 이해하기 어렵다.

  • 그러므로 두 개의 상태를 하나의 유효한 상태로 관리하는게 좋다.

const isSending = status === 'sending'
const isSent = status === 'sent'
  • 가독성을 위해 상수처리도 할 수 있다.

  1. 불필요한 state 피하기

  • 렌더링중에, 컴포넌트의 props나 기존 state 변수로 다른 state의 정보를 얻을 수 있다면, 해당 state는 제거할 수 있다.

  • Props를 state에 미러링하지 말기

    • 단, 처음에 한 번만 props를 읽고, 이후 props 변경을 무시하고싶다면 미러링을 쓰기도 한다.

  1. State의 중복 피하기

// 같은 정보를 가지고 있는 다른 state
const [items, setItems] = useState(initialItems);
const [selectedItem, setSelectedItem] = useState(items[0]);

// 아이템 제목을 수정하는 함수
function handleItemChange(id, e) {
  setItems(items.map(item => {
    if (item.id === id) {
      return {
        ...item,
        title: e.target.value,
      };
    } else {
      return item;
    }
  }));
}
  ...
  return(
    ...
  <p>You picked {selectedItem.title}.</p>
);

// 리스트에 보이는 input 값은 items을 기준으로 렌더링되기 때문에 잘 바뀐다. 하지만 selectedITem은 전혀 업데이트 하지 않기 때문에 selectedItem.title는 옛날 값을 가지고 있다.

// 이게 바로 state 중복이 만들어내는 대표적인 버그이다.

// 해결하기 위한 방법은 ?
// 중복을 없애고, 진짜로 필요한 최소 state만 선언한다.
// 이러한 결과로 React 관점으로 다시 정리하자면
// State에는 필수 정보만 넣고, 나머지 값들은 state에서 계산해서 만든 파생값으로 처리하자! 라고 할 수 있다.
//
  • 추가적으로 중복되는 state를 줄이는 예시

    • 조건 플래그인데, 다른 값으로부터 바로 알 수 있는 경우
    const [items, setItems] = useState([...]);
    const [isEmpty, setIsEmpty] = useState(false); // 이것보다는
    
    const isEmpty = items.length === 0; // 이런식으로
    

  1. 깊게 중첩된 state 피하기

// 평탄화 이전
export const initialTravelPlan = {
  id: 0,
  title: '(Root)',
  childPlaces: [{
    id: 1,
    title: 'Earth',
    childPlaces: [{
      id: 2,
      title: 'Africa',
      childPlaces: [{
        id: 3,
        title: 'Botswana',
        childPlaces: []
      }, {
        id: 4,
        title: 'Egypt',
        childPlaces: []
      }, {
        id: 5,
        title: 'Kenya',
        childPlaces: []
      }, {
        id: 6,
        title: 'Madagascar',
        childPlaces: []
      }, {
        id: 7,
        title: 'Morocco',
        childPlaces: []
      }, {
        id: 8,
        title: 'Nigeria',
        childPlaces: []
      }, {
        id: 9,
        title: 'South Africa',
        childPlaces: []
      }]
    }
    ...

// 평탄화 이후
export const initialTravelPlan = {
  0: {
    id: 0,
    title: '(Root)',
    childIds: [1, 42, 46],
  },
  1: {
    id: 1,
    title: 'Earth',
    childIds: [2, 10, 19, 26, 34]
  },
  2: {
    id: 2,
    title: 'Africa',
    childIds: [3, 4, 5, 6 , 7, 8, 9]
  },
  ...
  • 만약 state의 업데이트가 너무 중첩되어있다면, 평탄화를 하는 것이 좋다.

  • 평탄화를 하는것으로, State의 업데이트가 쉬워지고 중첩된 객체의 중복되는 부분을 방지해준다.


컴포넌트 간 State 공유하기

두 컴포넌트의 state를 항상 함께 업데이트하기 위해서는 각 컴포넌트의 state를 제거하고 가까운 공통 부모 컴포넌트로 옮긴 후 props로 전달하면 된다. 이러한 방법을 State 끌어올리기라 한다.

State 끌어올리기 예시

// 하나의 상태로 모두 업데이트 되는 상황
import { useState } from 'react'

function Panel({ title, children }) {
  const [isActive, setIsActive] = useState(false)
  return (
    <section className="panel">
      <h3>{title}</h3>
      {isActive ? <p>{children}</p> : <button onClick={() => setIsActive(true)}>Show</button>}
    </section>
  )
}

export default function Accordion() {
  return (
    <>
      <h2>Almaty, Kazakhstan</h2>
      <Panel title="About">
        With a population of about 2 million, Almaty is Kazakhstan's largest city. From 1929 to
        1997, it was its capital city.
      </Panel>
      <Panel title="Etymology">
        The name comes from <span lang="kk-KZ">алма</span>, the Kazakh word for "apple" and is often
        translated as "full of apples". In fact, the region surrounding Almaty is thought to be the
        ancestral home of the apple, and the wild <i lang="la">Malus sieversii</i> is considered a
        likely candidate for the ancestor of the modern domestic apple.
      </Panel>
    </>
  )
}
  • 위 코드는 하나의 상태로 컴포넌트의 상태가 관리되는 코드이다. 이러한 상황에서 컴포넌트 마다 독립적인 상태관리를 위해서는 State 끌어올리기가 필요하다.

  • 세 가지 단계를 통해 끌어올리기를 할 수 있다.

  1. 자식 컴포넌트의 State를 제거한다.
  2. 하드 코딩된 값을 공통 부모로부터 전달한다.
  3. 공통 부모에 State를 추가하고, 이벤트 핸들러와 함께 전달한다.

Step 1 : 자식 컴포넌트의 State를 제거하기


const [isActive, setIsActive] = useState(false);
// 자식 컴포넌트에 선언되어있던 State를 공통 부모 컴포넌트에서 선언한다.

function Panel({ title, children, isActive }) {
// 그 후 예시로 나타나있는 Panel 자식 컴포넌트의 Props로서 State를 내려준다.

  • 자식 컴포넌트에 선언되어 있던 State를 제거하여 공통 부모 컴포넌트에 선언한 후 State를 Props로 자식 컴포넌트에 전달한다.

Step 2 : 하드 코딩된 데이터를 부모 컴포넌트로 전달하기

  • State 끌어올리기를 하기 위한 가장 중요한 조건은 업데이트하려는 두 자식 컴포넌트의 가장 가까운 공통 부모 컴포넌트에 State를 선언해야 한다는 것이다.
import { useState } from 'react'

export default function Accordion() {
  return (
    <>
      <h2>Almaty, Kazakhstan</h2>
      <Panel title="About" isActive={true}>
        With a population of about 2 million, Almaty is Kazakhstan's largest city. From 1929 to
        1997, it was its capital city.
      </Panel>
      <Panel title="Etymology" isActive={true}>
        The name comes from <span lang="kk-KZ">алма</span>, the Kazakh word for "apple" and is often
        translated as "full of apples". In fact, the region surrounding Almaty is thought to be the
        ancestral home of the apple, and the wild <i lang="la">Malus sieversii</i> is considered a
        likely candidate for the ancestor of the modern domestic apple.
      </Panel>
    </>
  )
}

function Panel({ title, children, isActive }) {
  return (
    <section className="panel">
      <h3>{title}</h3>
      {isActive ? <p>{children}</p> : <button onClick={() => setIsActive(true)}>Show</button>}
    </section>
  )
}
  • 예시 코드에서 Panel 컴포넌트의 가장 가까운 공통 부모 컴포넌트는 Accordion이다. 그렇기에 Accordion은 두 Panel 상위에 있고 props를 제어할 수 있기 때문에, 어느 Panel이 활성화 되었는지에 대한 source of truth가 된다. 그래서 Accordion이 하드 코딩된 값을 가지는 isActive 값을 두 Pannel에 전달하도록 만든다.

Step 3 : 공통 부모에 State 추가하기

  • State 끌어올리기는 State로 저장하고 있는 컴포넌트의 특성 값을 바꿀 수 있다.

  • 위 예시에서는 한 번에 하나의 Panel만 활성화 되어야 하기 때문에, 공통 부모 컴포넌트인 Accordion은 어떤 Panel이 활성화 되어있는지 추적하고 있어야 한다.

  • 그래서 state 변수에 boolean 값을 대신해서 활성화 되어있는 Panel의 인덱스 값을 사용할 수 있다.

const [activeIndex, setActiveIndex] = useState(0)
  • 위 코드에서 0이면 비활성화 1이면 활성화 됐다고 볼 수 있다.

  • 각 Panel에서 Show 버튼을 클릭하면 Accordion의 activeIndex 값을 변경해야하는데, Panel 컴포넌트는 직접 State값을 변경할 수 없다. 왜냐하면 Accordion 내부에 선언된 상태 값이기 때문이다. 그렇기에 Accordion 컴포넌트는 Panel 컴포넌트가 State 값을 변경할 수 있도록 이벤트 핸들러를 통해 State값을 전달하는 방식을 통해 명시적으로 허용해야 한다.

제어 컴포넌트와 비제어 컴포넌트

  • 컴포넌트의 중요정보가 내부 State에 의해서가 아닌 props를 통해 만들어진다면, 컴포넌트가 제어된다고 표현한다.

  • 비제어 컴포넌트는 부모 컴포넌트에서 사용하기 쉽지만 여러 컴포넌트를 동시적으로 조정하려고 하면 제어 컴포넌트보다는 덜 유연하다고 할 수 있다.

  • 그렇기에, 컴포넌트를 작성할 때 어떤 정보가 props를 통해 제어되어야 하고, 어떤 정보가 state를 통해 제어되지 않아야하는지 고려하여 작성한다면, 리팩토링을 보다 쉽게 할 수 있다.

각 State의 단일 진실 공급원

  • 간단하게 생각하자면, 같은 의미 데이터를 중복해서 선언하지말고, State 하나당 책임지는 컴포넌트를 하나 정하는 것 이다.