Published on

State: A Component's Memory

Authors
  • avatar
    Name
    박준형
    Twitter

해당 글은 리액트 베타문서 State: A Component's Memory 번역입니다

React의 상태

React에서 컴포넌트는 사용자 interaction에 의해 화면에 표시되는 내용이 변경되어야한다.

폼요소에서 인풋박스의 값이 변경되면 입력필드가 변경되어야하고, 캐러셀에서 다음버튼을 누르면 이미지가 변경되어야하고, 구매버튼을 클릭하면 장바구니에 상품이 담겨야한다.

컴포넌트는 현재 입력값, 현재 이미지, 현재 장바구니 등을 기억해야한다.
리액트가 기억해야할 정보를 상태라고 부른다.

export default App () {
  let index = 0

  function handleClick() {
    index = index + 1
  }

  return (
    <div>
      <button onClick={handleClick}>Next</button>
      <p>{index}</p>
    </div>
  )
}

위의 코드에서 next 버튼을 아무리 눌러도 작동하지않는다.

이벤트 handleClick 핸들러가 로컬변수 index를 업데이트하고있다. 하지만 아래의 이유로 변경사항이 반영되지않는다.

  • 지역변수는 렌더링 간에 유지되지않는다.
  • 로컬변수를 변경해도 렌더링이 트리거되지 않는다.

새로운 데이터로 컴포넌트를 업데이트하려면 두가지 조건이 필요합니다.

  • 렌더링간의 데이터를 유지한다.
  • React를 트리거하여 새 데이터로 컴포넌트를 리렌더한다.

useState hook은 두가지 Interface를 제공한다.

const [state, setState] = useState(initialState)
  • state : 렌더링 간에 데이터를 유지하기 위한 상태 변수
  • setState : 상태 변수를 업데이트하고 컴포넌트를 리렌더하기 위해 React를 트리거하는 상태 설정 함수
export default App () {
  const [index,setIndex] = useState(0)

  function handleClick() {
    setIndex(index+1)
  }

  return (
    <div>
      <button onClick={handleClick}>Next</button>
      <p>{index}</p>
    </div>
  )
}

리액트에게 상태 변수를 업데이트하며 리렌더 트리거를 주기위해선 위와같이 작성해야합니다.

const [state, setState] = useState(0)

useState의 매개변수는 오직 초기값뿐이다. 위의 코드에서는 state에게 초기값(initialValue)로 0을 할당해주었다.

  • state variable (state): 상태변수로 데이터를 저장합니다.
  • state setter function (setState) : React에게 리렌더하도록 트리거가되는 상태변수를 업데이트하는 함수입니다.

위와 같이 코드를 작성했을때 아래와 같은 일이 일어납니다.

  • 컴포넌트는 최초 1회 렌더된다.
    왜냐하면 useState의 매개변수(초기값)으로 0이 state에 할당되어, 상태변수에 0을 저장하며 [0, setState] 배열을 반환한다.
  • 상태를 변경한다.
    유저가 버튼을 눌렀을때 버튼이 setState(state + 1)를 호출한다. 이는 React에게 현재 1이라고 알리고 리렌더하도록 트리거 역할을한다.
  • 컴포넌트가 리렌더한다. React는 useState의 초기값 0을 바라 보지만, React는 업데이트된 상태변수를 기억하고 [1, setState] 배열을 반환한다.

Multiple State

React를 사용하는 엔지니어는 하나의 컴포넌트에 여러 타입의 여러 상태를 작성할 수 있다.

import { useState } from 'react'
import { sculptureList } from './data.js'

export default function App() {
  const [index, setIndex] = useState(0)
  const [showMore, setShowMore] = useState(false)

  function handleNextClick() {
    setIndex(index + 1)
  }

  function handleMoreClick() {
    setShowMore(!showMore)
  }

  let sculpture = sculptureList[index]
  return (
    <>
      <button onClick={handleNextClick}>Next</button>
      <h2>
        <i>{sculpture.name} </i>
        by {sculpture.artist}
      </h2>
      <h3>
        ({index + 1} of {sculptureList.length})
      </h3>
      <button onClick={handleMoreClick}>{showMore ? 'Hide' : 'Show'} details</button>
      {showMore && <p>{sculpture.description}</p>}
      <img src={sculpture.url} alt={sculpture.alt} />
    </>
  )
}

위의 컴포넌트는 number type index 상태와, boolean type showMore 상태를 지니고있다.

이는 여러개의 각각의 상태가 연관되어있지않은 상태를 표현하기에 좋은 예제다. 하지만 두개의 상태를 함께 변경하는 경우가 잦다면, 하나의 상태로 결합하는것이 더 나을수도있다. 예를 들어 폼필드에 많은 폼아이템이 존재하는 경우 필드당 상태 변수 1개씩을 가지는것보다는 단일상태로 관리하는것이 더 편리하다.

choosing-the-state-structure

React는 반환하는 상태를 어떻게 알 수 있을까?

우리는 useState에게 어떤한 상태변수를 참조하는지에 대한 정보를 주지않는다는것을 알고있다.

