BABIL_PROJECT/BLE

React 와 Redux 불변성 (Immutability in React and Redux)

ForteQook 2022. 3. 31. 18:16

Immutability in React and Redux: The Complete Guide (daveceddia.com)

 

Immutability in React and Redux: The Complete Guide

Learn about side effects and how to avoid them, how to wield immutablity to update objects and arrays in Redux reducers, and the easy way to update state with Immer.

daveceddia.com

누군가가 내가 궁금한 부분을 딱 집어 포스팅 해놨다. 중요한 내용이니 번역하여 올려두려고 한다.

 

Immutability in React and Redux: The Complete Guide

불변성은 혼란스러운 주제일 수 있으며, 일반적으로 React, Redux 및 JavaScript에서 자주 나타납니다.

분명히 Props를 변경했다는 것을 알면서도, React 컴포넌트가 다시 렌더링되지 않는 버그에 부딪혔을 수 있습니다. 그리고 누군가가 "불변의 상태 업데이트를 해야 합니다."라고 말했을 수 있습니다. 여러분이나 팀원 중 한 명이 상태를 바꾸는 Redux Reduceers를 정기적으로 작성하고 있으며, 이를 지속적으로 수정해야 합니다(Reducers 혹은... 팀원을 😄).

까다롭습니다. 특히 무엇을 찾아야 할지 잘 모르겠다면 정말 미묘할 수 있습니다. 그리고 솔직히, 그게 왜 중요한지 잘 모르겠다면, 신경 쓰기가 어렵죠.

이 가이드에서는 불변의 정의와 앱에 불변의 코드를 작성하는 방법에 대해 설명합니다. 이하에 대해 설명하겠습니다.

불변성이란?

우선, 불변이란, 조작할 수 있는 가변의 반대입니다. 변환이란, 조작할 수 있는 가변성을 의미합니다.

그래서 무엇인가가 불변하다는 것은, 바꿀 수 없다는 것입니다.

극단적으로 말하면, 기존의 변수를 사용하는 대신 끊임없이 새로운 값들을 만들어내고 이전의 값을 대체하는 것입니다. JavaScript는 이렇게까지 극단적이지는 않지만, 일부 언어에서는 변환을 전혀 허용하지 않습니다(Elixir, Erlang, ML).

JavaScript는 순수하게 함수형 언어는 아니지만 때때로 기능하는 것처럼 흉내낼 수 있습니다. JS의 특정 어레이 작업은 불변합니다 (즉, 원래 어레이를 수정하는 대신 새 어레이를 반환합니다). 문자열 조작은 항상 불변합니다 (변경된 문자열로 새 문자열을 만듭니다). 그리고 불변의 함수를 직접 작성할 수도 있습니다. 몇 가지 규칙만 알아두면 됩니다.

 

변환을 사용한 코드 예시
다음으로, 가변성이 어떻게 기능하는지를 예를 제시하겠습니다. 여기서는 다음 Person 오브젝트부터 해보겠습니다.

let person = {
	firstName: "Bob",
	lastName: "Loblaw",
	address: {
		street: "123 Fake St",
		city: "Emberton",
		state: "NJ"
	}
}

Then let’s say we write a function that gives a person special powers:

function giveAwesomePowers(person) {
	person.specialPower = "invisibility";
	return person;
}

Ok so everyone gets the same power. Whatever, invisibility is great.

Let’s give some special powers to Mr. Loblaw now.

// Initially, Bob has no powers :(
console.log(person);

// Then we call our function...
let samePerson = giveAwesomePowers(person);

// Now Bob has powers!
console.log(person);
console.log(samePerson);

// He's the same person in every other respect, though.
console.log('Are they the same?', person === samePerson); // true

giveAwesomePowers는 그 안에 전달된 person을 변이시킵니다. 이 코드를 실행하면 처음 person을 출력할 때 Bob에게 specialPower 속성이 없음을 알 수 있습니다. 하지만 두 번째, 갑자기 눈에 보이지 않는 specialPower 을 갖게 되었습니다.

문제는 이 함수가 전달된 person을 수정했기 때문에 이전 함수의 모습을 알 수 없다는 것입니다. 그들은 영구적으로 변한겁니다.

give Awesome Powers에서 반환된 객체는 전달된 객체와 동일하지만 내부가 엉망입니다. 속성이 변경되었습니다. 돌연변이를 일으켰습니다.

중요한 내용이므로 다시 한 번 말씀드리지만, 오브젝트 내부는 바뀌었지만 오브젝트 참조는 바뀌지 않았습니다. 외부에서는 동일한 개체입니다 (따라서 person === samePerson 과 같은 동등성 검사가 True가 됩니다).

