블로그 이름 뭐로 하지

[React] React 배열 state에 원소 추가할 땐 concat or push? 본문

프론트엔드

[React] React 배열 state에 원소 추가할 땐 concat or push?

발등이 따뜻한 사람 2024. 2. 15. 20:56

약 1년 전 진행했던 T.time 프로젝트의 채팅창 쪽 코드를 보다가 array형태의 state에 원소를 추가할때 push를 쓰지 않고 concat을 사용하여 원소가 추가된 새로운 배열을 하나 만들고, 그 배열로 set해주는 것을 발견했다.

const [chat, setChat] = useState<(string | StaticImageData)[]>([]);

useEffect(() => {
    setTimeout(() => {
      if (textCount < CHAT_QUESTION_LIST[questionIndex].questions.length) {
        if (scrollRef.current !== null) {
          scrollRef.current.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'nearest' });
        }
        const newlist = chat.concat(CHAT_QUESTION_LIST[questionIndex].questions[textCount]);
        setChat(newlist);
        setTextCount(textCount + 1);
      }
    }, 700);
  }, [chat, questionIndex]);

 

위 코드에서 9번째 줄을 보면 concat을 사용하고 있다.

T.time프로젝트의 전체 코드를 보고 싶다면 밑에 링크로! (1년 전 짰던 코드이기에 분명 부족한 점이 있을거라고 생각합니다. 이슈 파서 리포트해주시면 기쁘고 감사한 마음으로 보완하도록 할게요!)

https://github.com/Antititi-time/T.TIME_CLIENT

 

GitHub - Antititi-time/T.TIME_CLIENT: 나와 팀이 함께 성장하는 시간, T.time

나와 팀이 함께 성장하는 시간, T.time. Contribute to Antititi-time/T.TIME_CLIENT development by creating an account on GitHub.

github.com

 

이 때 내가 왜 push를 안 썼을까? 하고 구글에 이것저것 검색해보니, 그 당시엔 알고있던 것들을 지금은 많이 까먹은 듯 하여 충격을 받고.. 이번엔 정말 장기기억으로 가져가고자 이 글을 작성하게 됐다.

 

push와 concat

일반적으로 두 메소드 모두 배열에 원소를 추가해서 저장한다는 목적을 가지고 사용된다. 그렇다면 무엇이 다를까?

push

const arr = [1,2];
arr.push(3);
console.log(arr)
//[1,2,3]

push는 리턴값이 새롭게 생성된 배열 안의 요소 개수이며, 원본 배열 자체가 변한다.

concat

const arr = [1,2];
const result = arr.concat(3);
console.log(arr)
//[1,2]
console.log(result)
//[1,2,3]

concat은 기존의 배열은 그대로 유지한 채, 새로운 배열을 만들어 반환해준다.

 

자, 이제 두 메소드의 차이를 알겠으니(아마 이건 모두가 알 거라고 생각한다.) 왜 state 배열에 요소를 추가할 때 concat을 사용해야 하는지 알아보자!

 

state와 불변성

불변성이란 어떠한 값을 직접적으로 건드려서 변경하지 않고, 새로운 값을 만들어낸 후, 그 값을 할당하는 방식으로 변경하는 것이다.

 

특히나 자바스크립트에서 객체 타입의 불변성을 지키는 것은 고려해야할 부분이 존재한다.

객체타입의 데이터를 어떠한 변수에 할당하고 그 변수를 다른 변수에 다시 할당했다면, 배열의 복사가 이루어지는 것이 아니라 같은 참조값을 갖게 된다. 그래서 만약 사본을 수정하면, 원본도 함께 수정이 된다.

 

엥? 이게 무슨 말이지? 싶다면 아래를 차근차근 보도록 하자.

 

javascript와 메모리 구조

JS 엔진은 call stack과 heap memory 2가지 메모리 공간을 가진다.

call stack: 실행 중인 함수를 추적해 계산을 수행하고 지역변수를 저장하는 공간. 이곳에 원시 타입들이 저장된다

