Published on

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

Authors
  • avatar
    Name
    junyeol kim

Updating Objects in State

  • State는 객체를 포함한 모든 종류의 자바스크립트 값을 가질 수 있다.

  • React state를 가진 객체를 직접 변경해서는 안되고, 업데이트 하고 싶을 때는 새로운 객체 or 기존 객체의 복사본을 생성하여 state가 복사본을 사용하도록 해야한다.

What's a mutation?

// 원시 타입 VS 참조 타입

// 원시 타입
let a = 10
let b = a
b = 20
console.log(a) // 10

// 참조 타입
let obj1 = { name: 'A' }
let obj2 = obj1
obj2.name = 'B'
console.log(obj1.name) // "B"
  • 원시 타입은 불변성(값을 변경할 수 없다)을 가진다.
    • 원시타입이란? JS에서 가장 기본이 되는 데이터 타입으로, Number, String, Boolean, Undefined, Null, Symbol, BigInt가 있다.

    • 원시 타입의 특징으로는 값 자체가 복사되며, 독립적이며, 불변성을 띠고있다.

const [x, setX] = useState(0)
setX(5) // 0을 5로 교체!
const [position, setPosition] = useState({ x: 0, y: 0 })
position.x = 5
  • 객체는 기술적으로 변경(mutation)이 가능하다.

  • 하지만 React에서는 하면 안된다. React는 객체의 참조(주소)를 비교하여 변경을 감지하는데, 직접 변경하면 참조가 같아서 변경을 감지하지 못하기 때문이다.

  • 따라서 React state의 객체는 불변성을 가진 것처럼 다뤄야 한다.

Treat state as read-only

// 잘못된 예시
const [position, setPosition] = useState({ x: 0, y: 0 })

function handleMove(e) {
  position.x = e.clientX
  position.y = e.clientY
}
  • 객체를 직접 변경하기에 React가 감지를 못한다.

  • 리렌더링이 일어나지않아, 화면이 업데이트가 되지 않는다.

// 올바른 예시
const [position, setPosition] = useState({ x: 0, y: 0 })

function handleMove(e) {
  setPosition({
    x: e.clientX,
    y: e.clientY,
  })
}
  • 새로운 객체를 생성해서 전달을 한다.

  • 그러므로 React가 변경을 감지해 리렌더링 한 후, 화면이 정상적으로 업데이트 된다.

Copying objects with the spread syntax

const [person, setPerson] = useState({
  firstName: 'Barbara',
  lastName: 'Hepworth',
  email: 'bhepworth@sculpture.com',
})
  • 위 객체 상황에서 일부 필드만 업데이트하고 나머지는 유지하고 싶을때 사용하는 방법이 있다.
// 번거로운 방법
setPerson({
  firstName: person.firstName,
  lastName: person.lastName,
  email: 'newemail@sculpture.com', // 이것만 변경
})
// spread 구문 사용
setPerson({
  ...person, // 기존 필드 복사
  email: 'newemail@sculpture.com', // 이것만 덮어쓰기
})
  • spread 구문의 장점은 간결하고, 유지보수가 용이하며, 실수를 방지할 수 있다.
// 여러 필드를 업데이트 하고 싶을 때
// 핸들러를 여러개 사용하는 상황은 생략
function handleChange(e) {
  setPerson({
    ...person,
    [e.target.name]: e.target.value  // 동적 속성명
  });
}

<input name="firstName" onChange={handleChange} />
<input name="lastName" onChange={handleChange} />
<input name="email" onChange={handleChange} />

Updating a nested object

const [person, setPerson] = useState({
  name: 'Niki de Saint Phalle',
  artwork: {
    title: 'Blue Nana',
    city: 'Hamburg',
    image: 'https://...',
  },
})
  • 중첩 객체 구조에서도 동일하게 spread 구문을 이용하여 객체를 변경을 해야한다.
setPerson({
  ...person, // 최상위 복사
  artwork: {
    ...person.artwork, // artwork도 복사
    city: 'New Delhi', // city만 변경
  },
})
  • 만약 더 깊은 중첩 구조를 변경해야한다면, 위와 같은 방식이 아니라 Immer을 사용하는 것을 권장한다.