giveAwesomePowers 함수가 person을 수정하지 않도록 하려면 몇 가지 변경을 해야 합니다. 우선, 불변성과 밀접하게 연관되어 있는, 함수를 순수하게 만드는 것에 대해 알아보도록 합시다.

 

불변성의 법칙
순수해지려면 함수가 다음 규칙을 따라야 합니다.

1. 순수 함수는 동일한 입력이 주어진 경우 항상 동일한 값을 반환해야 합니다.
2. 순수한 기능에는 부작용(side effects)이 없어야 합니다.

 

'부작용'이 뭐죠?

"부작용"은 광범위한 용어이지만, 기본적으로 그것은 바로 그 함수 범위 밖에 있는 것들을 수정하는 것을 의미한다. 부작용의 몇 가지 예를 들어 보겠습니다.

  • Mutating/modifying input parameters, like giveAwesomePowers does
  • Modifying any other state outside the function, like global variables, or document. (anything) or window.(anything)
  • Making API calls
  • console.log()
  • Math.random()

API 호출도 해당한다는 것에 대해 놀랄 수 있겠습니다. 어쨌든 fetch('/users')와 같은 것을 호출해도 UI에는 전혀 변화가 없는 것처럼 보일 수 있으니까요.

하지만 이렇게 생각해 봅시다. fetch('/users')를 호출한 경우 변경 사항이 있습니까? UI 밖에서도?

네, 브라우저의 네트워크 로그에 엔트리 포인트를 생성합니다. 서버로의 네트워크 접속을 (또는 셧다운) 합니다. 그리고 그 호출이 서버에 전달되면 모든 결과는 예측할 수 없습니다. 서버는 다른 서비스를 호출하거나 더 많은 돌연변이를 만드는 등 원하는 모든 작업을 수행할 수 있습니다. 가능성은 적지만, 로그 파일에 변환된 엔트리가 쓰일 있을 수도 있습니다.

 

So, like I said: “side effect” is a pretty broad term. Here’s a function that has no side effects:

function add(a, b) {
  return a + b;
}

당신은 이 함수를 한 번, 몇 번, 몇 번이고 불러도 이 세상 그 어떤 것도 변하지 않습니다. 엄밀히 말하면, 함수가 작동하는 동안 "세상은" 변할 수 있겠죠. 시간이 지나면 제국은 멸망할 수도 있지만 이 기능을 호출한다고 해서 직접적으로 그런 일이 생기지는 않을 것입니다. 즉 이 함수는 규칙 2를 만족시킵니다. 부작용이 없다는 것입니다.

또한 이 함수를 add(1, 2)와 같이 호출할 때마다 동일한 답변을 얻을 수 있습니다. add(1, 2)를 몇 번 호출해도 같은 출력이 나옵니다. 이는 규칙 1, 동일한 입력 == 동일한 출력을 만족시킵니다.

 

변환되는 JS 어레이 방식
특정 어레이 메소드는 사용되는 어레이 객체를 변환합니다.

  • push (add an item to the end)
  • pop (remove an item from the end)
  • shift (remove an item from the beginning)
  • unshift (add an item to the beginning)
  • sort
  • reverse
  • splice

맞습니다, JS Array의 sort는 불변하지 않습니다! 어레이가 원래 자리에 정렬됩니다. 이러한 작업 중 하나를 사용해야 하는 경우 가장 쉬운 방법은, 어레이의 복사본을 만든 다음 해당 복사본으로 작업하는 것입니다. 다음 방법 중 하나를 사용하여 어레이를 복사할 수 있습니다.

let a = [1, 2, 3];
let copy1 = [...a];
let copy2 = a.slice();
let copy3 = a.concat();

So, if you wanted to do an immutable sort on an array, you could do it like this:

let sortedArray = [...originalArray].sort(compareFunction);

또한 sort 에 대한 간단한 설명을 하나 더 하자면, compare Function은 0, 1, 또는 -1을 반환해야 합니다 . 부울이 아닙니다! 다음에 비교기를 쓸 때는 이 점을 명심하시길 바랍니다.

순수함수는 다른 순수함수만 호출할 수 있습니다.

 

잠재적인 문제의 원인 중 하나는 순수함수에서 순수함수가 아닌 함수를 호출하는 것입니다.

순수성은 추이적, 즉 모 아니면 도 입니다. 완전히 순수한 함수를 만든다고 해도, 결과적으로 setState 혹은 dispatch를 호출하거나 기타 부작용을 일으키는 다른 함수에 대한 호출로 종료한다면... 결과는 알 수 없게됩니다.

여기, "받아들일 만한" 부작용들이 몇 가지 있습니다. console.log를 사용하여 메시지를 로깅하는건 괜찮습니다. 그러니까, 엄밀히 말하면 부작용이지만 아무 영향도 없습니다.

 

GiveAwesomePowers 순수 버전


