HomeAboutMeBlogGuest
© 2025 Sejin Cha. All rights reserved.
Built with Next.js, deployed on Vercel
🐣
프론트엔드 데브코스 4기 교육생
/
🐶
이재호2팀
/
🌰
리액트 스터디
/
🌳
10월12일 목요일 1차스터디(Adding Interactivity - 상호작용 추가하기)
/
객체 state 업데이트

객체 state 업데이트

  • state는 객체를 포함해서, 어떤 종류의 JavaScript 값이든 저장할 수 있다.
    • 하지만 React state에 있는 객체를 직접 변경해서는 안된다.
    • 대신 객체를 업데이트하려면 새 객체를 생성하고(또는 기존 복사본을 만들고), 그 객체를 사용하도록 state를 설정해야 한다.
  1. mutation은 무엇인가?
      • 기술적으로 객체 자체의 내용을 변경하는 것은 가능하다. 이를 mutation 이라고 부른다.
      • React state의 객체는 기술적으로 mutation할 수 있지만 JavaScript 기본형처럼 불변하는 것으로 취급해야 한다.
        • 객체를 직접 mutation 하는 대신 항상 교체해야 한다.
  1. state를 읽기 전용으로 다루자
      • 즉, state에 넣는 모든 JavaScript 객체를 읽기 전용으로 취급해야 한다는 것이다.
      • 아래 코드는 정상동작하지 않는다.
import { useState } from 'react'; export default function MovingDot() { const [position, setPosition] = useState({ x: 0, y: 0 }); return ( <div onPointerMove={e => { position.x = e.clientX; position.y = e.clientY; }} style={{ position: 'relative', width: '100vw', height: '100vh', }}> <div style={{ position: 'absolute', backgroundColor: 'red', borderRadius: '50%', transform: `translate(${position.x}px, ${position.y}px)`, left: -10, top: -10, width: 20, height: 20, }} /> </div> ); }
  • 이유는 아래 코드에 있다.
onPointerMove={e => { position.x = e.clientX; position.y = e.clientY; }}
  • 이 코드는 이전 렌더링에서 position 객체에 할당된 값을 수정한다.
    • 그러나 state 설정자 함수르 사용하지 않으면 React는 객체가 변이된 사실을 알지 못한다.
    • 그래서 React는 아무런 반응도 하지 않는다.
    • 따라서 state 값은 읽기 전용으로 취급해야 한다.
onPointerMove={e => { setPosition({ x: e.clientX, y: e.clientY }); }}
Local mutation은 괜찮다.
const nextPosition = {}; nextPosition.x = e.clientX; nextPosition.y = e.clientY; setPosition(nextPosition);
setPosition({ x: e.clientX, y: e.clientY });
  • mutation은 이미 state가 있는 기존 객체를 변경할 때만 문제가 된다.
  • 객체를 변경해도 해당 객체에 의존하는 다른 객체에 실수로 영향을 미치지 않는다.
    • 이를 Local mutation이라고 한다.
    • 렌더링하는 동안에도 Local mutation을 수행할 수 있다.
  1. 전개 구문을 사용하여 객체 복사하기
import { useState } from 'react'; export default function Form() { const [person, setPerson] = useState({ firstName: 'Barbara', lastName: 'Hepworth', email: 'bhepworth@sculpture.com' }); function handleFirstNameChange(e) { person.firstName = e.target.value; } function handleLastNameChange(e) { person.lastName = e.target.value; } function handleEmailChange(e) { person.email = e.target.value; } return ( <> <label> First name: <input value={person.firstName} onChange={handleFirstNameChange} /> </label> <label> Last name: <input value={person.lastName} onChange={handleLastNameChange} /> </label> <label> Email: <input value={person.email} onChange={handleEmailChange} /> </label> <p> {person.firstName}{' '} {person.lastName}{' '} ({person.email}) </p> </> ); }
  • 이 경우 새 객체를 생성하고 이를 setPerson에 전달해야 정상 동작한다.
    • 큰 form의 경우 올바르게 업데이트 하기만 하면 모든 데이터를 객체에 그룹화하여 보관하는 것이 매우 편리하다.