Write concise update logic with Immer

  • state가 깊이 중첩되어있다면, flattening를 고려해야한다.

  • state 구조를 바꾸지않고, 중첩 전개를 할 수 있는 간편한 방법으로는 Immer 라이브러리가 있다.

  • Immer을 사용하면 코드의 법칙을 무시하고 객체를 변경하는 것 처럼 보일 수 있다.

import { useImmer } from 'use-immer'

const [person, updatePerson] = useImmer({
  name: 'Niki de Saint Phalle',
  artwork: {
    title: 'Blue Nana',
    city: 'Hamburg',
  },
})
// 일반적인 useState 방식
setPerson({
  ...person,
  artwork: {
    ...person.artwork,
    city: 'New Delhi',
  },
})

// Immer 방식
updatePerson((draft) => {
  draft.artwork.city = 'New Delhi'
  // 직접 변경하는 것 처럼 보임
})
  • draftProxy 특성을 가진 특별한 객체이다.

  • draft가 변경되는 순간을 감지하여, Immer가 자동으로 새로운 객체를 생성한다.

  • 코드는 직접 변경하는 것처럼 보이지만, 내부적으로는 불변성이 유지된다.

  • 그렇기에 원본 객체는 절대 변경되지 않으며, 변경된 부분만 새로 만들어 반환한다.

Updating Arrays in State

  • 배열은 JavaScript에서 가변적이지만, state에 저장할 때는 불변성을 유지해야 한다.

  • 객체와 마찬가지로, 배열을 업데이트하려면 새 배열을 생성하거나 기존 배열의 복사본을 만들어야 한다.

Updating arrays without mutation

  • JavaScript에서 배열은 객체의 한 종류이므로 배열을 직접 변경하면 안 된다.
  • arr[0] = 'bird' 같은 직접 할당을 피해야 한다.
  • push(), pop() 같은 변경 메서드를 피해야 한다.

배열 조작 방법 비교:

동작비권장 (배열 변경)권장 (새 배열 반환)
추가push, unshiftconcat, [...arr]
제거pop, shift, splicefilter, slice
교체splice, arr[i] = ...map
정렬reverse, sort배열 복사 후 정렬

Adding to an array

const [artists, setArtists] = useState([])

// 잘못된 방법
// push는 원본 배열을 변경
artists.push({ id: nextId++, name: name })

// 올바른 방법

// 배열 끝에 추가
setArtists([...artists, { id: nextId++, name: name }])

// 배열 앞에 추가
setArtists([{ id: nextId++, name: name }, ...artists])

Removing from an array

const [artists, setArtists] = useState([
  { id: 0, name: 'Sarah' },
  { id: 1, name: 'Ben' },
  { id: 2, name: 'Clara' },
])

// id가 1이 아닌 항목만 남김
setArtists(artists.filter((a) => a.id !== 1))

filter 메서드:

  • 조건을 만족하는 항목만 포함하는 새 배열 생성
  • 원본 배열은 변경하지 않음

Transforming an array

const [shapes, setShapes] = useState([
  { id: 0, type: 'circle', x: 50, y: 100 },
  { id: 1, type: 'square', x: 150, y: 100 },
  { id: 2, type: 'circle', x: 250, y: 100 },
])

// 모든 원을 아래로 50px 이동
const nextShapes = shapes.map((shape) => {
  if (shape.type === 'circle') {
    return { ...shape, y: shape.y + 50 } // 새 객체 생성
  } else {
    return shape // 변경 없으면 그대로 반환
  }
})

setShapes(nextShapes)

map 메서드:

  • 배열의 일부 또는 모든 항목을 변경하고 싶을 때
  • 각 항목을 변환한 새 배열 생성

Replacing items in an array

const [counters, setCounters] = useState([0, 0, 0])

// 인덱스 1의 값을 증가
const nextCounters = counters.map((c, i) => {
  if (i === 1) {
    return c + 1 // 새 값 반환
  } else {
    return c // 기존 값 유지
  }
})

setCounters(nextCounters)
  • 삼항 연산자를 사용하면 더 간결하게 작성할 수 있다: counters.map((c, i) => i === 1 ? c + 1 : c)