이제 규칙을 염두에 두고 함수를 다시 작성할 수 있습니다.

function giveAwesomePowers(person) {
  let newPerson = Object.assign({}, person, {
    specialPower: 'invisibility'
  })

  return newPerson;
}

이제 좀 달라졌습니다. 우리는 그 person을 수정하는 대신 완전히 새로운 person 객체를 만들고 있습니다.

Object.assign 을 본적이 없다면, 이것이 하는 일은 오브젝트에 속성을 할당하는 겁니다. 일련의 객체를 전달하면 중복된 속성을 덮어쓰면서 왼쪽에서 오른쪽으로 모두 병합됩니다. (그리고 "왼쪽에서 오른쪽으로"라는 의미는, Object.assign (result, a, b, c)를 실행하면 a를 결과물로 복사하고, 그 다음에 b, c가 복사된다는 겁니다).

단, 완전한 Marge는 이루어지지 않습니다. 각 인수의 직계 하위 속성만 이동합니다. 또, 중요한 것은, 속성의 카피나 클론을 작성하지 않는 것입니다. 참조를 그대로 유지하면서 그대로 할당합니다.

따라서 위의 코드는 빈 객체를 만들고, 빈 객체에 모든 person 속성을 할당한 다음 해당 객체에 specialPower 속성을 할당합니다. 오브젝트 스프레드 연산자를 사용하여 작성하는 방법도 있습니다.

function giveAwesomePowers(person) {
  let newPerson = {
    ...person,
    specialPower: 'invisibility'
  }

  return newPerson;
}

"새 객체를 만들고, person의 속성을 삽입한 다음 specialPower라는 다른 속성을 추가합니다."라고 읽을 수 있습니다. 현재 이 오브젝트 확산 구문은 공식적으로 ES2018의 JavaScript 사양의 일부입니다.

 

순수함수 새 객체 반환
이제 새로운 순수 버전의 giveAwesomePowers를 사용하여 이전부터의 실험을 다시 실행할 수 있습니다.

// Initially, Bob has no powers :(
console.log(person);

// Then we call our function...
var newPerson = giveAwesomePowers(person);

// Now Bob's clone has powers!
console.log(person);
console.log(newPerson);

// The newPerson is a clone
console.log('Are they the same?', person === newPerson); // false

큰 차이는 그 person 객체가 수정되지 않았다는 것입니다. Bob은 변하지 않았습니다. 이 함수는 모든 속성이 동일하고 투명인간 능력을 가진 Bob의 클론을 만들었습니다.

이것이 바로 함수형 프로그래밍의 좀 이상한 점 입니다. 객체는 끊임없이 생성되고 파괴됩니다. Bob을 변경하는 것이 아니라 클론을 생성하여, 클론을 수정한 후 진짜 Bob을 클론으로 대체합니다. 좀 암울하죠. '프레스티지'라는 영화를 보신 분이라면, 그런 느낌이라고 할 수 있습니다. (못 보신 분들은 제가 한 말은 잊어버리세요.)

 

React에서 불변성이 중요한 이유

리액트의 경우, state나 prop들을 변형시키지 않는 것이 중요합니다. 이 규칙에서는 컴포넌트가 함수인지 클래스인지는 중요하지 않습니다. 이렇게 코드를 작성하려고 한다면; this.state.something = ... 또는 this.props.something = ... ; 한 걸음 물러서서 더 나은 방법을 생각해 보세요.

상태를 변경할때는, 항상 this.setState 을 사용하세요. 상태를 직접 수정하지 않는 이유에 대해 궁금하시다면, 아래 링크를 참고하세요!

Why Not To Modify React State Directly (daveceddia.com)

props 는 일방통행입니다. props 는 컴포넌트에 '들어가기만' 합니다. props 는 양방향이 아닙니다. 적어도, prop을 새로운 값으로 설정하는 것과 같은 mutable operation을 거치거나 하지는 않죠.

일부 데이터를 부모에게 다시 전송하거나 부모 컴포넌트에서 뭔가를 트리거해야 하는 경우, 함수를 prop 으로 전달한 다음, 부모와의 통신이 필요할 때마다 자식 내부에서 해당 함수를 호출하면 됩니다. 다음은 이 방법으로 동작하는 콜백 prop 의 간단한 예 입니다.

function Child(props) {
  // When the button is clicked,
  // it calls the function that Parent passed down.
  return (
    <button onClick={props.printMessage}>
      Click Me
    </button>
  );
}

function Parent() {
  function printMessage() {
    console.log('you clicked the button');
  }

  // Parent passes a function to Child as a prop
  // Note: it passes the function name, not the result of
  // calling it. It's printMessage, not printMessage()
  return (
    <Child onClick={printMessage} />
  );
}

 

Pure Components에서는 불변성이 중요하다