heap memory: 참조 타입들이 할당되는 공간. 메모리 누수를 방지하기 위해 JS엔진의 메모리 관리자가 항상 관리하는 공간이다.

 

* 원시 타입: boolean, string, number, null, undefined, symbol

* 참조 타입: Object, Array

 

여기서 원시 타입같은 겨우, 값 할당시 call stack의 값에 해당 값이 그대로 저장된다.

반면에 참조 타입의 경우, 실제 값은 memory heap에 저장되고, 메모리 힙의 주소가 call stack의 값에 저장 된다.

 

JS에서의 변수 할당과 재할당

원시 타입

원시타입의 변수는 변수값이 변경되면 기존 콜스택의 메모리 영역의 값을 직접적으로 변경하지 않고, 새로운 메모리 영역에 변경된 변수값을 저장한다.

더 이상 참조되지 않는 데이터는 가비지 컬렉터에 의해 적절한 시점에 메모리에서 해제된다.

 

참조 타입

참조타입은 기존의 변수를 바꾸는 경우와 map, spread operator 등 기존의 변수를 변경한 새로운 변수를 반환하는 경우의 두 가지로 나누어서 봐야 한다.

 

우선 기존의 변수를 직접 변경하는 경우를 살펴보면, 변수 값이 변경되면 call stack의 변화는 없으며, memory heap의 value값만 변경된다. 즉, 기존의 메모리 영역의 값이 변경되므로 불변성 유지가 되지 않는다.

기존의 변수를 변경한 새로운 변수를 반환하는 경우에는 새로운 메모리 영역이 생성되어 불변성이 유지된다.

 

[!!여기서!!]

push를 사용하여 state를 직접적으로 변경하는 경우, call stack의 변화없이 memory heap의 값만 변경된다는 점,

concat을 사용하여 요소를 추가한 배열을 만들고 state에 set해주는 경우, 새로운 메모리 영역이 생성된다는 점을 알 수 있다.

 

불변성을 지켜야 하는 이유

불변성의 진짜 의미는 메모리 영역에서 값을 변경할 수 없다는 의미이다.

 

1) 리액트에서의 state 변화 감지 기준은 "콜스택의 주소값"이다.

리액트는 콜스택의 주소값만을 비교하여 상태 변화를 감지한다. 이를 "얕은 비교"라고 한다. 리액트의 빠른 state 변화 감지를 할 수 있도록 해주는 장점이자, 불변성을 지켜야 하는 이유이다.

 

참조 타입은 콜스택에 메모리 힙의 주소값만 저장하고, 실제 값은 메모리 힙의 value에 저장해두기 때문에, 참조 타입의 값을 직접 변경하면 콜스택의 주소값은 변경이 없이 똑같은 주소를 가리키고 있어서 react는 state의 변경이 없다고 판단하게 되고, 변경된 state는 재렌더링되지 않는다. 

 

때문에 T.time에서도 concat이 아닌 push를 사용했다면 react가 변화를 감지하지 못하고 계속해서 처음의 채팅창만 보여줬을 것이다.

 

2) 사이드 이펙트와 복잡한 코드를 방지할 수 있다.

기존 메모리 영역의 값을 사용하는 다른 코드에서 발생할 수 있는 오류를 사전에 방지 할 수 있으며 , 예기치 못한 오류를 해결할 코드를 추가적으로 만들지 않아도 된다는 이점이 있다.

 

 

결론!!

따라서! React에서 참조 타입을 변경할 때에는 concat을 통해 새로운 배열을 생성하거나, spread operator를 적극적으로 활용해야 한다.

그래야 call stack의 참조값(메모리 힙 주소값)이 변경되고, state변경을 정상적으로 감지할 수 있으니까!

 

 

 

<참고>

https://velog.io/@itssweetrain/TIL45React-push-vs-concat

https://velog.io/@badahertz52/%EC%B0%B8%EC%A1%B0%ED%83%80%EC%9E%85%EA%B3%BC-React%EC%9D%98-%EB%B6%88%EB%B3%80%EC%84%B1

'프론트엔드' 카테고리의 다른 글

[JavaScript] 디바운싱(Debouncing)  (1) 2024.01.28