import { useState } from 'react'; export default function Form() { const [person, setPerson] = useState({ firstName: 'Barbara', lastName: 'Hepworth', email: 'bhepworth@sculpture.com' }); function handleFirstNameChange(e) { setPerson({ ...person, firstName: e.target.value }); } function handleLastNameChange(e) { setPerson({ ...person, lastName: e.target.value }); } function handleEmailChange(e) { setPerson({ ...person, email: e.target.value }); } return ( <> <label> First name: <input value={person.firstName} onChange={handleFirstNameChange} /> </label> <label> Last name: <input value={person.lastName} onChange={handleLastNameChange} /> </label> <label> Email: <input value={person.email} onChange={handleEmailChange} /> </label> <p> {person.firstName}{' '} {person.lastName}{' '} ({person.email}) </p> </> ); }
여러 필드에 단일 이벤트 핸들러 사용하기
  • 위의 코드는 아래처럼 동적 프로퍼티(계산된 프로퍼티 속성)를 지정해서 단일 이벤트 핸들러를 사용하도록 수정할 수도 있다.
import { useState } from 'react'; 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> </> ); }
  1. 중첩된 객체 업데이트하기
    1. import { useState } from 'react'; export default function Form() { const [person, setPerson] = useState({ name: 'Niki de Saint Phalle', artwork: { title: 'Blue Nana', city: 'Hamburg', image: '<https://i.imgur.com/Sd1AgUOm.jpg>', } }); function handleNameChange(e) { setPerson({ ...person, name: e.target.value }); } function handleTitleChange(e) { setPerson({ ...person, artwork: { ...person.artwork, title: e.target.value } }); } function handleCityChange(e) { setPerson({ ...person, artwork: { ...person.artwork, city: e.target.value } }); } function handleImageChange(e) { setPerson({ ...person, artwork: { ...person.artwork, image: e.target.value } }); } return ( <> <label> Name: <input value={person.name} onChange={handleNameChange} /> </label> <label> Title: <input value={person.artwork.title} onChange={handleTitleChange} /> </label> <label> City: <input value={person.artwork.city} onChange={handleCityChange} /> </label> <label> Image: <input value={person.artwork.image} onChange={handleImageChange} /> </label> <p> <i>{person.artwork.title}</i> {' by '} {person.name} <br /> (located in {person.artwork.city}) </p> <img src={person.artwork.image} alt={person.artwork.title} /> </> ); }
  1. Immer로 간결한 업데이트 로직 작성하기
      • Immer는 mutation 구문을 사용하여도 사본을 생성해주는 라이브러리이다.
      updatePerson(draft => { draft.artwork.city = 'Lagos'; });
      import { useImmer } from 'use-immer'; export default function Form() { const [person, updatePerson] = useImmer({ name: 'Niki de Saint Phalle', artwork: { title: 'Blue Nana', city: 'Hamburg', image: '<https://i.imgur.com/Sd1AgUOm.jpg>', } }); function handleNameChange(e) { updatePerson(draft => { draft.name = e.target.value; }); } function handleTitleChange(e) { updatePerson(draft => { draft.artwork.title = e.target.value; }); } function handleCityChange(e) { updatePerson(draft => { draft.artwork.city = e.target.value; }); } function handleImageChange(e) { updatePerson(draft => { draft.artwork.image = e.target.value; }); } return ( <> <label> Name: <input value={person.name} onChange={handleNameChange} /> </label> <label> Title: <input value={person.artwork.title} onChange={handleTitleChange} /> </label> <label> City: <input value={person.artwork.city} onChange={handleCityChange} /> </label> <label> Image: <input value={person.artwork.image} onChange={handleImageChange} /> </label> <p> <i>{person.artwork.title}</i> {' by '} {person.name} <br /> (located in {person.artwork.city}) </p> <img src={person.artwork.image} alt={person.artwork.title} /> </> ); }
React에서 state mutation을 권장하지 않는 이유
  • 디버깅
    • console.log를 사용하고 state를 mutate하지 않으면 과거 기록이 최근 mutate에 지워지지 않는다.
    • 렌더링 사이에 state가 어떻게 변경되는 지 명확하게 확인할 수 있다.
  • 최적화
    • React의 일반적인 최적화 전략은 이전 프로퍼티나 state가 다음 프로퍼티나 state와 동일한 경우 작업을 건너 뛰는 것에 의존한다.
    • state를 mutate하지 않는다면 변경이 있었는지 확인하는 것이 매우 빠르다.
  • 새롭게 추가될 React의 기능들
    • 앞으로 추가될 기능들에서 state는 스냅샷처럼 취급되는 것에 의존하기 때문에, 이를 잘 사용해야 한다.
  • 요구 사항 변경에 용이
  • 더 간단하게 수정할 수 있음