기본적으로 React 컴포넌트 (함수 유형과 클래스 유형 모두, React.Component 를 확장하는 경우)는 부모가 리렌더할 때마다 또는 setState를 사용하여 상태를 변경할 때마다 리렌더됩니다.

성능에 맞게 React 구성 요소를 최적화하는 쉬운 방법은 클래스를 만들고, React.Component가 아닌 React.PureComponent 를 확장하는 것입니다. (작성년도가 2018년이라, 아직 클래스가 많이 쓰일때 였던것 같습니다).  이렇게 하면 컴포넌트의 상태가 변경되거나 props가 변경된 경우에만 컴포넌트가 다시 렌더링됩니다. 부모가 리렌더할 때마다 무의식적으로 리렌더되는 것이 아니라 마지막 렌더링 이후 props 중 하나가 변경된 경우에만 리렌더됩니다.

여기서 불변성이 발생합니다. Pure Component에 props를 전달할 때는, 반드시 props가 immutable 하게 갱신되는지 확인 해야합니다. 즉, 오브젝트 또는 어레이일 경우 값 전체를 새로운 (변경된) 오브젝트 또는 어레이로 대체해야 합니다. Bob과 마찬가지로, 본체를 제거하고 클론으로 교체합니다.

오브젝트 또는 어레이의 내부(속성의 변경, 새로운 아이템의 푸시, 또는 어레이내의 아이템의 변경 등)를 변경하면 오브젝트 또는 어레이는 참조상 이전 것과 동일하며 Pure Component는 변경된 것을 인식하지 못하고 리렌더되지 않습니다. 이상한 렌더링 버그가 발생하겠죠.

밥과 giveAwesomePowers 함수의 첫 번째 예를 기억하십니까? 함수에 의해 반환된 객체가, 전달된 person과 정확히 같았던 것을 (===) 기억하십니까? 두 변수가 같은 객체를 참조했기 때문입니다. 내부만 바뀐겁니다.

 

JS에서의 Referential Equality

'referently equal' 하다는게 무슨 뜻이죠? 곁다리 라고는 해도, 이해하고 넘어가는게 중요합니다.

JavaScript 객체와 배열은 메모리에 저장됩니다. (지금 끄덕여야 합니다)

기억 장소가 상자 라고 칩시다. 변수 이름은 상자를 "가리키며" 상자에는 실제 값이 저장됩니다.

JavaScript 에서는, 이러한 박스(메모리 주소)에는 이름이 붙여져 있어 알 수 없습니다. 변수가 가리키는 메모리 주소를 알아낼 수 없습니다. (C와 같은 일부 다른 언어에서는 실제로 변수의 메모리 주소를 검사하여 변수가 어디에 있는지 확인할 수 있습니다).

변수를 재할당하면 새 메모리 위치를 가리킵니다.

변수의 내부를 변환했다면, 여전히 같은 주소를 가리킵니다.

집 내부를 뜯어내고 벽, 부엌, 거실 수영장 등을 새로 설치하는 것과 마찬가지로 집 주소는 그대로입니다. 당신은 여전히 같은 곳에 살고 있기 때문에, 친척들에게 생일 돈을 어디에 보내야 하는지 상기시킬 필요가 없습니다.

=== 연산자로 두 개체 또는 어레이를 비교할 때, JavaScript는 실제로 그들이 가리키는 주소, 즉 참조를 비교하는 것입니다. JS는 오브젝트를 들여다 보지도 않습니다. 참조만 비교합니다. 그것이 바로 "referential equality"의 의미입니다.

따라서 개체를 가져와서 수정하면 개체의 내용은 수정되지만 참조는 변경되지 않습니다.

또 하나의 오브젝트를 다른 오브젝트에 할당했을 때 (또는, 사실상 같은 작업을 하는 것이지만, 함수 인수로써 전달했을 경우), 다른 오브젝트는 첫 번째 오브젝트와 같은 메모리 위치에 대한 다른 포인터에 불과합니다. 부두 인형 같은거죠. 두 번째 개체에 대해 수행하는 작업은 첫 번째 개체의 값에도 직접적인 영향을 미칩니다.

다음은 이를 좀 더 구체적으로 하기 위한 몇 가지 코드 입니다.

// This creates a variable, `crayon`, that points to a box (unnamed),
// which holds the object `{ color: 'red' }`
let crayon = { color: 'red' };

// Changing a property of `crayon` does NOT change the box it points to
crayon.color = 'blue';

// Assigning an object or array to another variable merely points
// that new variable at the old variable's box in memory
let crayon2 = crayon;
console.log(crayon2 === crayon); // true. both point to the same box.

// Niw, any further changes to `crayon2` will also affect `crayon1`
crayon2.color = 'green';
console.log(crayon.color); // changed to green!
console.log(crayon2.color); // also green!

