JavaScript Object Immutability

blair·2020년 6월 12일
2

JavaScript

목록 보기
2/4
post-thumbnail

문제발생

  • 영화정보를 제공하는 리액트 프로젝트를 진행하던 중 서버에서 받은 데이터를 state에 저장하고 리랜더링을 하기 위해 state내의 값을 변경했더니 해당 값을 사용하고 있는 모든 곳에서 side effect가 발생하여 버그가 유발된 적이 있다.

원인

  • 트러블 슈팅을 해보니, React는 setState 메소드를 통해서 리랜더링을 하는데 setState는 기존의 state값과 새롭게 생성된 객체값을 비교하며 최적화를 하기 때문에 기존 state값에 불변성을 유지해줘야 했던 것 이었다.
  • JS에서 원시 타입이라면 불변의 성질을 갖고있지만 그를 제외한 모든 타입, 즉 객체는 참조타입이라 mutable하고 React state는 객체형태로 관리하기 때문에 불변성을 부여해줘야 setState메소드가 실행될 때 리랜더링이 side effect없이 될 수 있다.

해결 및 느낀점

  • 불변성을 관리해주기 위해 setState메소드에 기존 state에 스프레트 연산자를 사용해서 shallow copy를 하며 해결했고, 해결한 방법 외에도 객체의 불변성 관리를 위해서 다른 어떤 방법들이 있는지, js의 객체에는 왜 불변성을 줘야하는지 등 에 대해서 추가적으로 공부하고 코드에 적용시켜보면서 많은것을 배웠다.

관련해서 더 공부할 것들

  • React의 상태관리에서 왜 불변성을 지켜야 할까?
  • React의 상태관리에서 불변성을 지키지 않으면 어떻게 될까?
  • JS의 Object에 불변성을 주는 방법들은 어떤 것 들이 있을까?
  • JS의 object는 왜 mutable할까?(immutable value와 비교하며 생각하기)
  • 배열만의 불변성을 지켜주는 고차원 함수들의 종류와 각각 차이에 대해서 설명할 수 있는가?

추가공부

JavaScript immutable value vs. mutable value

immutable value

let a = 'first' 
// 메모리 공간에 'first' 가 생성되고 식별자 a는 'first'의 메모리 주소를 가리킨다.
let b = a 
// 식별자 b도 식별자 a가 가리키는 'first'를 가리킨다.

a = 'second' 
// 메모리 공간에 'second'가 생성되고 'first'를 가리키고있던 식별자 a는 이제 'second'를 가리킨다.

console.log(a) 
//'second'     가리키는 메모리 공간을 변경했던 대로 'second'가 찍힌다.
console.log(b) 
//'first'     여전히 b는 'first'를 가리키고있을 뿐이다
  • 원시타입(불변하기 때문에 수정이 안되고 새로운 메모리 공간에 새로운 값이 생길 뿐이다.)
    : 스트링 b에 스트링a를 할당했을 때 a의 참조를 할당하는 것이 아니라 immutable한 값 'fitst'가 메모리에 새로 생성되고 b는 이 값을 참조하게 된다.
  • 그렇기 때문에 a가 메모리에 새로 생긴 다른 값을참조 하게더라도 변수 b의 참조값은 바뀌지 않는 것 이다.

mutable value

let a = {name:'first'}
let b = a

a.name = 'second'
console.log(a) // {name: "second"}
console.log(b) // {name: "second"}

// 번외
let c = {number : 1}
a = c
console.log(a) // {number: 1}
console.log(b) // {name: "second"}
  • 참조타입(변할 수 있기 때문에 수정 가능하다.)
    :객체 b에 객체 a를 할당했을 때 a의 참조를 할당하게 되므로 a가 바라보는 객체를 b또한 바라보게 되는 된다.
  • 또한 객체는 mutable한 값이므로 a.name가 변경되면 변경하지도 않은 객체 b.name도 동시에 변경된다. 정확히는 b.name도 변경되는 것이아니라 객체 a와 객체 b가 같은 어드레스를 참조하고 있기 때문에 변경된 객체의 프로퍼티가 찍힐 뿐 이다.

왜 리액트 상태관리에서 객체의 불변성을 유지해줘야 할까?

결론적으로는 불변성을 지키지 않으면 우리는 컴포넌트를 최적화 할 수 없기 때문이다.

  • 불변성을 유지하는 이유는 state or store값에 변화를 주고 반영시려면 setState나 dispatch를 사용해서 re-rendering을 하기 위함이다.
  • 더 자세히 말하자면 setState나 dispatch 메소드들이 re-rendering 하는 방식은 이전state와 비교해서 감지된 변화가 있을 경우에 re-rendering을 하게 되는 것 이므로 비교할 대상인 이전의 state값에는 변화를 주면 안되는 것 이다.
    (TMI : 불필요한 재렌더링을 피하기 위해 shouldComponentUpdate, useCallback(react-hooks)를 씀.)

