[Redux] Reducer 추가 내용

mokyoungg·2020년 10월 7일
1

Redux

목록 보기
4/7

출처는 공식 문서와 Udemy의 Modern React with Redux [2020 Update] 강의 입니다.
공식 문서 : https://ko.redux.js.org/
Udemy 강의 : https://www.udemy.com/

reducer는 redux의 기초 개념이다.
reducer에는 state(상태)값이 저장되고 action을 통해 변한다.
State v1 + action#1 >> Reducer >> State v2
이러한 reducer를 작성하는데는 몇가지 규칙이 있다.



1. Rules of Reducers

udemy 강의에서 나온 리듀서의 기본 규칙은 다음과 같다.(영어다)

  • Must return any value besides 'undefined'
  • Produces 'state', or data to be used inside of your app using only previous state and the action(reducers are pure)
  • Must not return reach 'out of itself' to decide what value to return
  • Must not mutate its input 'state' argument

1-1. reducer의 state값으로 undefined를 넘겨줄 수 없다.(1번 규칙)

reducer 호출시, 작동 방식

처음 리듀서가 호출 될 때
undefined + Acion #1 => Readucer => State v1

다음 리듀서가 호출 될 때
State v1 + Action #2 => Reducer => State v2

  • redux가 처음 부팅되면 리듀서를 한 번 호출한다.
  • reducer는 이전 상태(state v1)와 action을 받아서 다음 상태(state v2)를 반환하는 순수 함수.
  • 이때 reducer의 state 값이 undefined면 에러 메시지가 뜬다.
  • undefined와 action의 조합이 reducer로 이동하면 제대로 된 처리가 안 되는 것 같다.(?)
  • 따라서 최초의 state 값에 아무것도 주지 않으려면 undefined 대신 null을 사용해야 한다.
  • state의 기본값을 결정하기 위해선 작업하는 reducer의 목적에 달려있다.

예시 코드(/src/reducers/index.js)

export default (state = null, action) => {
  if(actin.type === 'action#1'){
    return action.payload
  return state
}

공식 페이지 설명(출처 : https://ko.redux.js.org/api/combinereducers#notes)j
combineReducers로 전달되는 모든 리듀서는 아래의 규칙을 따라야 한다.

  • 식별되지 않은 모든 상태에 대해서는 첫 인수로 주어진 state를 그대로 반환해야 한다.
  • undefined 를 반환해서는 안 된다. 이는 return 문에서 쉽게 할 수 있는 실수로, 다른 곳에서 에러가 나기 전에 combineReducers에서 에러를 발생(throw)시킨다.
  • state가 undefined로 주어지면 반드시 해당 리듀서의 초기 상태를 반환해야 한다. 예전 룰을 따르면 초기 상태 또한 undefined가 될 수 없다. ES6의 선택적 인수(optional arguments) 문법을 사용하면 간편하지만, 직접 첫 인수가 undefined가 아닌지 확인 할 수도 있다.

1-2. Pure Reducer (2번-3번 규칙)

  • Reducer는 순수 함수로, (이전)state와 action 객체만 보도록 되어있다.
  • 따라서, API 요청, DOM 접근하는 등의 방식은 안 된다.

예시 코드(/src/reducers/index.js)

export default (state = null, action) => {
  // Bad
    return document.querySelector('input')
  // Bad
    return axios.get('/post')
}

공식 페이지 설명 (출처 : https://ko.redux.js.org/basics/reducers)
Reducer가 반드시 순수해야 한다는 점만 기억해두자.
인수가 주어지면, 다음 상태를 계산해서 반환하면 된다.예기치 못한 일은 없어야 한다.
사이드 이펙트도 없어야 한다. API 호출도 안된다. 변경도 안된다. 계산만 가능하다.

1-3. reducer의 state 인자를 변경해서는 안 된다.

강의에선 'mutate'라는 단어를 사용했는데 나는 state의 모습을 변경(변형)해서는 안 된다고 해석하였다.
그렇다면 변경(변형)된다는건 무슨 말인가?
변경(변형)은 문자열이나 숫자가 아닌 배열이나 객체에서 일어난다.
최초로 할당한 배열(객체)의 값을 변경하는 것을 'mutate'라고 생각하면 될 것 같다.

mutate의 예시 코드(배열과 객체)

배열(colors) 최초의 모습
const colors = ['red', 'green']

//배열 변형
1) 배열의 요소 추가
colors.push('blue') // colors = ['red', 'green', 'blue']
2) 배열의 요소 삭제
colors.pop() // colors = ['red']
3)배열의 요소 변경
colors[0] = 'pink' // colors = ['pink', 'green']
객체(profile) 최초의 모습
cosnt profile = { name : 'jordan' }