// ...because these two variables refer to the same object in memory
console.log(crayon2 === crayon);

 

왜 동치 여부를 깊이 확인하지 않는가?


동일하다고 선언하기 전에 두 개체의 내부를 서로 확인하는 것이 더 "올바른" 것처럼 보일 수 있습니다. 사실이지만, 더 느리기도 합니다.

얼마나 느릴까요? 글쎄요, 그건 비교되는 객체에 따라 다를겁니다. 1만 명의 자녀와 손자 properties 를 가진 객체는 두개의 properties를 가진 객체보다 느릴 겁니다. 예측할 수 없는거죠.

reference equality 검사는 컴퓨터 과학자들이 "상수 시간"이라고 부르는 것입니다. 상수 시간(a.k.a. O(1))은 입력의 크기에 관계없이 작업이 항상 동일한 시간이 걸린다는 것을 의미합니다.

반면 완전 equality 검사는 선형 시간(예: k.a. O(N))일 가능성이 높으며, 이는 객체에 있는 키의 수에 비례한다는 것을 의미합니다. 선형 시간은 일반적으로 일정 시간보다 느립니다.

이렇게 생각해 보십시오. JS가 a === b와 같은 두 값을 비교할 때마다 실행하는 데 1초가 걸린다고 가정해 보십시오. 자, 그럼 참고 자료 확인을 위해 한 번 해 보시겠습니까? 아니면 두 개체의 깊이로 내려가서 각각의 속성을 비교하시겠습니까? 꽤 느리게 들리죠?

실제로는 equality 점검이 1초보다 훨씬 빠르지만, 그래도 여전히, "최소한의 작업을 하라"는 원칙이 적용됩니다. 그 외의 모든 것이 같다면, 가장 퍼포먼스가 뛰어난 옵션을 사용합니다. 앱이 느린 이유를 찾는 데 걸리는 시간을 절약할 수 있습니다. 조심하면(그리고 조금 운이 따라준다면) 처음부터 느려지지 않게할 수 있겠죠 :)

const가 변경을 예방 합니까?

간단한 대답은 "아니오" 입니다. let도, const도 객체의 내부를 바꾸는 것을 막을 수는 없습니다. 변수를 선언하는 세 가지 방법 모두에서, 변수를 내부로 변환할 수 있습니다.

"하지만 그건 const라고 불려요! 상수여야 하는거 아닌가요?"

음, 그렇죠. const는 당신이 참조를 재할당하지 못하게 할 뿐입니다. 그것은 당신이 오브젝트를 바꾸는 것을 막지 못하는것 뿐 입니다. 다음은 예를 들어보겠습니다.

const order = { type: "coffee" }

// const will allow changing the order type...
order.type = "tea"; // this is fine

// const will prevent reassigning `order`
order = { type: "tea" } // this is an Error

다음에 const를 보게 되면 명심하세요.

개인적으로, 오브젝트나 어레이가 (대부분의 경우) 변이되어서는 안 된다는 것을 상기시키기 위해 const를 즐겨 사용합니다. 어레이 또는 오브젝트를 변환하는 것이 확실한 코드를 작성할 경우 let을 사용하여 선언합니다. 하지만 그것은 단지 관례일 뿐입니다. (그리고 대부분의 관례처럼, 때때로 그것을 깬다면, 그것은 관례가 전혀 없는 것과 같습니다.)

Redux에서 상태를 갱신하는 방법

Redux에서는, 리듀서가 순수한 함수여야 합니다. 즉, 상태를 직접 변경할 수 없다는 겁니다.상기 Bob에서와 마찬가지로 이전 상태를 기반으로 새로운 상태를 작성해야 합니다. (리듀서가 무엇인지, 그 이름이 어디서 유래했는지 잘 모를 경우 읽어보십시오.)

불변 상태 갱신을 실행하기 위한 코드 작성은 까다로울 수 있습니다. 아래에는 몇 가지 일반적인 패턴이 있습니다.

브라우저 개발자 콘솔이든 실제 앱이든 직접 사용해 보십시오. 중첩된 오브젝트 업데이트에 특히 주의를 기울이고, 이러한 업데이트를 연습하십시오. 나는 그것들이 가장 까다롭다고 생각합니다.

이 모든 것은 실제로 React 상태에도 적용되므로, 이 가이드에서 학습한 내용은 Redux 사용 여부에 관계없이 적용됩니다.

마지막에는 Immer라는 라이브러리를 쉽게 사용할 수 있는 방법에 대해 알아보겠습니다. 하지만 끝까지 건너뛰지는 마십시오. 기존의 코드 베이스로 작업하는 경우는, 이 「long hand」를 사용하는 방법을 이해하는 것이 매우 유용할 겁니다.

 