useState에는 어떠한 식별자도 없는데 어떻게 상태 변수를 반환하는것일까? 어디 마법같은 함수에 의존적일까라는 질문에 당당하게 NO라고 답할수 있다.

간결한 문법 사용을 위해 hooks는 동일한 컴포넌트의 모든 렌더링 안정적인 호출순서에 의존적이다. hooks rule을 지켰다면 (“only call Hooks at the top level”), hooks는 동일한 순서로 호출되기떄문에 잘 작동한다.

내부적으로 React는 모든 컴포넌트마다 상태 쌍의 배열을 보유한다. 또한 0으로 초기화된 현재 쌍 index도 유지한다. useState를 호출할 때마다 React는 다음 상태의 쌍을 제공하고 state를 증가시킨다. 이 메커니즘의 자세한 내용은 React hooks: not magic, just arrays에서 확인이 가능하다.

아래의 코드는 React를 사용하지 않지만 내부적으로 작동방식을 설명하는 아이디어를 제공해준다.

let componentHooks = []
let currentHookIndex = 0

// useState 정의
function useState(initialState) {
  // 상태쌍의 배열
  let pair = componentHooks[currentHookIndex]

  if (pair) {
    /*
     * 첫번째 렌더에는 해당코드 실행X
     * 상태 쌍의 배열이 존재할경우 반환하고 다음 hooks 호출을 준비한다
     */
    currentHookIndex++
    return pair
  }

  // 최초렌더시에 pair에 상태를 생성하고 저장한다
  pair = [initialState, setState]

  // 유저가 상태변경을 요청했을때 새로운 값을 쌍에 넣어준다.
  function setState(nextState) {
    pair[0] = nextState
    updateDOM()
  }

  // 다음 렌더링의 상태쌍을 저장하고 다음 hooks 호출을 준비한다
  componentHooks[currentHookIndex] = pair
  currentHookIndex++
  return pair
}

// 컴포넌트
function Gallery() {
  // Each useState() call will get the next pair.
  const [index, setIndex] = useState(0)
  const [showMore, setShowMore] = useState(false)

  function handleNextClick() {
    setIndex(index + 1)
  }

  function handleMoreClick() {
    setShowMore(!showMore)
  }

  let sculpture = sculptureList[index]

  // 이부분은 JSX(React)가 맡아야할 부분이지만, 예시를 위해 작성했다.
  return {
    onNextClick: handleNextClick,
    onMoreClick: handleMoreClick,
    header: `${sculpture.name} by ${sculpture.artist}`,
    counter: `${index + 1} of ${sculptureList.length}`,
    more: `${showMore ? 'Hide' : 'Show'} details`,
    description: showMore ? sculpture.description : null,
    imageSrc: sculpture.url,
    imageAlt: sculpture.alt,
  }
}

// 리렌더 발생을 위한 함수
function updateDOM() {
  // 현재 훅 인덱스를 리렌더 이전에 초기화한다.
  currentHookIndex = 0
  let output = Gallery()

  // 컴포넌트 반환값과 맞는 DOM요소를 연결해주는데 이 역할은 리액트가 제공해준다.
  nextButton.onclick = output.onNextClick
  header.textContent = output.header
  moreButton.onclick = output.onMoreClick
  moreButton.textContent = output.more
  image.src = output.imageSrc
  image.alt = output.imageAlt
  if (output.description !== null) {
    description.textContent = output.description
    description.style.display = ''
  } else {
    description.style.display = 'none'
  }
}

let nextButton = document.getElementById('nextButton')
let header = document.getElementById('header')
let moreButton = document.getElementById('moreButton')
let description = document.getElementById('description')
let image = document.getElementById('image')

React를 사용하기 위해 이 모든과정을 이해할필요는 없지만, 유용한 사실인건 틀림없다.

State는 고립되어있고 프라이빗하다.

State는 컴포넌트 인스턴스의 로컬 요소다. 즉 동일한 컴포넌트를 두번 렌더링하면 각 복사본이 완전히 격리된 상태가 된다. 둘중 하나의 컴포넌트에서 내부 상태를 변경하더라도 다른 컴포넌트는 영향을 받지않는다.

import Gallery from './Gallery.js'

export default function Page() {
  return (
    <div className="Page">
      <Gallery />
      <Gallery />
    </div>
  )
}

상태가 모듈 상단에서 선언한 일반적인 변수와 다른점이다. state는 특정 함수 호출이나 코드의 위치에 연결되지는 않지만, 특정 컴포넌트 내부에서 로컬변수로 동작한다.

위에서 Gallery 컴포넌트를 두번 호출하여 렌더했지만, 각각의 컴포넌트는 따로 컴포넌트 인스턴스 내부에 저장되어있다.

또한 Page 컴포넌트는 Gallery 컴포넌트 내부의 상태나 다른 요소가 있는지 확인할 수 있는 방법이 존재하지않는다. Props와 달리 State는 state를 선언한 컴포넌트외에 완전히 비공개된다.

이럴때 두 Gallery 컴포넌트의 state를 동기화 시키려면 자식 컴포넌트(Gallery)에서 state를 제공하고 그 state를 가까운 부모 컴포넌트에 state를 추가하여 Props로 State를 공유하는 방법이 존재한다.