예를들어보기 위해
리액트가 컴포넌트를 렌더링하는 과정을 살펴보자.
1.setState를 호출 (혹은 부모로부터 props를 전달 받음)
2.shouldComponentUpdate를 실행했는데 false를 리턴하면 여기서 멈추고, true를 리턴하면 다음 단계로 이동
3.가상 DOM과 실제 DOM을 비교해서 변경사항이 있으면 화면을 다시 그린다
핵심은 2번에 있다. 다음과 같은 2개의 상태를 비교한다고 가정해보자.

const array = [1,2,3,4];
const sameArray = array;
sameArray.push(5);
>
console.log(array !== sameArray); // false
const array = [1,2,3,4];
const differentArray = [...array, 5];
console.log(array !== differentArray); // true

첫 번째 코드의 array와 sameArray변수가 참조하고 있는 배열의 주소 값은 서로 같다. 하지만, 두 번째 코드의 각각의 배열은 다른 레퍼런스를 가지기 때문에 비교했을 때 다르다는 결과 값이 나오게 된다.
이와 같이 불변성을 유지하여 코드를 작성하면 각 객체의 값이 아닌 레퍼런스 값만 비교를 해주면 된다.
즉, shouldComponentUpdate내의 코드는 한 줄이면 컴포넌트를 최적화할 수 있게 된다.

JavaScript Object에 불변성을 주는 방법들

  • 방법1 : 표준함수 사용

    • es7 : spread, 배열이라면 concat,map,filter등 기존 배열을 건드리지 않고 새로운 배열을 리턴하는 배열의 고차원 함수도 사용할 수 있다.
    • es6 : Object.assign과 Object.freeze를 이용해서 불변성 부여가 가능합니다.
  • 방법 2: immutability-helper를 사용

    immutability-helper는 react 공식 문서에서 추천하는 패키지다. 방법 1과는 달리 중첩된 object를 다루기가 쉽다.

  • 방법 3: javascript 라이브러리 immutable-js, immer사용
    사용 방법 참고

표준함수 Object.assign & Object.freeze

  • JavaScript Object에 불변성을 주기 위한 방법중 표준함수를 사용한 방법에 대해서 자세히 공부해보았다.

Object.assign() (shallow copy)

  • 기존의 객체를 복사해서 한개 더 만들어 낸 다음에 그 복사된 객체를 변경하는 방법
  • 첫번째 인자에 빈 객체 넣어주고 두번째 인자에 내가 복사하고 싶은 객체를 넣는다.
    그럼 첫번째 인자 객체에 두번째 인자인 내가 복사하고 싶은 객체를 복사해서 새로운 객체를 리턴한다.
    하지만 이렇게 한다고 하더라도 shallow copy 이기때문에 바깥 객체는 카피하는대 객체 안에 객체 카피못함 즉 객체안의 객체는 주소값을 그대로 동일하게 가리키고 있는것이다.
const a = { name: "모모",
           b : {nick : "YJ"}
          }

const c = Object.assign({}, a) // 복사

c.name = '콩이' // 복사한 b객체의 name만 변경시도
c.name         //'콩이' 변경됨
a.name         // '모모' 본래 객체의 name은 변경되지 않음 

c.b.nick = "김버섯" // // 복사한 c객체의 프로퍼티 b객체의 nick 변경시도
c.b.nick          //"김버섯" 변경됨
a.b.nick          //"김버섯" 본래 객체의 프로퍼티도 변경되버림..

// 내부객체는 복사가 안되서 그대로 본래 객체의 주소값을 참조하고 있기 때문에 
// 즉 c객체의 b 프로퍼티와 a객체의 b프로퍼티는 같은 주소값을 참고하고있다.

Object.freeze() (shallow copy)

  • 객체자체에 불변성을 부여하는 것임
    하지만 이 방법 역시도 shallow copy이기 때문에 내부의 객체는 변할 수 있다.(불변이아니다)
    내부까지 다 불변하게 만들려면 안의 객체까지 다 파고들어 각각에 Object.freeze()먹여줘야 한다.

const a = { name: "모모",
            b : {nick : "YJ"}
          }

Object.freeze(a)    // 불변성 부여

a.name = "콩이"      // 변경시도
a.name              // "모모" 변하지 않음(불변함)

a.b.nick = "블레어"   // 내부객체에 변경시도
a.b.nick            // "블레어" 변경됨

Object.freeze(a.b) // 내부객체도 불변성 부어
a.b.nick = "김버섯"   // 내부객체에 변경시도
a.b.nick            // "블레어" 변하지 않음(불변함)
  • 하지만 위 두 메소드를 사용해서 불변객체를 만드는 방법은 번거러울 뿐더러 성능상 이슈가 있어서 큰 객체에서는 사용하지 않는것이 좋다. ⇒ immutable.js 를 사용해보도록

참고자료

https://poiemaweb.com/js-immutability
https://yuddomack.tistory.com/entry/자바스크립트-변수-파라미터와-메모리-참조
https://velog.io/@kimtaeeeny/
https://medium.com/@ljs0705/
https://github.com/BKJang/dev-tips/issues/42

profile
개발공부를 하며 나누고 성장하고 싶습니다 :) 피드백은 항상 환영합니다.

0개의 댓글