확산 연산자 "..."


이러한 예에서는 어레이 및 객체에 대해 확산 연산자를 많이 사용합니다. 작동 방식은 이렇습니다.

이 "..." 표기는 객체 또는 배열 앞에 배치되며, 그 안에 있는 자식의 압축을 풀고 작성된 곳에 삽입합니다.

// For arrays:
let nums = [1, 2, 3];
let newNums = [...nums]; // => [1, 2, 3]
nums === newNums // => false! it's a new array

// For objects:
let person = {
  name: "Liz",
  age: 32
}
let newPerson = {...person};
person === newPerson // => false! it's a new object

// Internal properties are left alone:
let company = {
  name: "Foo Corp",
  people: [
    {name: "Joe"},
    {name: "Alice"}
  ]
}
let newCompany = {...company};
newCompany === company // => false! not the same object
newCompany.people === company.people // => true!

위와 같이, 확산 연산자를 사용하여 다른 객체와 동일한 내용을 포함하는 새 객체 또는 배열을 쉽게 만들 수 있습니다. 이것은 오브젝트/어레이의 복사본을 작성한 후 변경하고 싶은 특정 속성을 덮어쓸 때 유용합니다.

let liz = {
  name: "Liz",
  age: 32,
  location: {
    city: "Portland",
    state: "Oregon"
  },
  pets: [
    {type: "cat", name: "Redux"}
  ]
}

// Make Liz one year older, while leaving everything
// else the same:
let olderLiz = {
  ...liz,
  age: 33
}

The spread operator for objects is part of standard JavaScript as of ES2018.

 

상태 업데이트 방법


이러한 예는 Redux reducer에서 상태를 반환하는 컨텍스트로 작성됩니다. 들어오는 상태가 어떻게 보이는지, 업데이트된 상태를 어떻게 반환하는지 보여 드리겠습니다.

예시를 깨끗하게 유지하기 위해, "action" 매개변수는 완전히 무시하겠습니다. 모든 액션에 대해 이 상태 갱신이 이루어진다고 가정합니다. 물론 독자분들의 리듀서에서는 각 액션에 대한 케이스가 포함된 switch 구문이 있을 것입니다만, 이 경우 설명이 복잡해질 것이라 생각합니다.

 

리액트에서 상태 업데이트 하기


이 예제를 일반적인 리액트 상태에 적용하고 싶으시면, 이 예제의 몇 가지 사항을 조정하면 됩니다.

React는 당신이 this.setState() 전달한 오브젝트를 얕은 병합 하기 때문에, Redux 에서처럼 기존 상태에 대해 스프레드 연산자를 사용할 필요 없습니다.

Redux Reducer에서는 다음과 같이 써야합니다.

return {
  ...state,
  (updates here)
}

With plain React state, you can write it like this, without the spread operator:

this.setState({
  updates here
})

단, setState는 얕은 병합을 수행하므로, 상태 내에서 깊이 중첩된 항목(첫 번째 수준보다 깊은 항목)을 업데이트할 때 객체(또는 어레이) 확산 연산자를 사용해야 합니다.

 

Redux: 객체 업데이트 하기


Redux 상태 객체의 최상위 속성을 업데이트하려면 기존 상태를 "...상태" 와 같이 복사한 다음 변경할 속성을 새 값과 함께 나열합니다.

function reducer(state, action) {
  /*
    State looks like:

    state = {
      clicks: 0,
      count: 0
    }
  */

  return {
    ...state,
    clicks: state.clicks + 1,
    count: state.count - 1
  }
}

 

Redux: 객체 내의 객체 업데이트


업데이트할 객체가 Redux 상태 내에서 한개 수준 (혹은 여러개)  인 경우, 업데이트할 개체를 포함하여 모든 수준에 대해 복사본을 만들어야 합니다. 다음은 한 레벨에 대한 예를 제시하겠습니다.

