Study/React

[React] React에서 useState로 배열 상태 업데이트 : 배열 데이터 수정의 함정과 해결 방법

다니니니 2024. 10. 2. 22:35
728x90

1. 서론

약 한달 전 프론트엔드 포지션으로 면접을 봤었는데 이런 문제가 나왔다.

import React, { useState } from 'react';

function App() {
  const [articles, setArticles] = useState(['남자옷추천', '신발추천', '화장품추천'])

  const changeItem = () => {
    const newArr = articles;
    newArr[0] = '여자옷추천';
    setArticles(newArr);
  }

  return (
    <div className="App">
      <ul>
        <li>{articles[0]}</li>
        <li>{articles[1]}</li>
        <li>{articles[2]}</li>
      </ul>
      <button onClick={changeItem}>클릭</button>
    </div>
  );
}

export default App;

위의 코드를 보여주고 버튼을 클릭했을 때, 예상되는 결과와 문제점이 무엇인지 문제가 있다면 어떻게 고쳐야 하는지에 대한 문제였다.

 

부끄럽게도 나는 대답을 제대로 못했다.(대답을 하긴 했는데 틀린 답변이었다.),

대답은 제대로 못했지만 결과가 어떻게 되는지 궁금해서 면접관 분들에게 답을 여쭤보았고, 다행히 친절히 설명해주셨다.

 

면접관 분들이 대답해주신 것을 바탕으로 위의 코드를 어떻게 해결해야 하는지 다뤄보도록 하겠다. 

2. 본론

우선 저 코드를 화면에 띄우면 아래와 같은 화면이 나온다.

저 코드만 보면 클릭을 했을 때, 첫번째 항목인 남자옷 추천이 여자옷 추천으로 바뀌지 않을까? 예상된다.

하지만 아무리 클릭해도 남자옷추천 항목이 여자옷추천으로 변경되지 않는다. 

왜 그럴까?

 

리액트에서 배열 변수의 State 업데이트

다시 한 번 changesItem 함수의 코드를 살펴보자

  const changeItem = () => {
    const newArr = articles;
    newArr[0] = '여자옷추천';
    setArticles(newArr);
  }

위 함수에서 newArr 라는 배열 변수에 articles 배열을 복사한다.

복사된 newArr 의 0번 인덱스의 항목을 "여자옷추천"으로 수정하고,

setArtticles(newArr) 로 상태를 업데이트한다.

 

여기서 문제는 newArr 변수에 articles 배열을 직접 복사했다는 것이다.

 

React State 의 배열은 읽기 전용으로 취급해야 한다.

즉, 위와 같이 newArr[0] =  '여자옷추천' 으로 값을 재할당하거나 push() 나 pop() 메서드와 같이

배열을 직접적으로 변형시키는 방법도 사용해서는 안된다.

(이는 공식 문서에 나와 있는 내용이다.)

 

const newArr = articles;  이 방법은 articles 배열에 대한 참조를 복사한다.

즉, newArr 와 articles는 같은 참조값(메모리상 주소)을 가지고 있다는 이야기다.

리액트에서 상태 변경의 기준은 참조값의 변경이다. 

 

const newArr = articles; 

newArr[0] = '여자옷추천'

 

따라서, 이 방식은 배열내의 값은 변했더라도 참조값이 변경되지 않았기 때문에 리액트에서 그 변경을 감지하지 못하고 리렌더링이 되지 않는 것이다.

 

해결방법

그렇다면 리액트에서 배열 변수의 값을 바꾸고 리렌더링이 되게 하려면 어떻게 해야 할까?

상태를 직접 수정하지 않고 새로운 배열을 생성해서 상태를 업데이트해야 한다.

 

공식 문서에서는 아래 표와 같이 권장하고 있다. 

  🟢
추가 push, unshift concat, [...arr] spread 연산자
삭제 pop, shift, splice filter, slice
수정 splice, arr[i] = 'value' map, spread 연산자로 복사 후 값 할당
정렬 reverse, sort spread 연산자 로 복사 후 정렬

 

따라서 서론에 있는 코드에서 changItem 함수를 수정하자면 아래의 두가지 방법으로 수정할 수 있다.

 

1. 스프레드 연산자 이용

  const changeItem = () => {
    const newArr = [...articles]; // 스프레드 연산자로 새로운 배열 생성
    newArr[0] = '여자옷추천';
    setArticles(newArr);
  }

 

2. map 메서드 이용

  const changeItem = () => {
    const newArr = articles.map((item, index) => {
      if (index === 0) return '여자옷추천';
      return item;
    }) // map 메서드 이용
    setArticles(newArr);
  }

 

위의 두가지 방법을 사용하면 값이 변경했을 때 리렌더링이 되어

버튼을 클릭했을 때 텍스트가 바뀌는 것을 확인할 수 있다.

 

3. 결론

useState 훅을 사용할 떄는 불변성을 유지해야 한다.

숫자형이나 문자열, 논리형 같은 원시 변수(Primitive Value) 는 변경 불가능한 값이라서 값을 재할당하면

이전 메모리는 무시한 채 콜 스택에 새로운 메모리 영역을 부여해서 값을 할당한다.

배열, 객체와 같은 참조 변수 (Reference Value) 은 원본 배열이나 객체에 값이 추가되거나 삭제되어도

콜 스택의 메모리는 힙 영역의 주소값이기 때문에 메모리는 변하지 않는다.

따라서 배열이나 객체의 값을 수정해서 상태 업데이트를 하고 싶으면 항상 새로운 복사본을 만드는 방법을 사용해서 상태를 변경해야 한다.

그래야 리액트에서 상태 변경을 감지하고 리렌더링을 한다.

(이것은 useState 뿐 아니라 상태를 변경하는 리덕스에서도 똑같이 적용된다.)

 

불변성을 유지하면 성능을 최적화하고, 코드의 예측 가능성을 높일 수 있다.

 

리액트의 state는 자바스크립트의 클로저를 이용해서 만들었는다는데,

사실 이것에 대해서 잘 안다고 할 수 없어서, 나중에 클로저에 대해서도 공부를 해야할 것 같다.

 

4. Reference

https://jaehan.blog/posts/react/useState-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC%EC%99%80-%ED%81%B4%EB%A1%9C%EC%A0%80

 

useState 동작 원리와 클로저 - 사툰사툰 REACT

꾸준히 성장하고 싶은 프론트엔드 엔지니어입니다. 저만의 경험과 기록을 담아두었습니다 | Error Typescript Frontend React Next.js Nginx

jaehan.blog

 

https://react.dev/learn/updating-arrays-in-state#updating-arrays-without-mutation

 

Updating Arrays in State – React

The library for web and native user interfaces

react.dev

 

https://dohi0512.github.io/useState%EC%9D%98%20%EB%B6%88%EB%B3%80%EC%84%B1/

 

https://dohi0512.github.io/useState%EC%9D%98%20%EB%B6%88%EB%B3%80%EC%84%B1/

개요 React를 사용해서 개발하다 보면 useState 를 사용하는 상황이 많다. const [state,setState] = useState(0); state = state + 1 setState(prev => prev + 1) 위와 같은 코드를 보았을 때 state 의 상태를 직접 변경하면

dohi0512.github.io

 

728x90