Learn React 2장 / 상호작용 추가하기

user profile img

신현호

React

React Docs

Post Thumbnail

목차

    2장 / 상호작용 추가하기

    이벤트에 응답하기

    이벤트 핸들러를 추가하려면 먼저 함수를 정의한 다음 이를 적절한 JSX 태그에 props로 전달합니다. 예를 들어, 아직 아무 작업도 수행하지 않는 버튼이 있습니다.

    jsx

    export default function Button() {
      return <button>I don't do anything</button>
    }
    

    우리는 다음의 세 단계를 거쳐 사용자가 클릭할 때 메시지를 표시하도록 설정할 수 있습니다.

    1. Button 컴포넌트 안에 handleClick이라는 함수를 선언합니다.
    2. 해당 함수 내부의 로직을 구현합니다. (alert를 사용하여 메시지 표시)
    3. JSX의 <button>onClick={handleClick} 을 추가합니다.

    handleClick 함수를 정의한 다음 이를 <button> 에 props로 전달했습니다. handleClick이벤트 핸들러 입니다.
    이벤트 핸들러 함수는 다음과 같은 특징을 가집니다.

    1. 일반적으로 컴포넌트 안에 정의됩니다.
    2. handle로 시작하는 이름 뒤에 이벤트 이름이 오도록 합니다.
      • 관례상 이벤트 핸들러의 이름은 handle 뒤에 이벤트 이름을 붙이는 것이 일반적입니다.

    이벤트 핸들러는 JSX에서 인라인으로 정의될 수 있습니다.

    jsx

    <button onClick={function handleClick() {
      alert('You clicked me!');
    }}>
    

    간결하게 화살표 함수를 통해 정의하는 방법도 있습니다.

    jsx

    <button onClick={() => {
      alert('You clicked me!');
    }}>
    

    인라인 이벤트 핸들러는 함수가 짧을 경우 편리합니다.

    익명 함수를 사용할 때는 화살 함수를 사용하세요

    Airbnb 자바스크립트 스타일가이드에서는 익명 함수를 화살 함수로 다루는 것을 권장합니다. 화살 함수에서의 this가 더 직관적으로 작동하기 때문입니다.
    이에 대해 더 알고싶다면 자바스크립트의 this 동작에 대해서 학습해보시는 것을 추천드립니다.

    이벤트 핸들러 함수를 호출하지말고 전달하세요

    Bad : <button onClick={handleClick()}> , <button onClick={alert('...')}>
    Good : <button onClick={handleClick}> , <button onClick={() => alert('...')}>

    이벤트 핸들러는 컴포넌트 내부에서 선언되기 때문에 컴포넌트의 props에 접근할 수 있습니다.
    다음은 클릭하면 message와 prop과 함께 alert를 표시하는 버튼입니다.

    이렇게 하면 이 두 버튼이 서로 다른 메시지를 표시할 수 있습니다. 전달되는 메시지를 변경해보세요.

    가끔 부모 컴포넌트가 자식의 이벤트 핸들러를 지정하고 싶을 때가 있습니다. Button 컴포넌트를 사용하는 위치에 따라 버튼은 동영상을 재생하고 다른 버튼을 이미지를 업로드 하는 등.. 서로 다른 기능을 실행하고 싶을 수 있습니다.

    이렇게 하기 위해서는, 컴포넌트가 부모로부터 받는 prop을 이벤트 핸들러로 다음과 같이 전달합니다.

    여기에서 Toolbar 컴포넌트는 PlayButtonUploadButton을 렌더링합니다.

    • PlayButtonhandlePlayClickonClick prop으로 내부의 Button에 전달합니다.
    • UploadButton() => alert('Uploading!')onClick prop으로 내부의 Button에 전달합니다.

    마지막으로 Button 컴포넌트는 onClick 이라는 prop을 받습니다. 해당 prop을 브라우저의 빌트인 <button> 으로 직접 전달하며, onClick={onClick} 을 사용합니다.
    이는 클릭 시 전달된 함수를 호출하도록 React에게 지시합니다.

    디자인 시스템을 사용하는 경우, 버튼 같은 컴포넌트에 스타일링은 포함하지만 동작을 지정하지 않는 것이 일반적입니다. 대신 PlayButtonUploadButton과 같은 컴포넌트는 이벤트 핸들러를 전달합니다.

    <button><div> 와 같은 기본 제공 컴포넌트는 onClick과 같은 브라우저 이벤트 이름만 지원합니다.
    하지만 자체 컴포넌트를 빌드할 떄는 이벤트 핸들러 prop의 이름을 원하는 방식으로 지정할 수 있습니다.

    관례상 이벤트 핸들로 props는 on으로 시작하고 그 뒤에 대문자가 와야합니다.

    jsx

    function Button({ onSmash, children }) {
      return <button onClick={onSmash}>{children}</button>
    }
    
    export default function App() {
      return (
        <div>
          <Button onSmash={() => alert('Playing!')}>Play Movie</Button>
          <Button onSmash={() => alert('Uploading!')}>Upload Image</Button>
        </div>
      )
    }
    

    이 예제에서 <button onClick={onSmash}>는 브라우저 <button>에 여전히 onClick이라는 prop이 필요하지만 사용자 정의 Button 컴포넌트가 수신하는 prop이름은 사용자가 지정할 수 있음을 보여줍니다.

    컴포넌트가 여러 상호작용을 지원하는 경우, 앱별 개념에 따라 이벤트 핸들러 props의 이름을 지정할 수 있습니다. 예를들어 아래의 예제는 onPlayMovie, onUploadImage 이벤트 핸들러를 수신합니다.

    App 컴포넌트는 ToolbaronPlayMovie 또는 onUploadImage로 어떤 작업을 수행할지 알 필요가 없다는 점을 주목하세요.

    이것이 Toolbar의 구현 세부 사항입니다, 여기서 ToolbarButtononClick핸들러로 전달하지만 나중에 키보드 단축키에서 촉발시킬 수도 있습니다.

    prop의 이름을onPlayMovie와 같은, 앱별 상호작용의 이름을 따서 지정하면 나중에 사용 방식을 유연하게 변경할 수 있습니다.

    이벤트 핸들러는 적절한 HTML 태그에 사용하세요

    적절한 HTML 태그에 이벤트 핸들러를 사용해야합니다. 예를 들어 div요소에 onClick요소를 넣는 것 보단 button요소에 onClick요소가 들어가는것이 좋습니다.
    버튼의 기본 브라우저 스타일이 마음에 들지 않고 링크나 다른 UI 요소처럼 보이길 원한다면, CSS를 사용하여 원하는 방식으로 조정하세요

    이벤트 핸들러는 컴포넌트에 있을 수 있는 모든 하위 컴포넌트의 이벤트도 포착합니다. 이벤트가 트리 위로 버블 또는 전파 되는 것을 이벤트가 발생한 곳에서 시작하여 트리 위로 올라간다고 합니다.

    아래의 예제에 존재하는 <div>는 2개의 버튼을 포함합니다. <div>와 각 버튼에는 모두 고유한 onClick 핸들러가 있습니다. 버튼을 클릭하면 어떤 핸들러가 실행될까요?

    두 버튼 중 하나를 클릭하면 해당 버튼의 onClick이 먼저 실행되고 그 다음에 부모 <div>onClick이 실행됩니다.
    따라서 두 개의 메시지가 나타납니다. 툴바 자체를 클릭하면 부모 <div>onClick만 실행됩니다.

    이벤트 핸들러는 이벤트 객체를 유일한 인수로 받습니다. 이것은 관례상 event를 의미하는 e라고 불립니다. 이 객체를 사용하여 이벤트에 대한 정보를 읽을 수 있습니다.

    해당 이벤트 객체를 사용하면 전파를 중지할 수도 있습니다. 이벤트가 상위 컴포넌트에 도달하지 못하도록 하려면 아래 Button 컴포넌트처럼 e.stopPropagation()을 호출해야 합니다.

    버튼을 클릭하면 다음과 같은 작업이 실행됩니다.

    1. React는 <button>에 전달된 onClick 핸들러를 호출합니다.
    2. Button에 정의된 이 핸들러는 다음을 수행합니다.
      • 이벤트가 더 이상 버블링되지 않도록 e.stopPropagation()을 호출합니다.
      • Toolbar 컴포넌트에서 전달된 props인 onClick 함수를 호출합니다.
    3. Toolbar 컴포넌트에 정의된 이 함수는 버튼 자체의 경고를 표시합니다.
    4. 전파가 중지되었으므로 부모 <div>onClick 핸들러가 실행되지 않습니다.

    e.stopPropagation() 덕분에 이제 버튼을 클릭하면 두 개의 알림 (<button>과 부모 툴바 <div>)이 아닌 하나의 알림 (<button> 에서)만 표시됩니다.
    버튼을 클릭하는 것과 주변 툴바를 클릭하는 것은 다르므로 이 UI에서는 전파를 중지하는 것이 적절합니다.

    이 클릭 핸들러가 코드 한 줄을 실행한 다음 부모가 전달한 onClick prop을 호출하는 방식을 주목하세요

    jsx

    function Button({ onClick, children }) {
      return (
        <button
          onClick={(e) => {
            e.stopPropagation()
            onClick()
          }}>
          {children}
        </button>
      )
    }
    

    부모 onClick 이벤트 핸들러를 호룰하기 전에 이 핸들러에 코드를 더 추가할 수도 있습니다. 이 패턴은 전파에 대한 대안을 제공하빈다.
    자식 컴포넌트가 이벤트를 처리하는 동시에 부모 컴포넌트가 몇 가지 추가 동작을 지정할 수 있게 해줍니다. 프로퍼게이션과 달리 자동이 아닙니다.
    하지만 이 패턴의 장점은 특정 이벤트의 결과로 실행되는 전체 체인 코드를 명확하게 따라갈 수 있다는 것입니다.

    전파에 의존하고 있고 어떤 핸들러가 실행되고 왜 실행되는지 추적하기 어려운 경우 대신 이 접근 방식을 시도해 보세요.

    일부 브라우저 이벤트에는 연결된 기본 동작이 있습니다. 예를 들어, <form> submit 이벤트는 내부의 버튼을 클릭할 때 발생하며 기본적으로 전체 페이지를 다시 로드합니다.

    이럴 때는 이벤트 객체에서 e.preventDefault()를 호출하여 이런 일이 발생하지 않도록 할 수 있습니다.

    e.stopPropagation()e.preventDefault()를 혼동하지 마세요. 둘 다 유용하지만 서로 관련이 없습니다.

    • e.stopPropagation()은 위 태그에 연결된 이벤트 핸들러의 실행을 중지합니다.
    • e.preventDefault()는 해당 이벤트가 있는 몇 가지 이벤트에 대해 기본 브라우저 동작을 방지합니다.

    이벤트 핸들러에 부작용이 생길 수 있나요?

    물론입니다! 이벤트 핸들러는 부작용이 가장 많이 발생하는 곳입니다.
    렌더링 함수와 달리 이벤트 핸들러는 순수할 필요가 없으므로 타이핑에 대한 응답으로 input 값을 변경하거나 버튼 누름에 대한 응답으로 목록을 변경하는 등 무언가를 변경하기에 좋은 곳입니다.
    하지만 일부 정보를 변경하려면 먼저 정보를 저장할 방법이 필요합니다. React에서는 컴포넌트의 메모리인 state를 사용해 이 작업을 수행합니다.

    state: 컴포넌트의 메모리

    다음은 조각상 이미지를 렌더링하는 컴포넌트입니다. "Next" 버튼을 클릭하면 index1, 2로 변경하여 다음 조각상을 표시해야 합니다.
    그러나 이것은 작동하지 않습니다. 아래의 예제를 테스트해보세요!

    handleClick 이벤트 핸들러가 지역 변수 index를 업데이트하고 있습니다. 하지만 두 가지 이유로 인해 변경 사항이 표시되지 않습니다.

    1. 지역 변수는 렌더링 간에 유지되지 않습니다. React는 이 컴포넌트를 두 번째로 렌더링 할 때 지역 변수에 대한 변경 사항을 고려하지 않고 처음부터 렌더링합니다.
    2. 지역 변수를 변경해도 렌더링을 발동시키지 않습니다. React는 새로운 데이터로 컴포넌트를 다시 렌더링해야 한다는 것을 인식하지 못합니다.

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

    1. 렌더링 사이에 데이터를 유지합니다.
    2. 새로운 데이터로 컴포넌트를 렌더링(리렌더링)하도록 React를 촉발 합니다.

    useState 훅은 이 두가지를 제공합니다.

    1. 렌더링 사이에 데이터를 유지하기 위한 state 변수.
    2. 변수를 업데이트하고 React가 컴포넌트를 다시 렌더링하도록 촉발하는 state 설정자 함수.

    state 변수를 추가하려면 파일 상단에 있는 React에서 useState를 import합니다.

    jsx

    import { useState } from 'react'
    

    그런다음 위 예제의 로직을 다음과 같이 바꿉니다.

    jsx

    // bad
    let index = 0
    
    // good
    const [index, setindex] = useState(0)
    

    index는 state 변수이고 setIndex는 설정자 함수입니다.

    구조분해 할당

    위에서 [] 구문을 배열 구조분해라고 하며, 배열에서 값을 읽어올 수 있습니다. useState가 반환하는 배열에는 항상 정확히 두 개의 항목이 있습니다.

    이것이 handleClick에서 함께 작동하는 방식입니다.

    jsx

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

    이제 "Next" 버튼을 클릭하면 현재 조각상이 바뀝니다.

    React에서는 useState를 비롯해 use로 시작하는 다른 함수를 훅(hook)이라고 부릅니다.
    훅은 React가 렌더링 중일 때만 사용할 수 있는 특별한 함수입니다. 이를 통해 다양한 React의 기능들을 "연결"할 수 있습니다.
    state는 이러한 기능 중 하나일 뿐이며, 나중에 다른 훅들도 만나게 될 것입니다.

    useState를 호출하는 것은, React에게 이 컴포넌트가 무언가를 기억하기를 원한다고 말하는 것입니다.

    jsx

    const [index, setIndex] = useState(0)
    

    이 경우에는 React가 index를 기억하기를 원합니다.

    useState의 유일한 인수는 state 변수의 초기값입니다. 이 예제에서는 useState(0)에 의해 index의 초기값이 0으로 설정되어 있습니다.

    컴포넌트가 렌더링될 때마다 useState는 두 개의 값을 포함하는 배열을 제공합니다.

    1. 저장한 값을 가진 state 변수
    2. state 변수를 업데이트하고 React가 컴포넌트를 다시 렌더링하도록 촉발할 수 있는 state 설정자 함수

    그렇기때문에 위의 useState 구문은 다음과 같이 작동합니다.

    1. 컴포넌트가 처음 렌더링됩니다. index의 초기값으로 0useState에 전달했으므로 [0, setIndex]가 반환됩니다. React는 0을 최신 state 값으로 기억합니다.
    2. state를 업데이트합니다. 사용자가 버튼을 클릭하면 setIndex(index + 1)를 호출합니다. index0이므로 setIndex(1)입니다. 이렇게 하면 React는 이제 index1임을 기억하고 다음 렌더링을 촉발합니다.
    3. 컴포넌트가 두 번째로 렌더링됩니다. React는 여전히 useState(0)을 보지만, index1로 설정한 것을 기억하고 있기 때문에, 이번에는 [1, setIndex]를 반환합니다.
    4. 이런식으로 계속됩니다!

    하나의 컴포넌트에 원하는 만큼 많은 유형의 state 변수를 가질 수 있습니다. 이 컴포넌트에는 숫자 타입 index와, '세부 정보 표시'를 클릭하면 토글되는 불리언 타입인 showMore라는, 두 개의 state 변수가 있습니다.

    이 예제에서 indexshowMore처럼 서로 연관이 없는 경우 여러 개의 state 변수를 갖는 것이 좋습니다.
    그러나 두 개의 state 변수를 자주 함께 변경하는 경우에는 두 변수를 하나로 합치는 것이 더 좋을 수 있습니다.

    예를 들어, 필드가 많은 폼의 경우 필드별로 state 변수를 사용하는 것보다 객체를 값으로 하는 하나의 state 변수를 사용하는것이 더 편리합니다.

    React는 어떤 state를 반환할지 어떻게 알 수 있을까요? (1)

    useState 호출이 어떤 state 변수를 참조하는지에 대한 정보를 받지 못한다는 것을 눈치채셨을 것입니다.
    useState에 전달되는 "식별자"가 없는데 어떤 state 변수를 반환할지 어떻게 알 수 있을까요? 함수를 파싱하는 것과 같은 마법에 의존할까요? 대답은 '아니오' 입니다.

    React는 어떤 state를 반환할지 어떻게 알 수 있을까요? (2)

    React는 간결한 구문을 구현하기 위해 훅은 동일한 컴포넌트의 모든 렌더링에서 안정적인 호출 순서에 의존합니다.
    위의 규칙("최상위 수준에서만 훅 호출")을 따르면, 훅은 항상 같은 순서로 호출되기 때문에 실제로 잘 작동합니다. 또한 린터 플러그인은 대부분의 실수를 잡아줍니다.
    내부적으로 React는 모든 컴포넌트에 대해 한 쌍의 state 배열을 가집니다. 또한 렌더링 전에 0으로 설정된 현재 쌍 인덱스를 유지합니다.
    useState를 호출할 때마다 React는 다음 state 쌍을 제공하고 인덱스를 증가시킵니다.

    React는 어떤 state를 반환할지 어떻게 알 수 있을까요? (3)

    링크의 예제에서는 React를 사용하지 않지만, 내부적으로 useState가 어떻게 작동하는지에 대한 아이디어를 제공합니다.
    How does React know which state to return?
    React를 사용하기 위해 이 작동 방식을 이해해야할 필요는 없지만, 유용한 멘탈 모델이 될 수 있을 것입니다.

    state는 화면의 컴포넌트 인스턴스에 지역적입니다. 즉, 동일한 컴포넌트를 두 군데에서 렌더링하면 각 사본은 완전히 격리된 state를 갖게 됩니다!
    이 중 하나를 변경해도 다른 컴포넌트에는 영향을 미치지 않습니다.

    이 예시에서는 앞의 Gallery 컴포넌트가 로직을 변경하지 않고 두 군데에서 렌더링되었습니다. 각 갤러리 내부의 버튼을 클릭해 보세요. 각각의 state가 독립적인 것을 확인할 수 있습니다.

    이것이 바로 모듈 상단에 선언하는 일반 변수와 state의 차이점입니다. state는 특정 함수 호출에 묶이지 않고, 코드의 특정 위치에 묶이지도 않지만, 화면상의 특정 위치에 "지역적"입니다.
    두 개의 <Gallery /> 컴포넌트를 렌더링했으므로 해당 state는 별도로 저장됩니다.

    Page 컴포넌트는 Gallery의 state뿐 아니라 심지어 state가 있는지 여부조차 전혀 "알지 못한다"는 점도 주목하세요.
    props와 달리 state는 이를 선언하는 컴포넌트 외에는 완전히 비공개이며, 부모 컴포넌트는 이를 변경할 수 없습니다.
    따라서 다른 컴포넌트에 영향을 주지 않고 state를 추가하거나 제거할 수 있습니다.

    두 Gallery 컴포넌트의 state를 동기화하려면 어떻게 해야 할까요? React에서 이를 수행하는 올바른 방법은 자식 컴포넌트에서 state를 제거하고 가장 가까운 공유 부모 컴포넌트에 추가하는 것입니다.
    다음 몇 페이지는 단일 컴포넌트의 state를 구성하는 데 초점을 맞추겠지만, 이 주제는 컴포넌트 간의 state 공유에서 다시 다룰 것입니다.

    렌더링하고 커밋하기

    컴포넌트가 화면에 표시하기 이전에 React에서 렌더링을 해야 합니다. 해당 과정의 단계를 이해하면 코드가 어떻게 실행되는지 이해할 수 있고 React 동작에 관해 설명하는데 도움이 됩니다.

    주방에서 요리사가 컴포넌트를 재료로 맛있는 요리를 한다고 상상해보세요. 이 시나리오에서 React는 고객들의 요청을 받고 주문을 가져오는 웨이터입니다.
    이 과정에는 UI를 요청하고 제공하는 세 가지 단계가 있습니다.

    1. 렌더링 촉발 (손님의 주문을 주방으로 전달)
    2. 컴포넌트 렌더링 (주방에서 주문 받기)
    3. DOM에 커밋 (테이블에 주문한 요리 내놓기)

    request

    컴포넌트 렌더링이 일어나는 두 가지 이유

    1. 컴포넌트의 첫 렌더링인 경우
    2. 컴포넌트의 state(또는 상위 요소 중 하나)가 업데이트된 경우

    앱을 시작하기 위해서는 첫 렌더링을 촉발시켜야 합니다. 프레임워크와 샌드박스가 때때로 코드를 숨기지만, 대상 DOM노드로 createRoot를 호출한 다음 컴포넌트로 render 메서드를 호출하면 됩니다.

    컴포넌트가 처음 렌더링되면 set 함수로 state를 업데이트하여 추가 렌더링을 촉발시킬 수 있습니다. 컴포넌트의 state를 업데이트하면 자동으로 렌더링이 대기열에 추가됩니다.

    rendering

    렌더링을 촉발시키면, React는 컴포넌트를 호출하여 화면에 표시할 내용을 파악합니다. "렌더링"은 React에서 컴포넌트를 호출하는 것입니다.

    • 첫 렌더링에서 React는 루트 컴포넌트를 호출합니다.
    • 이후 렌더링에서 React는 state 업데이트에 의해 렌더링이 발동된 함수 컴포넌트를 호출합니다.

    이 과정은 재귀적입니다. 업데이트된 컴포넌트가 다른 컴포넌트를 반환하면 React는 다음으로 해당 컴포넌트를 렌더링하고 해당 컴포넌트도 컴포넌트를 반환하면 반환된 컴포넌트를 다음에 렌더링하는 방식입니다.

    중첩된 컴포넌트가 더 이상 없고 React가 화면에 표시되어야 하는 내용을 정확히 알 때까지 이 단계는 계속됩니다.

    다음 예제에서는 React는 Gallery()Image()를 여러 번 호출합니다.

    • 첫 렌더링을 하는 동안 React <section>, <h1> 그리고 3개의 <img> 태그에 대한 DOM 노드를 생성합니다.
    • 리렌더링하는 동안 React는 이전 렌더링 이후 변경된 속성을 계산합니다. 다음 단계인 커밋 단계까지는 해당 정보로 아무런 작업도 수행하지 않습니다.

    렌더링은 항상 순수한 계산이어야 합니다

    • 동일한 입력에는 동일한 출력을 해야합니다. 동일한 입력이 주어지면 컴포넌트는 항상 동일한 JSX를 반환해야 합니다.
    • 이전의 state를 변경해서는 안됩니다. 렌더링 전에 존재했던 객체나 변수를 변경해서는 안 됩니다.

    컴포넌트를 렌더링한 후 React는 DOM을 수정합니다.

    • 초기 렌더링의 경우 Reactsms appendChild() DOM API를 사용하여 생성한 모든 DOM 노드를 화면에 표시합니다.
    • 리렌더링의 경우 React는 필요한 최소한의 작업을 적용하여 DOM이 최신 렌더링 출력과 일치하도록 합니다.

    React는 렌더링 간에 차이가 있는 경우에만 DOM 노드를 변경합니다. 예를 들어, 매초 부모로부터 전달된 다른 props로 다시 렌더링하는 컴포넌트가 있습니다.
    <input>에 텍스트를 입력하여 value를 업데이트하지만 컴포넌트가 리렌더링될 때 텍스트가 사라지지 않습니다.

    마지막 단계에서 React가 <h1>의 내용만 새로운 time으로 업데이트하기 때문입니다. <input>이 JSX에서 이전과 같은 위치로 확인되므로 React는 <input>또는 value를 건드리지 않습니다!

    렌더링이 완료되고 React가 DOM을 업데이트한 후 브라우저는 화면을 다시 그립니다. 이 단계를 "브라우저 렌더링"이라고 하지만 이 문서의 나머지 부분에서 혼동을 피하고자 "페인팅"이라고 부를 것입니다.

    스냅샷으로서의 state

    state 변수는 읽고 쓸 수 있는 일반 JS 변수처럼 보일 수 있습니다. 하지만 state는 스냅샷처럼 동작합니다.
    state 변수를 설정해도 이미 가지고 있는 state 변수는 변경되지 않고, 대신 리렌더링이 실행됩니다.

    클릭과 같은 사용자 이벤트에 반응하여 사용자 인터페이스가 직접 변경된다고 생각할 수 있습니다.
    React에서는 이 멘탈 모델과는 조금 다르게 작동합니다. 이전 페이지에서 state를 설정하면 React에 리렌더링을 요청하는 것을 보았습니다.

    즉, 인터페이스가 이벤트에 반응하려면 state를 업데이트해야 합니다.

    아래의 예시에서는 "send"를 누르면 setIsSent(true)가 React에 UI를 다시 렌더링하도록 지시합니다.

    버튼을 클릭하면 다음과 같은 일이 발생합니다.

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

    rerendering

    state와 렌더링의 관계를 자세히 살펴보겠습니다.

    "렌더링"이란 React가 컴포넌트, 즉 함수를 호출한다는 뜻입니다. 해당 함수에서 반환하는 JSX는 시간상 UI의 스냅샷과 같습니다.
    prop, 이벤트 핸들러, 로컬 변수는 모두 렌더링 당시의 state를 사용해 계산됩니다.

    사진이나 동영상 프레임과 달리 반환하는 UI '스냅샷'은 대화형입니다. 여기에는 input에 대한 응답으로 어떤 일이 일어날지 지정하는 이벤트 핸들러와 같은 로직이 포함됩니다.
    그러면 React는 이 스냅샷과 일치하도록 화면을 업데이트하고 이벤트 핸들러를 연결합니다. 결과적으로 버튼을 누르면 JSX에서 클릭 핸들러가 발동됩니다.

    React가 컴포넌트를 다시 렌더링할 때 다음과 같은 과정이 발생합니다.

    1. React가 함수를 다시 호출합니다.
    2. 함수가 새로운 JSX 스냅샷을 반환합니다.
    3. 그러면 React가 반환한 스냅샷과 일치하도록 화면을 업데이트합니다.

    컴포넌트의 메모리로서 state는 함수가 반환된 후 사라지는 일반 변수와 다릅니다. state는 실제로 함수 외부에 마치 선반에 잇는 것처럼 React 자체에 "존재" 합니다.
    React가 컴포넌트를 호출하면 특정 렌더링에 대한 state의 스냅샷을 제공합니다. 컴포넌트는 해당 렌더링의 state 값을 사용해 계산된 새로운 props 세트와 이벤트 핸들러가 포함된 UI의 스냅샷을 JSX에 반환합니다!

    snapshot

    다음은 이것이 어떻게 작동하는지 보여주는 간단한 실험입니다. 이 예제에서는 '+3' 버튼을 클릭하면 setNumber(number + 1)를 세 번 호출하므로 카운터가 세 번 증가할 것으로 예상할 수 있습니다.

    의도대로 기능이 잘 작동하나요? 아닙니다. 버튼을 클릭했을 때 number는 한 번만 증가하고 있습니다.

    state를 설정하면 다음 렌더링에 대해서만 변경됩니다. 첫 번째 렌더링에서 number0이었습니다.
    따라서 해당 렌더링의 onClick 핸들러에서 setNumber(number + 1)가 호출된 후에도 number의 값은 여전히 0입니다.

    jsx

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

    이 버튼의 클릭 핸들러가 React에게 지시하는 작업은 다음과 같습니다.

    1. setNumber(number + 1): number0이므로 setNumber(0 + 1)입니다.
      • React는 다음 렌더링에서 number1로 변경할 준비를 합니다.
    2. setNumber(number + 1): number0이므로 setNumber(0 + 1)입니다.
      • React는 다음 렌더링에서 number1로 변경할 준비를 합니다.
    3. setNumber(number + 1): number0이므로 setNumber(0 + 1)입니다.
      • React는 다음 렌더링에서 number1로 변경할 준비를 합니다.

    setNumber(number + 1)를 세 번 호출했지만, 이 렌더링에서 이벤트 핸들러의 number는 항상 0이므로 state를 1로 세 번 설정했습니다.
    이것이 이벤트 핸들러가 완료된 후 React가 컴포넌트 안의 number3이 아닌 1로 다시 렌더링하는 이유입니다.

    코드에서 state 변수를 해당 값으로 대입하여 이를 시각화할 수도 있습니다. 이 렌더링에서 number state 변수는 0이므로 이벤트 핸들러는 다음과 같습니다.

    jsx

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

    다음 렌더링에서는 number1이므로 렌더링의 클릭 핸들러는 다음과 같이 표시됩니다.

    jsx

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

    그렇기 때문에 버튼을 다시 클릭하면 카운터가 2로 설정되고, 다음 클릭 시에는 3으로 설정되는 방식입니다.

    그렇다면 아래의 예제에서 버튼을 클릭하면 어떤 알림이 표시되는지 맞춰보세요.

    아까와 비슷하게 생각한다면 첫 클릭에 알람으로 "0"이 표시된다는 것을 짐작할 수 있습니다.

    jsx

    setNumber(0 + 5)
    alert(0)
    

    하지만 경고에 타이머를 설정하여 컴포넌트가 다시 렌더링된 후에만 발동하도록 하면 어떨까요? "0" 또는 "5"라고 표시될까요? 맞춰보세요!

    맞추셨나요? 이전과 같은 방법을 사용하면 이 또한 쉽게 이해할 수 있습니다.

    jsx

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

    React에 저장된 state는 알림이 실행될 때 변경되었을 수 있지만, 사용자가 상호작용한 시점에 state 스냅샷을 사용하는 건 이미 예약되어 있던 것입니다!

    state 변수의 값은 이벤트 핸들러의 코드가 비동기적이더라도 렌더링 내에서 절대 변경되지 않습니다.
    해당 렌더링의 onClick내에서, setNumber(number + 5)가 호출된 후에도 number의 값은 계속 0입니다.
    이 값은 컴포넌트가 호출해 React가 UI의 "스냅샷을 찍을" 때 "고정"된 값입니다.

    다음은 이벤트 핸들러가 타이밍 실수를 줄이는 방법을 보여주는 예입니다. 아래는 5초 지연된 메시지를 보내느 양식입니다. 이 시나리오를 상상해보세요

    1. "보내기" 버튼을 눌러 "Alice"에게 "안녕하세요"를 보냅니다.
    2. 5초 지연이 끝나기 전에 "받는 사람" 필드의 값을 "Bob"으로 변경합니다.

    alert에 어떤 내용이 표시될까요? "앨리스에게 인사했습니다" 라고 표시될까요? 아니면 "밥에게 인사했습니다" 라고 표시될까요?
    아래의 코드를 실행해보세요 :)

    React는 하나의 렌더링 이벤트 핸들러 내에서 state값을 "고정"으로 유지합니다. 코드가 실행되는 동안 state가 변경되었는지 걱정할 필요가 없습니다.

    하지만 다시 렌더링하기 전에 최신 state를 읽고 싶다면 어떻게 해야 할까요? 다음 페이지에서 설명하는 state 업데이터 함수를 사용하면 됩니다!

    여러 state 업데이트를 큐에 담기

    state 변수를 들어가면 다음 렌더링이 큐에 들어갑니다. 그러나 경우에 따라 다음 렌더링 큐에 넣기 전에, 값에 대해 여러 작업을 수행하고 싶을 때도 있습니다.
    이를 위해서는 React가 state 업데이트를 어떻게 배치하면 좋을지 이해하는 것이 도움이 됩니다.

    React는 state를 업데이트를 하기 전에 이벤트 핸들러의 모든 코드가 실행될 때까지 기다립니다. 이 때문에 리렌더링은 모든 setState()의 호출이 완료된 이후에만 일어납니다. 이러한 실행 동작은 위의 예에서 확인한 바 있습니다.

    이는 음식점에서 주문을 받는 웨이터를 생각해 볼 수 있습니다. 웨이터는 첫번째 요리를 말하자마자 주방으로 달려가지 않습니다!
    대신 주문이 끝날 떄까지 기다렸다가 주문을 변경하고, 심지어 테이블에 있는 다른 사람의 주문도 받습니다.

    setState

    이렇게 하면 너무 많은 리렌더링을 촉발하지 않고도 여러 컴포넌트에서 나온 다수의 state 변수를 업데이트할 수 있습니다.
    하지만 이는 이벤트 핸들러와 그 안에 있는 코드가 완료될 때까지 UI가 업데이트되지 않는다는 의미이기도 합니다.

    일괄처리(배칭)라고도 하는 이 동작은 React 앱을 훨씬 빠르게 실행할 수 있게 해줍니다. 또한 일부 변수만 업데이트된 "반쯤 완성된" 혼란스러운 렌더링을 처리하지 않아도 됩니다.

    React는 클릭과 같은 여러 의도적인 이벤트에 대해 일괄 처리하지 않으며, 각 클릭은 개별적으로 처리됩니다.
    React는 일반적으로 안전한 경우에만 일괄 처리를 수행하니 안심하세요. 예를 들어, 첫 번째 버튼 클릭으로 양식이 비활성화되면 두 번째 클릭으로 양식이 다시 제출되지 않도록 보장합니다.

    흔한 사례는 아니지만, 다음 렌더링 전에 동일한 state 변수를 여러 번 업데이트 하고 싶다면 setNumber(number + 1)와 같은 다음 state 값을 전달하는 것 대신, setNumber(prev => prev + 1)와 같이 큐의 이전 state를 기반으로 하는 다음 state를 계산하는 함수를 전달할 수 있습닏다.
    이는 단순히 state 값을 대체하는 것이 아니라 React에게 "state 값으로 무언가를 하라"고 지시하는 방법입니다.

    여기서 n => n + 1은 업데이터 함수(updater function)라고 부릅니다. 이를 state 설정자 함수에 전달할 때,

    1. React는 이벤트 핸들러의 다른 코드가 모두 실행된 후에 이 함수가 처리되도록 큐에 넣습니다.
    2. 다음 렌더링 중에 React는 큐를 순회하여 최종 업데이트된 state를 제공합니다.

    jsx

    setNumber((n) => n + 1)
    setNumber((n) => n + 1)
    setNumber((n) => n + 1)
    

    React가 이벤트 핸들러를 수행하는 동안 여러 코드를 통해 작동하는 방식은 다음과 같습니다.

    1. setNumber(n => n + 1) : n => n + 1 함수를 큐에 추가합니다.
    2. setNumber(n => n + 1) : n => n + 1 함수를 큐에 추가합니다.
    3. setNumber(n => n + 1) : n => n + 1 함수를 큐에 추가합니다.

    다음 렌더링 중에 useState를 호출하면 React는 큐를 순회합니다. 이전 number state는 0이었으므로 React는 이를 첫 번째 업데이터 함수에 n 인수로 전달합니다.
    그런 다음 React는 이전 업데이터 함수의 반환 값을 가져와서 다음 업데이터 함수에 n으로 전달하는 식으로 반복합니다.

    따라서 해당 예제는 이전의 정적인 값을 지정하던 예제와는 다르게 3을 최종 결과로 저장하고 useState에서 반환합니다.
    이것이 위 예제의 "+3"을 클릭하면 값이 3씩 올바르게 증가하는 이유입니다.

    그렇다면 이 이벤트 핸들러는 어떨까요? 다음 렌더링에서 number가 어떻게 될까요?

    이 이벤트 핸들러가 React에 지시하는 작업은 다음과 같습니다.

    1. setNumber(number + 5) : number0이므로 setNumber(0 + 5)`입니다. React는 큐에 "5로 바꾸기"를 추가합니다.
    2. setNumber(n => n + 1) : n => n + 1`는 업데이터 함수입니다. React는 해당 함수를 큐에 추가합니다.

    여기에서 우리는 setState(5)가 실제로는 setState(n => 5)처럼 동작하지만 n이 사용되지 않는다는 것을 눈치챌 수 있습니다.

    이벤트 핸들러가 완료되면 React는 리렌더링을 실행합니다. 리렌더링 하는 동안 React는 큐를 처리합니다.
    업데이터 함수는 렌더링 중에만 실행되므로, 업데이터 함수는 순수해야하며 결과만 반환해야 합니다. 업데이터 함수 내부에서 state를 변경하거나 다른 사이드 이펙트를 실행하려고 하지 마세요.
    Strict 모드에서 React는 각 업데이터 함수를 두 번 실행(두 번째 결과는 버림)하여 실수를 찾을 수 있도록 도와줍니다.

    업데이터 함수 인수의 이름은 해당 state 변수의 첫 글자로 지정하는 것이 일반적입니다.

    jsx

    setEnabled((e) => !e)
    setLastName((ln) => ln.reverse())
    setFriendCount((fc) => fc * 2)
    

    좀 더 자세한 코드를 선호하는 경우 setEnabled(enabled => !enabled)와 같이 전체 state 변수 이름을 반복하거나,
    setEnabled(prevEnabled => !prevEnabled)와 같은 접두사를 사용하는 것이 널리 사용되는 규칙입니다.

    객체 state 업데이트

    State에는 모든 종류의 자바스크립트 값을 저장할 수 있습니다.

    jsx

    const [x, setX] = useState(0)
    

    지금까지 숫자, 문자열, 불리언을 다루었습니다. 이러한 자바스크립트 값들은 변경할 수 없거나 "읽기 전용"을 의미하는 "불변성"을 가집니다.
    값을 교체하기 위해서는 리렌더링이 필요합니다.

    jsx

    setX(5)
    

    x state는 0에서 5로 바뀌었지만, 숫자 0 자체는 바뀌지 않았습니다. 숫자, 문자열, 불리언과 같이 자바스크립트에 정의되어 있는 원시 값들은 변경할 수 없습니다.
    state에 있는 이러한 객체를 생각해보세요.

    jsx

    const [position, setPosition] = useState({ x: 0, y: 0 })
    

    기술적으로 객체 자체의 내용을 바꿀 수 있습니다. 이것을 변경(mutation)이라고 합니다

    jsx

    position.x = 5
    

    하지만 리액트 state의 객체들이 기술적으로 변경 가능할지라도, 숫자, 불리언, 문자열과 같이 불변성을 가진 것처럼 다루어야 합니다.
    객체를 변경하는 대신 교체해야 합니다.

    다시 말하면, state에 저장한 자바스크립트 객체는 어떤 것이라도 읽기 전용인 것처럼 다루어야 합니다.

    아래 예시에서 state의 object는 현재 포인터 위치를 나타냅니다. 프리뷰 영역을 누르거나 커서를 움직일 때 빨간 점이 이동해야 합니다. 하지만 점은 초기 위치에 머무릅니다.

    어떤게 문제인지 찾으셨나요? 문제는 해당 부분입니다.

    jsx

    onPointerMove={e => {
    position.x = e.clientX;
    position.y = e.clientY;
    }}
    

    이 코드는 position에 할당된 객체를 이전 렌더링에서 수정합니다. 그러나 react는 state 설정 함수가 없으면 객체가 변경되었는지 알 수 없습니다.
    따라서 리액트는 아무것도 하지 않스빈다. 이는 식사를 한 뒤에 주문을 바꾸려는 것과 같스빈다. state를 변경하는것이 어떤 경우에는 동작할 수 있지만, 권장하지 않습니다.
    렌더링 시에 접근하려는 state 값은 읽기 전용처럼 다루어야 합니다.

    이러한 경우에서 리렌더링을 발생시키고 싶다면, 새 객체를 생성하여 state 설정 함수로 전달하세요

    jsx

    onPointerMove={e => {
      setPosition({
        x: e.clientX,
        y: e.clientY
      });
    }}
    

    setPosition은 React에게 다음과 같이 요청합니다.

    • position을 이 새로운 객체로 교체하라
    • 그리고 이 컴포넌트를 다시 렌더링하라

    이제 프리뷰 영역을 누르거나 hover 시에 빨간 점이 포인터를 따라오는 것을 볼 수 있습니다.

    이전 예시에서 position 객체는 현재 커서 위치에서 항상 새롭게 생성됩니다. 하지만 종종 새로 생성하는 객체에 존재하는 데이터를 포함하고 싶을 수 있습니다.
    예를 들어 폼에서 단 한 개의 필드만 수정하고, 나머지 모든 필드는 이전 값을 유지하고 싶을 수 있습니다.

    이 input 필드는 onChange 핸들러가 state를 변경하기 때문에 동작하지 않습니다.

    예를 들어, 이 코드는 이전 렌더의 state를 변경합니다.

    jsx

    person.firstNAme = e.target.value
    

    원하는 동작을 정확히 얻기 위해서는 새로운 객체를 생성하여 setPerson으로 전달해야 합니다.
    하지만, 단 하나의 필드가 바뀌었기 때문에 기존에 존재하는 다른 데이터를 복사해야 합니다,

    jsx

    setPerson({
      firstName: e.target.value, // input의 새로운 first name
      lastName: person.lastName,
      email: person.email,
    })
    

    ... 객체 전개 구문을 사용하면 모든 프로퍼티를 각각 복사하지 않아도 됩니다.

    jsx

    setPerson({
      ...person, // 이전 필드를 복사
      firstName: e.target.value, // 새로운 부분은 덮어쓰기
    })
    

    이제 폼이 동작합니다!

    각 input 필드에 대해서 분리된 state를 선언하지 않았음을 기억하세요. 큰 폼들은 올바르게 업데이트한다면, 한 객체에 모든 데이터를 그룹화하여 저장하는 것이 편리합니다.

    ... 전개 문법은 "얕다"는 점을 알아두세요. 이것은 한 레벨 깊이의 내용만 복사합니다. 빠르지만, 중첩된 프로퍼티를 업데이트하고 싶다면 한 번 이상 사용해야 한다는 뜻이기도 합니다.

    얕은 복사와 깊은 복사

    자바스크립트에서 복사는 얕은 복사와 깊은 복사가 있습니다. 얕은 복사의 경우 원값이 변경되면 참조값도 변경되지만 깊은 복사의 경우 별개의 객체가 생성됩니다.

    얕은 복사

    자바스크립트에서 얕은 복사는 다음과 같이 사용할 수 있습니다.

    • ... 전개 연산자
    • Array.prototype.slice() (start와 end를 설정하지 않았을 때)
    • Object.assign()

    깊은 복사

    자바스크립트에서 깊은 복사는 다음과 같이 사용할 수 있습니다.

    • JSON.parse && JSON.stringify (function일 경우 undefined로 처리하여 사용 불가능)
    • 재귀함수로 직접 구현하기
    • 외부 라이브러리를 사용하기 (Lodash)

    여러 필드에 단일 이벤트 핸들러를 사용할 수도 있습니다.

    [] 괄호를 객체 정의 안에 사용하여 동적 이름을 가진 프로퍼티를 명시할 수 있습니다.
    아래는 이전 예제와 같지만, 세 개의 다른 이벤트 핸들러 대신 하나의 이벤트 핸들러를 사용하는 예제가 있습니다.

    jsx

    export default function Form() {
      const [person, setPerson] = useState({
        firstName: 'Barbara',
        lastName: 'Hepworth',
        email: 'bhepworth@sculpture.com',
      })
    
      function handleChange(e) {
        setPerson({
          ...person,
          [e.target.name]: e.target.value,
        })
      }
    
      return (
        <>
          <label>
            First name:
            <input
              name="firstName"
              value={person.firstName}
              onChange={handleChange}
            />
          </label>
          <label>
            Last name:
            <input
              name="lastName"
              value={person.lastName}
              onChange={handleChange}
            />
          </label>
          <label>
            Email:
            <input name="email" value={person.email} onChange={handleChange} />
          </label>
          <p>
            {person.firstName} {person.lastName} ({person.email})
          </p>
        </>
      )
    }
    

    아래와 같이 중첩된 객체 구조를 생각해 보세요.

    jsx

    const [person, setPerson] = useState({
      name: 'Niki de Saint Phalle',
      artwork: {
        title: 'Blue Nana',
        city: 'Hamburg',
        image: 'https://i.imgur.com/Sd1AgUOm.jpg',
      },
    })
    

    person.artwork.city를 업데이트 하고 싶다면, 변경하는 방법은 명백합니다.

    jsx

    person.artwork.city = 'New Delhi'
    

    하지만 React에서는 state를 변경할 수 없는 것으로 다루어야 합니다! city를 바꾸기 위해서는 먼저(이전 객체의 데이터로 생성된) 새로운 artwork 객체를 생성한 뒤, 그것을 가르키는 새로운 person 객체를 만들어야 합니다.

    jsx

    const nextArtwork = { ...person.artwork, city: 'New Delhi' }
    const nextPerson = { ...person, artwork: nextArtwork }
    setPerson(nextPerson)
    

    또는 단순하게 함수를 호출할 수 있습니다.

    jsx

    setPerson({
      ...person, // 다른 필드 복사
      artwork: {
        // artwork 교체
        ...person.artwork, // 동일한 값 사용
        city: 'New Delhi', // 하지만 New Delhi!
      },
    })
    

    이 방법은 코드가 길어질 수 있지만 많은 경우에 정상적으로 동작합니다.

    객체들은 사실 중첩되어 있지 않습니다

    js

    let obj = {
      name: 'Niki de Saint Phalle',
      artwork: {
        title: 'Blue Nana',
        city: 'Hamburg',
        image: 'https://i.imgur.com/Sd1AgUOm.jpg',
      },
    }
    

    위의 객체는 코드에서 "중첩되어" 나타납니다. 하지만, "중첩"은 객체의 동작에 대해 생각하는 부정확한 방법입니다.
    코드가 실행될 때, "중첩된" 객체라는 것은 없습니다. 실제로 당신은 두 개의 다른 객체를 보는 것입니다.

    js

    let obj1 = {
      title: 'Blue Nana',
      city: 'Hamburg',
      image: 'https://i.imgur.com/Sd1AgUOm.jpg',
    }
    
    let obj2 = {
      name: 'Niki de Saint Phalle',
      artwork: obj1,
    }
    

    obj1 객체는 obj2 "안"에 없습니다. obj3 또한 obj1을 "가리킬" 수 있기 때문입니다.

    js

    let obj1 = {
      title: 'Blue Nana',
      city: 'Hamburg',
      image: 'https://i.imgur.com/Sd1AgUOm.jpg',
    }
    
    let obj2 = {
      name: 'Niki de Saint Phalle',
      artwork: obj1,
    }
    
    let obj3 = {
      name: 'Copycat',
      artwork: obj1,
    }
    

    obj3.artwork.city를 변경하려 했다면, obj2.artwork.cityobj1.city 둘 다에 영향을 미칠 것입니다.
    이는 obj3.artwork, obj2.artworkobj1이 같은 객체이기 때문입니다. 객체를 "중첩된" 것으로 생각하면 이해하기 어려울 수 있습니다.
    그것들은 프로퍼티를 통해 서로를 "가리키는" 각각의 객체들입니다.

    state가 깊이 중첩되어있다면 평탄화를 고려해보세요. 만약 state 구조를 바꾸고싶지 않다면, 중첩 전개할 수 있는 더 간편한 방법이 있습니다.
    Immer는 편리하고, 변경 구문을 사용할 수 있게 해주며 복사본 생성을 도와주는 인기 있는 라이브러리입니다.

    Immer를 사용하면 작성한 코드는 "법칙을 깨고" 객체를 변경하는 것처럼 보일 수 있습니다.

    jsx

    updatePerson((draft) => {
      draft.artwork.city = 'Lagos'
    })
    

    하지만 일반적인 변경과는 다르게 이것은 이전 state를 덮어쓰지 않습니다!

    Immer는 어떻게 작동할까요?

    Immer가 제공하는 draft는 Proxy라고 하는 아주 특별한 객체 타입으로, 당신이 하는 일을 "기록"합니다. 객체를 원하는 만큼 자유롭게 변경할 수 있는 이유죠!
    Immer는 내부적으로 draft의 어느 부분이 변경되었는지 알아내어, 변경사항을 포함한 완전히 새로운 객체를 생성합니다.

    Immer를 사용하기 위해서는,

    1. package.json에 dependency로 use-immer를 추가하세요
    2. npm install을 실행하세요
    3. import { useState } from 'react'import { useImmer } from 'use-immer'로 교체하세요.

    이벤트 핸들러가 매우 간결해졌습니다. 하나의 컴포넌트 안에서 원하는 만큼 useStateuseImmer를 섞어 사용할 수 있습니다.
    Immer는 업데이트 핸들러를 간결하게 관리할 수 있는 좋은 방법이며, 특히 state가 중첩되어 있고 객체를 복사하는 것이 중복되는 코드를 만들 때 특히 유용합니다.

    왜 리액트에서 state 변경은 권장되지 않나요?

    1. 디버깅: state를 변경하지 않는다면 state가 렌더링 사이에 어떻게 바뀌었는지 명확하게 알 수 있습니다.
    2. 최적화: 보편적인 리액트 최적화 전략은 이전 props 또는 state가 다음 것과 동일할 때 일을 건너뛰는 것에 의존하기때문에 state를 절대 변경하지 않는다면 변경사항이 있었는지 확인하는 작업이 매우 빨라집니다.
    3. 새로운 기능: 우리가 만드는 새로운 리액트 기능들은 스냅샷처럼 다루어지는 것에 의존합니다. state의 과거 버전을 변경한다면 새로운 기능을 사용하지 못할 수 있습니다.
    4. 요구사항 변화: 취소/복원 구현, 변화 내역 조회, 사용자가 이전 값으로 폼을 재설정하기 등의 기능은 아무것도 변경되지 않았을 때 더 쉽습니다.
    5. 더 간단한 구현: 리액트는 변경에 의존하지 않기 때문에 객체로 뭔가 특별한 것을 할 필요가 없습니다. 프로퍼티를 가져오거나, 항상 프록시로 감싸거나 하는 등의 초기화 시 다른 작업을 하지 않아도 됩니다.

    배열 state 업데이트

    JS에서 배열은 다른 종류의 객체입니다. 객체와 마찬가지로 React state에서 배열은 읽기 전용으로 처리해야 합니다.
    arr[0] = 'bird'처럼 배열 내부의 항목을 재할당해서는 안되며 push()pop()같은 함수로 배열을 변경해서는 안됩니다.

    대신 배열을 업데이트할 때마다 새 배열을 state 설정 함수에 전달해야 합니다. 이를 위해 state의 원본 배열을 변경시키지 않는 filter()map() 함수를 사용하여 원본 배열로부터 새 배열을 만들수 있고, 이 배열들을 state에 설정하면 됩니다.

    다음은 일반적인 배열 연산에 대한 참조 표입니다. React state 내에서 배열을 다룰 땐, 다음과 같은 방법을 선호해야 합니다.

    비선호(배열을 변경)선호(새 배열을 반환)
    추가push, unshiftconcat, [...arr] 전개 연산자
    제거pop, shift, splicefilter, splice
    교체splice, arr[i] = ...map
    정렬reverse, sorttoReversed, toSorted

    또는 두 열의 함수를 모두 사용할 수 있도록 하는 Immer를 사용할 수 있습니다.

    배열 state의 값을 추가하고 싶다면 다음과 같은 방법을 사용하세요

    jsx

    const [artists, setArtists] = useState([])
    
    // bad
    artists.push({
      id: nextId++,
      name: name,
    })
    
    // good
    setArtists(
      // 아래의 새로운 배열로 state를 변경합니다.
      [
        ...artists, // 기존 배열의 모든 항목에,
        { id: nextId++, name: name }, // 마지막에 새 항목을 추가합니다.
      ]
    )
    
    // good (배열의 앞에 추가하고 싶을 경우)
    setArtists([
      { id: nextId++, name: name }, // 추가할 항목을 앞에 배치하고,
      ...artists, // 기존 배열의 항목들을 뒤에 배치합니다.
    ])
    

    배열 state의 값을 제거하고 싶다면 다음과 같은 방법을 사용하세요

    jsx

    let initialArtists = [
      { id: 0, name: 'Marta Colvin Andrade' },
      { id: 1, name: 'Lamidi Olonade Fakeye' },
      { id: 2, name: 'Louise Nevelson' },
    ]
    
    const [artists, setArtists] = useState(initialArtists)
    
    setArtists(artists.filter((a) => a.id !== artist.id))
    

    배열 state의 일부 또는 전체 항목을 변경하고자 하면 다음과 같은 방법을 사용하세요

    jsx

    let initialShapes = [
      { id: 0, type: 'circle', x: 50, y: 100 },
      { id: 1, type: 'square', x: 150, y: 100 },
      { id: 2, type: 'circle', x: 250, y: 100 },
    ]
    
    const [shapes, setShapes] = useState(initialShapes)
    
    const nextShapes = shapes.map((shape) => {
      if (shape.type === 'square') {
        // 변경시키지 않고 반환합니다.
        return shape
      } else {
        // 50px 아래로 이동한 새로운 원을 반환합니다.
        return {
          ...shape,
          y: shape.y + 50,
        }
      }
    })
    // 새로운 배열로 리렌더링합니다.
    setShapes(nextShapes)
    

    배열 state에 항목을 삽입하고 싶다면 다음과 같은 방법을 사용하세요

    jsx

    const initialArtists = [
      { id: 0, name: 'Marta Colvin Andrade' },
      { id: 1, name: 'Lamidi Olonade Fakeye' },
      { id: 2, name: 'Louise Nevelson' },
    ]
    
    const [artists, setArtists] = useState(initialArtists)
    
    const nextArtists = [
      // 삽입 지점 이전 항목
      ...artists.slice(0, insertAt),
      // 새 항목
      { id: nextId++, name: name },
      // 삽입 지점 이후 항목
      ...artists.slice(insertAt),
    ]
    setArtists(nextArtists)
    setName('')
    

    state 배열을 reverse 혹은 sort하고싶다면 다음과 같은 방법을 사용하세요

    jsx

    // reverse
    const items = [1, 2, 3]
    console.log(items) // [1, 2, 3]
    
    const reversedItems = items.toReversed()
    console.log(reversedItems) // [3, 2, 1]
    console.log(items) // [1, 2, 3]
    
    // sort
    const values = [1, 10, 21, 2]
    const sortedValues = values.toSorted((a, b) => a - b)
    console.log(sortedValues) // [1, 2, 10, 21]
    console.log(values) // [1, 10, 21, 2]
    

    아니면 이 모든걸 간단하게 Immer 라이브러리를 사용하여 간결하게 업데이트 로직을 작성할 수 있습니다.

    Immer를 사용하면 artwork.seen = nextSeen과 같이 변경해도 괜찮다는 것에 유의하세요.

    jsx

    updateMyTodos((draft) => {
      const artwork = draft.find((a) => a.id === artworkId)
      artwork.seen = nextSeen
    })
    

    이는 원본 state를 변경하는 것이 아니라, Immer에서 제공하는 특수 draft 객체를 변경하기 떄문입니다.
    마찬가지로 push()pop() 변경 함수들도 draft의 컨텐츠에 적용할 수 있습니다.

    Profile Image

    신현호

    Frontend Developer

    프론트엔드 개발자를 꿈꾸고 있는 대학생입니다. 끊임없이 배우고 성장하는 개발자가 되기 위해 노력하고 있습니다.

    React Docs

    총 5개의 포스트가 존재합니다.