Inserting into an array

const [artists, setArtists] = useState([
  { id: 0, name: 'Sarah' },
  { id: 1, name: 'Ben' },
  { id: 2, name: 'Clara' },
])

// 인덱스 1에 새 항목 삽입
const insertAt = 1
const nextArtists = [
  ...artists.slice(0, insertAt), // 앞부분 복사
  { id: nextId++, name: name }, // 새 항목
  ...artists.slice(insertAt), // 뒷부분 복사
]

setArtists(nextArtists)

slice 메서드 :

  • 배열의 일부를 복사하여 새 배열 생성

  • slice(start, end): start부터 end 이전까지 복사

  • 원본 배열은 변경하지 않음

const arr = [1, 2, 3, 4, 5]
arr.slice(0, 2) // [1, 2]
arr.slice(2) // [3, 4, 5]

Making other changes to an array

const [list, setList] = useState([3, 1, 2])

// 잘못된 방법 - reverse()와 sort()는 원본 변경
list.reverse()
list.sort()

// 올바른 방법 - 먼저 복사, 그다음 변경
function handleClick() {
  const nextList = [...list] // 배열 복사
  nextList.reverse() // 복사본 변경
  setList(nextList)
}

// 더 간단한 방법
function handleClick() {
  setList([...list].reverse())
}

Updating objects inside arrays

const [myList, setMyList] = useState([
  { id: 0, title: 'Big Bellies', seen: false },
  { id: 1, title: 'Lunar Landscape', seen: false },
  { id: 2, title: 'Terracotta Army', seen: true },
])

// 잘못된 방법 - 중첩 객체를 직접 변경
myList[0].seen = true

// 올바른 방법 - map으로 새 배열, spread로 새 객체
function handleToggle(artworkId, nextSeen) {
  setMyList(
    myList.map((artwork) => {
      if (artwork.id === artworkId) {
        return { ...artwork, seen: nextSeen } // 새 객체 생성
      } else {
        return artwork // 변경 없으면 그대로
      }
    })
  )
}

배열과 객체의 관계:

  • 객체는 실제로 배열 "내부"에 위치하지 않는다.

  • 배열은 각 객체의 참조(메모리 주소) 를 저장한다.

  • 따라서 배열을 복사해도 내부 객체는 같은 객체를 가리킨다.

// 메모리 관점에서 이해
const obj1 = { name: 'A' } // 메모리 주소: 0x001
const obj2 = { name: 'B' } // 메모리 주소: 0x002

const arr = [obj1, obj2] // [0x001, 0x002] 참조만 저장

// 배열을 복사해도
const newArr = [...arr] // [0x001, 0x002] 같은 참조 복사!
newArr[0] === arr[0] // true (같은 객체!)

// 따라서 내부 객체도 새로 만들어야 함
const newArr = arr.map((obj) => ({ ...obj })) // 새 객체들로 구성된 새 배열

Write concise update logic with Immer

import { useImmer } from 'use-immer'

const [myList, updateMyList] = useImmer([
  { id: 0, title: 'Big Bellies', seen: false },
  { id: 1, title: 'Lunar Landscape', seen: false },
])
// useState VS useImmer

// useState
setMyList(
  myList.map((artwork) => {
    if (artwork.id === artworkId) {
      return { ...artwork, seen: nextSeen }
    } else {
      return artwork
    }
  })
)

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

Immer로 배열 조작:

// 추가
updateMyList((draft) => {
  draft.push({ id: nextId++, name: name })
})

// 제거
updateMyList((draft) => {
  const index = draft.findIndex((a) => a.id === artworkId)
  draft.splice(index, 1)
})

// 삽입
updateMyList((draft) => {
  draft.splice(insertAt, 0, { id: nextId++, name: name })
})

// 정렬
updateMyList((draft) => {
  draft.sort((a, b) => a.name.localeCompare(b.name))
})

Immer의 장점 (배열):

  • push, pop, splice 같은 변경 메서드를 자유롭게 사용 가능

  • 복잡한 중첩 구조에서 특히 유용

  • 코드가 직관적이고 간결함