// 객체 변형
1) 객체의 요소 추가
profile.number = 23 // profile = { name: 'jordan', number: 23 }
2) 객체의 요소 삭제
delete profile.name // profile = undefinded
3) 객체의 요소 변경
profile.name = 'sam' // profile = { name: 'sam' }

공식 페이지 설명
(출처 : https://ko.redux.js.org/recipes/structuring-reducers/prerequisite-concepts#immutable-data-management)

리듀서가 순수해야 한다는 의미는...

  • 사이드이펙트(API 호출, 지역객체, 지역변수가 아닌 것을 수정하는 등)가 생기면 안 됨.
  • 순수하지 않은 함수(Date.now 혹은 Math.random와 같은)를 호출하면 안 됨.
  • 인수를 변경하면 안 된다. 리듀서가 상태를 변화시킬 때 이미 존재하는 상태 객체를 수정하면 안 된다.
    그 대신 변화에 필요한 새로운 객체를 만들어야 한다. 리듀서가 업데이트하는 상태내의 모든 객체에 동일한 접근이 필요하다.

불변성, 사이드이펙트, 변화에 대한 주의

변경은 시간 단위 디버깅과 리덕스의 connect함수를 방해하기 때문에 권장되지 않는다.

  • 시간 단위로 디버깅할 때 리덕스 DevTools는 기록 된 액션들에게 상태 값만 만드는 것을 기대한다.
    변화나 비동기 액션같은 사이드 이펙트는 시간단위 디버깅에서 각 단계 간의 동작을 변화시켜 애플리케이션을 중단시킨다.
  • react-redux에서 connect는 컴포넌트가 업데이트되어야 하는지 확인하기 위해 mapStateToProps 함수로 부터 반환된 props를 확인한다. 성능의 향상을 위해서 connect는 불변성에 기인한 더 쉬운 방법을 취하며 객체가 동일한지 확인하기 위해 얕은 참조 검사를 사용한다. 즉, 직접 객체를 변화시킨 것은 감지하지 못하며, 다시 렌더링 되지 않는다.

그 외 리듀서에서 유니크한 ID나 타임스탬프와 같은 것들을 생성하는 것은 코드를 예측할 수 없게 하고 디버그, 테스트를 어렵게 한다.

정리히자면 배열이나 객체와 같은 참조형 데이터의 경우,
내부의 데이터가 바뀌는 것은 새로운 객체가 만들어지는 것이 아니라 기존의 객체 내부의 값이 바뀌는 것이다.
react-redux는 성능을 위해 얇은 참조 검사를 하는데 이때 참조형 데이터는 내용은 바뀌었으나 주솟값(?)은 일치하기 때문에 변화가 된 것을 감지하지 않는다. 그래서 다시 렌더링 되지 않는다. 그래서 배열(객체)을 변화시키는게 아니라 새로운 배열(객체)을 만들고 새로운 값을 추가해야 한다.

참고: https://velog.io/@mokyoungg/JS-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%83%80%EC%9E%85%EC%BD%94%EC%96%B4-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8


2. Reducer 내부의 배열과 객체를 업데이트하는 방법.

reducer 내부의 배열과 객체를 수정할 땐 ES6에서 등장한 Spread구문(전개구문)을 비롯한 여러 메서드가 사용된다.

Spread 구문이란?
전개 구문을 사용하면 배열이나 문자열과 같이 반복 가능한 문자를 0개 이상의인수(함수로 호출할 경우) 또는 요소(배열 리터럴의 경우)로 확장하여, 0개 이상의 키-값의 쌍으로 객체로 확장시킬 수 있다.
출처 : https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/Spread_syntax

filter() 메서드
주어진 함수의 테스트를 통과하는 모든 요소를 모아 새로운 배열로 반환한다.
출처 : https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/filter

map() 메서드
배열 내의 모든 요소 각각에 대하여 주어진 함수를 호출한 결괄르 모아 새로운 배열을 반환한다.
출처 : https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/map

lodash 라이브러리
객체 및 배열의 작업을 위해 사용되는 매우 인기있는 라이브러리(라고 한다.)
출처 : https://lodash.com/docs/

설명을 보면 무슨말인지 모르겠다. 예시를 보면 이해가 간다.


2-1. 배열의 요소 업데이트

배열(colors) 최초의 모습
const colors = ['red', 'green']

1) 배열의 요소 추가
colors.push('blue') // colors = ['red', 'green', 'blue'] //Bad(기존 배열의 요소 변경)
[...colors, 'blue'] // colors = ['red', 'green'] // Good(기존 배열의 요소 변경 없음) 

