• ✏️ React의 불변성

    2024. 3. 12.

    by. 서카츄

     

    React의 불변성이란?

    값이나 상태를 변경할 수 없는 것. = 메모리에 있는 값을 변경할 수 있는 것!

    리액트에서 불변성을 지켜주는 이유는 리액트가 상태 업데이트를 하는 원리 때문이다.

    리액트는 상태값을 업데이트 할 때 `얕은 비교`를 수행한다.

    즉, 배열이나 객체의 속성을 하나하나 비교하는 것이 아니라, 이전 참조값과 현재 참조값만을 비교하여 상태 변화를 감지한다.

    이런 이유로 배열이나 객체를 업데이트 할때 `setState([...state, newState])` 이런식으로

    새로운 참조값을 가진 배열이나 객체를 생성한다.

    바로 이렇게 `불변성` 을 지킴으로써 리액트는 상태변화를 감지할 수 있다.

     

    리액트는 배열이나 객체를 업데이트 할 때, 새로운 참조값을 가진 배열이나 객체를 생성해 상태변화를 감지한다.
    새로운 참조값을 가진 배열이나 객체를 생성하므로 원본 데이터의 참조값에 영향을 주지 않기에 불변성을 지킨다는 것이다.

     

     

     

    원시데이터 : 숫자, 문자, 불리언

    원시데이터가 아닌것들 : 배열, 객체, 함수 🔥불변성이 없다!

    React는 화면을 랜더링 할지를 state변화에 따라 감지하고, 단순 변수는 무시한다.

     

    즉, 같은 메모리 참조를 안한다.

    원시데이터 아닌것 = 같은 메모리 참조를 하기 때문에 state에서 바로 출력해서 랜더링 돼서 볼 수 있다.

    즉, 불변성을 지켜주는 것이 중요하다.

     

     

     

    원시타입 중 하나인 string으로 예를 들어보자

    let string = 'data1'
    string = 'data2'

     

    변수 string은 data1 → data2로 값이 변경 되었다.
    위 예시를 보면 string 변수는 'data1' → 'data2' 로 값이 변경된 것처럼 보이지만
    실제 메모리영역에는 'data1', 'data2' 둘다 존재한다.

     

     

     

     

    let string = 'data1' // string: 'data1'가 메모리 영역1에 등록
    string = 'data2' // string: 'data2'가 메모리 영역2에 등록

     

    위 예시에서 메모리 영역을 총 2개 사용한 것
    변수 string은 'data1' 였고, 여기에 'data2'를 재할당하였는데 

    기존 메모리 영역 1에 있는 'data1'의 값은 그대로 두고, 메모리 영역2에 'data2'를 새로 할당했다.
    즉, 메모리영역에서 'data2'는 'data1'을 대체하는 것이 아니라 새로운 영역에 할당된다.


     

     

     

    let a = 10;
    let b = a;
    a = 20;
    console.log(a,b); //20 10

     

    자바스크립트에서 Number값은 불변성을 유지하기 때문에 

    새롭게 20이라는 값을 가지는 주소를 a에 할당하게 되기 때문에 위와 같은 결과가 나오게 된다.
    다른 원시타입들도 마찬가지이고, 이것을 `불변성`이라고 한다.

     

     

     

     

    Object 타입

    let array = [1, 2, 3, 4] // 메모리영역 1
    array.push(5) // 메모리영역 1
    array = [1, 2, 3, 4] // 메모리영역 2 (새로운 참조값)

     

    array.push(5)는 원본데이터를 수정함으로써 불변성을 지켜주지 않은 것이 되고,
    array = [1, 2, 3, 4] 는 새로운 배열 [1, 2, 3, 4]을 할당하고 

    새로운 참조값을 만들어 주어 불변성을 지켜준 것이 된다.

     

     

     

     

     

    var coke = { name: 'coca', price: 2980, }
    var new_coke = coke;
    coke.name = 'pepsi';
    console.log(coke.name,new_coke.name);//'pepsi' 'pepsi'

     

    원본 메모리영역은 그대로이고, 그 안에 값을 변경한 것으로

    이렇게 결과들이 출력된다 (불변성x)
    불변성의 진짜 의미는 메모리 영역에서 값을 변경할 수 없다는 의미이다.

     

     

     

    React의 불변성

    React에서 불변성을 지키는 이유는 리액트에서 상태 업데이트를 하는 원리 때문이다.
    리액트는 상태값을 업데이트 할 때 얕은 비교를 수행한다.

    즉, 객체의 속성 하나하나를 비교하는게 아니라 `참조값`만 비교하여 상태 변화를 감지한다. 

    따라서 Object or Array의 속성이나 값을 바꾸는 불변성이 없는 변경은 

    상태가 바뀌었다고 React에서 파악을 못하기 때문에 이런 불변성을 지키는 업데이트가 필요하다. 

    그러므로 배열이나 객체를 새로 생성해서 새로운 참조값을 만들어서 상태를 업데이트 한다.

    이렇게 불변성을 지켜줌으로써 또 다른 이점은 바로 사이드 이펙트를 방지하는 것!

    즉 외부에 존재하는 원본데이터를 직접 수정하지 않고,

    원본데이터의 복사본을 만들어서 값을 사용하기에 예상치 못한 오류를 사전에 방지할 수 있다.

    반대로 생각해보면 외부의 값을 함부로 변경할 수 있는 것은 위험한 일이다.

    만약 다른 어떤 곳에서 원본데이터를 사용하고 있다고 하면

    어플리케이션 어딘가에서 사이드 이펙트가 일어날 가능성이 있기 때문이다.

     

     

     

    불변성을 안지킨 React

    React 에서는 얕은 비교를 통해 새로운 값인지 아닌지를 판단 한 후, 

    만약 새로운 값임을 판단하게 되면?

    부모 컴포넌트가 리렌더링을 하면 

    자식 컴포넌트도 함께 리렌더링 되게 된다.

    만약 어떠한 컴포넌트를 리렌더링 하고 싶을 때 배열 타입인 state가 있다고 가정해본다.
    state.push(10)을 통해서 배열에 직접 10이라는 값을 추가한다.
    React에서는 해당 state라는 값은 새로운 참조값으로 바뀐것이 아니기 때문에 

    push 이전의 state와 push 이후의 state가 같다고 판단하여 리렌더링을 하지 않게 된다.

     

     


    객체의 경우

    const myObj = { a: 1 }
    myObj.a = 2

     

    'myObj'의 내부 프로퍼티의 값을 변경한다 가정했을 때
    myObj에서 참조하고 있는 'a'값은 변경되지 않고 값이 변경된다. 

    단, 'a'가 참조하고 있는 '1'의 주소 값은 변경된다.


    이러한 이유 때문에 state 값을 변경하고 React에게 리렌더링을 원한다고 알리고 싶다면

    새로운 배열을 생성해서 새로운 참조값을 생성하고 그 안에 기존의 값을 넣어줘야 한다.

     

     

     

     

    불변하게 만드는 법

    spread operator, map, filter, slice, reduce 등등 새로운 배열을 반환하는 메소드들을 활용하면 된다.
    → splice는 원본데이터를 변경한다.


    setState를 이용할 때 원시타입 경우에는 값을 바로 넣어주어도 되지만
    참조타입인 경우에는 새로운 객체나 배열을 생성한 후 값을 넣어주어야 한다.

    주로 자바스크립트에서 객체를 불변하게 만드는 방법은 크게 3가지 정도가 있다.

    assign 메서드, spread 연산자 사용, 라이브러리 사용이다.

     

     

     

     

    assign

    Object.assign 메서드를 사용할 수 있다.

    Object.assign({}, myObj, {
    b: 2
    })

     

    Object.assign은 매개 변수로 전달된 객체의 모든 속성을 첫 번째 매개 변수에 지정된 객체에 복사한다.

     

     

     

     

    spread

    var coke = {
    name: 'coca',
    price: 2980,
    }
    var new_coke = {...coke};
    coke.name = 'pepsi';
    console.log(coke.name,new_coke.name);//'pepsi' 'coca'

     

    스프레드 문법을 사용하여 객체를 복사해야지 객체가 불변성을 유지할 수 있다.
    하지만 스프레드 문법은 1레벨 깊이에서만 유효하게 동작하기 때문에 

    객체 내부의 객체의 불변성까지는 유지할 수 없다.

     

     

     

    2레벨

    var coke = { name: 'coca', fake: { name: 'pepsi', } }
    var new_coke = {...coke};
    coke.fake.name = 'coca zero';
    console.log(coke.fake.name,new_coke.fake.name); //'coca zero' 'coca zero'

     

    레벨2 객체까지 불변성을 유지해주려면 아래와 같이 별도의 변수에 값을 재할당하고 넣어주는 번거러운 과정을 거쳐야 한다.

    const coke = { name: 'coca', fake: { name: 'pepsi', } }
    const new_fake = {...coke.fake};
    const new_coke = {...coke};
    new_coke.fake = new_fake;
    coke.fake.name = 'coca zero';
    console.log(coke.fake.name,new_coke.fake.name);
    //'coca zero' 'pepsi'

     

     


    론.

    객체는 같은 주소값을 참조해서 바라보기 때문에 콘솔로그에는 변경된 값을 찍어보면 나오지만,

    랜더링 되는 곳은 그대로 변했다는 내용이 안보인다.

    그래서 다른 객체를 생성해주는 것이다. (다른 주소로 바꾼다)

     

     

     

     

     

    댓글