function reducer(state, action) {
  /*
    State looks like:

    state = {
      house: {
        name: "Ravenclaw",
        points: 17
      }
    }
  */

  // Two points for Ravenclaw
  return {
    ...state, // copy the state (level 0)
    house: {
      ...state.house, // copy the nested object (level 1)
      points: state.house.points + 2
    }
  }

Here’s another example, this time updating an object that’s two levels deep:

function reducer(state, action) {
  /*
    State looks like:

    state = {
      school: {
        name: "Hogwarts",
        house: {
          name: "Ravenclaw",
          points: 17
        }
      }
    }
  */

  // Two points for Ravenclaw
  return {
    ...state, // copy the state (level 0)
    school: {
      ...state.school, // copy level 1
      house: {         // replace state.school.house...
        ...state.school.house, // copy existing house properties
        points: state.school.house.points + 2  // change a property
      }
    }
  }

이 코드는 중첩된 항목을 업데이트 할 때 읽기 어려울 수 있겠죠!

 

Redux: 키를 통한 개체 업데이트

function reducer(state, action) {
  /*
    State looks like:

    const state = {
      houses: {
        gryffindor: {
          points: 15
        },
        ravenclaw: {
          points: 18
        },
        hufflepuff: {
          points: 7
        },
        slytherin: {
          points: 5
        }
      }
    }
  */

  // Add 3 points to Ravenclaw,
  // when the name is stored in a variable
  const key = "ravenclaw";
  return {
    ...state, // copy state
    houses: {
      ...state.houses, // copy houses
      [key]: {  // update one specific house (using Computed Property syntax)
        ...state.houses[key],  // copy that specific house's properties
        points: state.houses[key].points + 3   // update its `points` property
      }
    }
  }

 

Redux: 어레이에 항목 추가 하기 (앞에)

mutable 한 방법으로는, Array의 .unshift 함수를 사용하여 앞에 항목을 추가합니다. 다만, Array.protype.unshift는 어레이를 변환합니다. 이러한 조작은 하고 싶지 않습니다.

다음은 Redux에 적합한 항목을 어레이의 선두에 immutable 하게 추가하는 방법입니다.

function reducer(state, action) {
  /*
    State looks like:

    state = [1, 2, 3];
  */

  const newItem = 0;
  return [    // a new array
    newItem,  // add the new item first
    ...state  // then explode the old state at the end
  ];

 

Redux: 어레이에 항목 추가 (뒤에)

이를 위해서는 Array의 .push 함수를 사용하여 항목을 끝에 추가합니다. 하지만 그렇게 되면 어레이가 변이되겠죠.

다음은 항목을 배열 끝에 불변하게 추가할 수 있는 방법입니다.

function reducer(state, action) {
  /*
    State looks like:

    state = [1, 2, 3];
  */

  const newItem = 0;
  return [    // a new array
    ...state, // explode the old state first
    newItem   // then add the new item at the end
  ];

You can also make a copy of the array with .slice, and then mutate the copy:

function reducer(state, action) {
  const newItem = 0;
  const newState = state.slice();

  newState.push(newItem);
  return newState;

 

Redex: Map 을 사용하여 어레이 항목 업데이트 하기

Array의 .map 함수는 사용자가 제공하는 함수를 호출하여 기존 각 항목을 전달하고 반환 값을 새 항목의 값으로 사용하여 새 배열을 반환합니다.

즉, N개의 항목이 있는 배열이 있고 N개의 항목이 있는 새 배열이 필요한 경우 .map을 사용합니다. 어레이를 한 번 통과하여 하나 이상의 항목을 업데이트/교체할 수 있습니다.

(N개의 항목이 있는 배열에서 더 적은 항목이 필요한 경우 .filter를 사용합니다. 아래 "배열에서 항목 제거" 를 참조하십시오.)

function reducer(state, action) {
  /*
    State looks like:

    state = [1, 2, "X", 4];
  */

  return state.map((item, index) => {
    // Replace "X" with 3
    // alternatively: you could look for a specific index
    if(item === "X") {
      return 3;
    }

    // Leave every other item unchanged
    return item;
  });
}

 

Redx: 어레이 내에서 객체 갱신 하기

이것은 위와 같은 방법으로 동작합니다. 유일한 차이점은, 여기서는 새 객체를 구성하고 변경할 객체의 복사본을 반환해야 한다는 것입니다.

Array의 .map 함수는 사용자가 제공하는 함수를 호출하여 기존 각 항목을 전달하고 반환 값을 새 항목의 값으로 사용하여 새 배열을 반환합니다.

즉, N개의 항목이 있는 배열이 있고 N개의 항목이 있는 새 배열이 필요한 경우 .map을 사용합니다. 어레이를 한 번 통과하여 하나 이상의 항목을 업데이트/교체할 수 있습니다.

(N개의 항목이 있는 배열에서 더 적은 항목이 필요한 경우 .filter를 사용합니다. 배열에서 항목 제거를 참조하십시오.

이 예에서는 이메일 주소를 가진 사용자가 배열되어 있습니다. 그들 중 한 명이 이메일을 바꿔서 업데이트해야 합니다. 액션의 일부로서 유저의 ID와 새로운 이메일이 어떻게 입력되는지 보여드리겠습니다만, 다른 곳(예를 들면, Redux 를 사용하지 않는 경우)의 값을 받아들이도록 조정할 수 있습니다.

function reducer(state, action) {
  /*
    State looks like:

    state = [
      {
        id: 1,
        email: 'jen@reynholmindustries.com'
      },
      {
        id: 2,
        email: 'peter@initech.com'
      }
    ]

    Action contains the new info:

    action = {
      type: "UPDATE_EMAIL"
      payload: {
        userId: 2,  // Peter's ID
        newEmail: 'peter@construction.co'
      }
    }
  */

  return state.map((item, index) => {
    // Find the item with the matching id
    if(item.id === action.payload.userId) {
      // Return a new object
      return {
        ...item,  // copy the existing item
        email: action.payload.newEmail  // replace the email addr
      }
    }

    // Leave every other item unchanged
    return item;
  });
}

 

Redux: 어레이 중간에 항목 삽입 하기

배열의 .splice 함수는 항목을 삽입하지만, 배열도 역시 변환합니다.

원본을 변환하고 싶지 않기 때문에, 먼저 .slice를 사용하여 복사본을 만든 다음 .splice를 사용하여 항목을 복사본에 삽입할 수 있습니다.

다른 방법으로는, 새 요소 이전에 모든 요소를 복사해 넣고, 새 요소를 삽입한 다음, 모든 요소를 복사하는 방법이 있습니다. 하지만 index를 틀리기 쉽겠죠.

프로 팁: 이 경우 유닛테스트를 해보시길 바랍니다! off-by-one 에러를 저지르기 쉽습니다.

function reducer(state, action) {
  /*
    State looks like:

    state = [1, 2, 3, 5, 6];
  */

  const newItem = 4;

  // make a copy
  const newState = state.slice();

  // insert the new item at index 3
  newState.splice(3, 0, newItem)

  return newState;

  /*
  // You can also do it this way:

  return [                // make a new array
    ...state.slice(0, 3), // copy the first 3 items unchanged
    newItem,              // insert the new item
    ...state.slice(3)     // copy the rest, starting at index 3
  ];
  */
}

 

Redux: 인덱스 별로 배열 항목 업데이트 하기

Array의 .map을 사용하여 특정 인덱스에 대한 새 값을 반환하고 다른 요소는 변경하지 않고 그대로 둘 수 있습니다.

function reducer(state, action) {
  /*
    State looks like:

    state = [1, 2, "X", 4];
  */

  return state.map((item, index) => {
    // Replace the item at index 2
    if(index === 2) {
      return 3;
    }

    // Leave every other item unchanged
    return item;
  });
}

 

Redux: 필터가 있는 어레이에서 항목 삭제 하기

Array의 .filter 함수는 사용자가 제공한 함수를 호출하여, 기존 각 항목을 전달하고, 함수가 "true"(또는 truthy)를 반환한 항목만 포함하는 새 배열을 반환합니다. false를 반환하면 해당 항목이 제거됩니다.

N개의 항목이 포함된 배열이 있고 더 적은 항목으로 끝나려면 .filter를 사용합니다.

function reducer(state, action) {
  /*
    State looks like:

    state = [1, 2, "X", 4];
  */

  return state.filter((item, index) => {
    // Remove item "X"
    // alternatively: you could look for a specific index
    if(item === "X") {
      return false;
    }

    // Every other item stays
    return true;
  });
}

Redux 문서의 이 불변 업데이트 패턴 섹션에서 기타 유용한 방법을 확인하십시오.

Immutable Update Patterns | Redux

 

Immutable Update Patterns | Redux

Structuring Reducers > Immutable Update Patterns: How to correctly update state immutably, with examples of common mistakes

redux.js.org

 

Immer를 통한 간단한 상태 업데이트

만약 당신이 위의 불변의 상태 업데이트 코드를 보고 비명을 지르며 도망가고 싶다면, 비난하지 않겠습니다.

깊이 중첩된 개체 업데이트는 읽기 어렵고 쓰기 어려우며 올바르게 수행하기가 어렵습니다. 유닛 테스트는 필수적이지만, 그것조차도 코드를 읽고 쓰는 것을 쉽게 만들지 못합니다.

다행히, 도움이 될 만한 라이브러리가 있습니다. Michael Weststrate의 Immer를 사용하면 당신이 잘 알고 좋아하는 모든 가변 코드들, [].push, [].pop 그리고 = 로 작성할 수 있고, Immer는 이 코드를 사용하여 마법과 같은 완벽한 불변의 업데이트를 생성합니다.

 

immerjs/immer: Create the next immutable state by mutating the current one (github.com)

 

GitHub - immerjs/immer: Create the next immutable state by mutating the current one

Create the next immutable state by mutating the current one - GitHub - immerjs/immer: Create the next immutable state by mutating the current one

github.com

'BABIL_PROJECT > BLE' 카테고리의 다른 글

BLE  (0) 2022.06.18
BLE_WRITE  (0) 2022.04.26
BLE_SCAN 액션 수정 (main/index.js)  (0) 2022.04.11
BLE_SCAN 액션 수정 (babilScan.js)  (0) 2022.04.10
BLE-PLX  (0) 2022.01.29