2) 배열의 요소 삭제
colors.pop() // colors = ['red'] // Bad
colors.filter(element => element != 'green') // Good

3)배열의 요소 변경
colors[0] = 'pink' // colors = ['pink', 'green'] // Bad
colors.map((el) => el === 'red' ? 'pink' : el) // Good

2-2. 객체의 요소 업데이트

객체(profile) 최초의 모습
cosnt profile = { name : 'jordan', age : 30 }

1) 객체의 요소 추가
profile.number = 23 // profile = { name: 'jordan', number: 23 } // Bad
{ ...state, number:23 } // Good

2) 객체의 요소 삭제
delete profile.name // profile = undefinded // Bad
{ ...state, age: undefined } // Good
_.omit(state, 'age) // Good, lodash 라이브러리를 사용하는 방법.

3) 객체의 요소 변경
profile.name = 'sam' // profile = { name: 'sam' } // Bad
{ ...state, name : 'Sam' } // Good

3. Reducer의 switch 구문

Reducer는 action의 type에 따라 그 action의 payload를 반환하거나 기존의 state를 반환한다.

const exampleReducer = (state = null, action ) => {
// action의 타입을 평가한다.
  if (action.type === 'ACTION_NUM1') {
    return action.payload
  }
  return state
}

이렇게 코드를 작성해도 작동되지만 reducer를 작성할 때는 주로 switch 구문을 사용한다고 한다.

switch 구문이란?
스위치 구문은 표현식의 값을 case에 따라 평가하고, 그 case와 일치하는 경우 표현식을 실행한다.(?)
(switch는 reducer에서만 사용한 특별한 구문이 아니라 자바스크립트의 구문이다.)

The switch statement evaluates an expression, matching the expression's value to a case clause, and executes statements associated with that case, as well as statements in cases that follow the matching case.

출처 : https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/switch

swtich 코드 예시(reducer)

const exampleReducer = (state = null, action) => {
  //action의 type을 평가한다.
  switch (action.type) {
    //action의 type이 'ACTION_NUM1' 인가?
    case 'ACTION_NUM1':
      // 참(true)이면 해당 액션의 payload를 반환
      return action.payload;
    case 'ACTION_NUM2':
      return action.payload;
    case 'ACION_NUM3':
      return acttion.payload;
    // 해당하는 action이 없는가?
    default:
      // 참(true)이면 기존의 state 값 반환
      return state;
  }
}

if와 else if 구문과 같다. 그러나 코드를 작성하기엔 더 편하다.


4. dummy reducer 만들기

처음 어플리케이션을 제작할 때 그러니까 (큰 그림을 그렸지만) reducer의 정확한 목적을 정하지 않았을 때,
일단 redux를 사용하기 위해서 기초 세팅을 하고 싶지만 reducer에 무엇을 넣어야 할 지 모를 때,
등등 아무튼 이럴 때 dummy reducer를 만들어 놓고 시작하면 작업하면서 에러를 보는 일이 없다.
(redux를 사용할때 reducer가 비어있으면 에러가 뜬다.)

유효한 reducer인 척을 하는 reducer 만들기

export default combineReducers({
  dummy: () => 'hi there'
})


reducer를 배우고 이해한 부분을 작성.
틀린 내용은 당연히 있으니 더 공부하자.

profile
생경하다.

0개의 댓글