Redux를 사용한 JavaScript의 불변성

게시 됨: 2022-03-11

풍부하고 복잡한 JavaScript 응용 프로그램의 계속 성장하는 생태계에서 현재 사용자, 로드된 게시물 목록 등 이전보다 더 많은 상태를 관리해야 합니다.

이벤트 기록이 필요한 모든 데이터 세트는 상태 저장으로 간주될 수 있습니다. 상태를 관리하는 것은 힘들고 오류가 발생하기 쉬울 수 있지만, 이 기사의 목적을 위해 변경할 수 없는 데이터와 특정 지원 기술(예: Redux)로 작업하는 것은 상당한 도움이 될 수 있습니다.

불변 데이터에는 제한 사항이 있습니다. 즉, 일단 생성되면 변경할 수 없지만 특히 참조 대 값 평등에서 많은 이점이 있으므로 자주 데이터 비교에 의존하는 응용 프로그램의 속도를 크게 높일 수 있습니다(업데이트가 필요한지 확인 , 예를 들어).

불변 상태를 사용하면 데이터에 대한 재귀 비교를 수행할 필요 없이 상태가 변경되었는지 빠르게 알 수 있는 코드를 작성할 수 있습니다. 이는 일반적으로 훨씬 더 빠릅니다.

이 기사에서는 액션 생성기, 순수 함수, 합성된 리듀서, Redux-saga 및 Redux Thunk를 통한 불순한 액션, 마지막으로 React와 함께 Redux 사용을 통해 상태를 관리할 때 Redux의 실제 적용을 다룰 것입니다. 즉, MobX, Relay 및 Flux 기반 라이브러리와 같이 Redux에 대한 많은 대안이 있습니다.

왜 Redux인가?

Redux를 MobX, Relay 및 대부분의 다른 Flux 기반 구현과 같은 대부분의 다른 상태 컨테이너와 구분하는 주요 측면은 Redux가 "액션"(일반 JavaScript 개체)을 통해서만 수정할 수 있는 단일 상태를 가지고 있다는 것입니다. 리덕스 스토어. 대부분의 다른 데이터 저장소에는 React 구성 요소 자체에 포함된 상태가 있으므로 여러 저장소를 가지거나 변경 가능한 상태를 사용할 수 있습니다.

이렇게 하면 변경 불가능한 데이터에 대해 작동하는 순수 함수인 저장소의 리듀서가 실행되고 잠재적으로 상태가 업데이트됩니다. 이 프로세스는 더 이해하기 쉽고 더 결정적인 단방향 데이터 흐름을 적용합니다.

Redux 흐름.

Redux 리듀서는 불변 데이터에서 작동하는 순수 함수이기 때문에 동일한 입력이 주어지면 항상 동일한 출력을 생성하므로 테스트하기 쉽습니다. 다음은 감속기의 예입니다.

 import Immutable from 'seamless-immutable' const initialState = Immutable([]) // create immutable array via seamless-immutable /** * a reducer takes a state (the current state) and an action object (a plain JavaScript object that was dispatched via dispatch(..) and potentially returns a new state. */ function addUserReducer(state = initialState, action) { if (action.type === 'USERS_ADD') { return state.concat(action.payload) } return state // note that a reducer MUST return a value } // somewhere else... store.dispatch({ type: 'USERS_ADD', payload: user }) // dispatch an action that causes the reducer to execute and add the user

순수 함수를 처리하면 Redux가 다음과 같이 일반적으로 변경 상태로 쉽게 수행되지 않는 많은 사용 사례를 쉽게 지원할 수 있습니다.

  • 시간 여행(시간을 이전 상태로 되돌리기)
  • 로깅(매장에서 돌연변이를 일으킨 원인을 파악하기 위해 모든 단일 작업을 추적)
  • 공동 작업 환경(예: 작업이 일반 JavaScript 개체이고 직렬화되고 유선을 통해 전송되고 다른 시스템에서 재생될 수 있는 GoogleDocs)
  • 간편한 버그 보고(전달된 작업 목록을 보내고 정확히 동일한 상태를 얻기 위해 재생)
  • 렌더링 최적화
  • 순수 기능을 쉽게 단위 테스트할 수 있으므로 감속기를 쉽게 테스트하십시오.

액션 크리에이터

Redux의 액션 생성자는 코드를 깨끗하고 테스트 가능하게 유지하는 데 도움이 됩니다. Redux의 "액션"은 발생해야 하는 돌연변이를 설명하는 일반 JavaScript 객체에 불과하다는 것을 기억하십시오. 즉, 동일한 객체를 계속해서 작성하는 것은 반복적이고 오류가 발생하기 쉽습니다.

Redux의 액션 생성자는 단순히 돌연변이를 설명하는 일반 JavaScript 객체를 반환하는 도우미 함수입니다. 이렇게 하면 반복적인 코드를 줄이고 모든 작업을 한 곳에 보관할 수 있습니다.

 export function usersFetched(users) { return { type: 'USERS_FETCHED', payload: users, } } export function usersFetchFailed(err) { return { type: 'USERS_FETCH_FAILED', payload: err, } } // reducer somewhere else... const initialState = Immutable([]) // create immutable array via seamless-immutable /** * a reducer takes a state (the current state) and an action object (a plain JavaScript object that was dispatched via dispatch(..) and potentially returns a new state. */ function usersFetchedReducer(state = initialState, action) { if (action.type === 'USERS_FETCHED') { return Immutable(action.payload) } return state // note that a reducer MUST return a value }

불변 라이브러리와 함께 Redux 사용하기

리듀서와 액션의 특성으로 인해 테스트하기가 쉽지만, 불변성 헬퍼 라이브러리 없이는 객체를 변경하는 것으로부터 사용자를 보호할 수 있는 방법이 없습니다. 즉, 모든 리듀서에 대한 테스트는 특히 강력해야 합니다.

사용자를 보호할 라이브러리가 없으면 발생할 수 있는 문제의 다음 코드 예제를 고려하십시오.

 const initialState = [] function addUserReducer(state = initialState, action) { if (action.type === 'USERS_ADD') { state.push(action.payload) // NOTE: mutating action!! return state } return state // note that a reducer MUST return a value }

이 코드 예제에서는 이전 상태가 현재 상태와 동일하므로 시간 여행이 중단됩니다. 데이터가 데이터를 전달하더라도 상태에 대한 참조가 변경되지 않았기 때문에 순수 구성 요소는 잠재적으로 업데이트(또는 다시 렌더링)되지 않을 수 있습니다. 포함이 변경되었으며 돌연변이를 추론하기가 훨씬 더 어렵습니다.

불변성 라이브러리가 없으면 Redux가 제공하는 모든 이점을 잃게 됩니다. 따라서 특히 여러 손으로 코드를 만지는 대규모 팀에서 작업할 때 immutable.js 또는 seamless-immutable과 같은 불변성 도우미 라이브러리를 사용하는 것이 좋습니다.

어떤 라이브러리를 사용하든 Redux는 동일하게 작동합니다. 둘 다 장단점을 비교하여 사용 사례에 가장 적합한 것을 선택할 수 있습니다.

불변.js

Immutable.js는 Facebook에서 구축한 라이브러리로 Maps, Lists, Sets, Sequences와 같은 데이터 구조에 보다 기능적인 스타일을 적용합니다. 불변의 영구 데이터 구조 라이브러리는 서로 다른 상태 간에 가능한 최소한의 복사를 수행합니다.

장점:

  • 구조 공유
  • 업데이트 시 더 효율적
  • 메모리 효율성 향상
  • 업데이트를 관리하는 도우미 메서드 모음이 있습니다.

단점:

  • 기존 JS 라이브러리(예: lodash, ramda)와 원활하게 작동하지 않습니다.
  • 특히 수화/탈수 및 렌더링 중에 (toJS/fromJS)로의 변환이 필요합니다.

심리스 불변

Seamless-immutable은 ES5까지 역호환되는 불변 데이터용 라이브러리입니다.

객체의 돌연변이를 비활성화하는 defineProperty(..) 와 같은 ES5 속성 정의 기능을 기반으로 합니다. 따라서 lodash 및 Ramda와 같은 기존 라이브러리와 완벽하게 호환됩니다. 프로덕션 빌드에서 비활성화하여 잠재적으로 상당한 성능 향상을 제공할 수도 있습니다.

장점:

  • 기존 JS 라이브러리(예: lodash, ramda)와 원활하게 작동
  • 변환을 지원하기 위해 추가 코드가 필요하지 않음
  • 프로덕션 빌드에서 검사를 비활성화하여 성능을 향상시킬 수 있습니다.

단점:

  • 구조적 공유 없음 - 개체/배열이 얕게 복사되어 대용량 데이터 세트의 경우 속도가 느려집니다.
  • 메모리 효율이 좋지 않음

Redux 및 다중 감속기

Redux의 또 다른 유용한 기능은 감속기를 함께 구성하는 기능입니다. 이를 통해 훨씬 더 복잡한 응용 프로그램을 만들 수 있으며 상당한 크기의 응용 프로그램에서는 필연적으로 여러 유형의 상태(현재 사용자, 로드된 게시물 목록, 등). Redux는 combineReducers 기능을 자연스럽게 제공하여 이 사용 사례를 지원하고 권장합니다.

 import { combineReducers } from 'redux' import currentUserReducer from './currentUserReducer' import postsListReducer from './postsListReducer' export default combineReducers({ currentUser: currentUserReducer, postsList: postsListReducer, })

위의 코드를 사용하면 currentUser 에 의존하는 구성 요소와 postsList 에 의존하는 다른 구성 요소를 가질 수 있습니다. 이것은 또한 단일 구성 요소가 트리의 모든 분기에만 관련되므로 성능을 향상시킵니다.

Redux의 불순한 작업

기본적으로 일반 JavaScript 객체만 Redux에 전달할 수 있습니다. 그러나 미들웨어를 사용하면 Redux는 현재 시간 가져오기, 네트워크 요청 수행, 디스크에 파일 쓰기 등과 같은 불순한 작업을 지원할 수 있습니다.

'미들웨어'는 전달되는 작업을 가로챌 수 있는 기능에 사용되는 용어입니다. 가로채면 다른 프레임워크(예: Express.js)의 미들웨어와 마찬가지로 작업을 변환하거나 비동기 작업을 전달하는 등의 작업을 수행할 수 있습니다.

두 가지 매우 일반적인 미들웨어 라이브러리는 Redux Thunk와 Redux-saga입니다. Redux Thunk는 명령형 스타일로 작성되었으며 Redux-saga는 기능적 스타일로 작성되었습니다. 둘 다 비교해보자.

리덕스 썽크

Redux Thunk는 다른 연결 가능 함수를 반환하는 함수인 썽크를 사용하여 Redux 내에서 불순한 작업을 지원합니다. Redux-Thunk를 사용하려면 먼저 Redux Thunk 미들웨어를 스토어에 마운트해야 합니다.

 import { createStore, applyMiddleware } from 'redux' import thunk from 'redux-thunk' const store = createStore( myRootReducer, applyMiddleware(thunk), // here, we apply the thunk middleware to R )

이제 Redux 저장소에 썽크를 디스패치하여 불순한 작업(예: API 호출 수행)을 수행할 수 있습니다.

 store.dispatch( dispatch => { return api.fetchUsers() .then(users => dispatch(usersFetched(users)) // usersFetched is a function that returns a plain JavaScript object (Action) .catch(err => dispatch(usersFetchError(err)) // same with usersFetchError } )

썽크를 사용하면 코드를 테스트하기 어렵고 코드 흐름을 통해 추론하기가 더 어려워질 수 있다는 점에 유의하는 것이 중요합니다.

Redux-사가

Redux-saga는 제너레이터라고 하는 ES6(ES2015) 기능과 기능적/순수한 도우미 라이브러리를 통해 불순한 작업을 지원합니다. 생성기의 가장 큰 장점은 다시 시작 및 일시 중지할 수 있고 API 계약을 통해 테스트하기가 매우 쉽다는 것입니다.

sagas를 사용하여 이전 썽크 방법의 가독성과 테스트 가능성을 어떻게 향상시킬 수 있는지 봅시다!

먼저 Redux-saga 미들웨어를 스토어에 마운트하겠습니다.

 import { createStore, applyMiddleware } from 'redux' import createSagaMiddleware from 'redux-saga' import rootReducer from './rootReducer' import rootSaga from './rootSaga' // create the saga middleware const sagaMiddleware = createSagaMiddleware() // mount the middleware to the store const store = createStore( rootReducer, applyMiddleware(sagaMiddleware), ) // run our saga! sagaMiddleware.run(rootSaga)

run(..) 함수는 실행을 시작하기 위해 saga와 함께 호출되어야 합니다.

이제 사가를 만들어 보겠습니다.

 import { call, put, takeEvery } from 'redux-saga/effects' // these are saga effects we'll use export function *fetchUsers(action) { try { const users = yield call(api.fetchUsers) yield put(usersFetched(users)) } catch (err) { yield put(usersFetchFailed(err)) } } export default function *rootSaga() { yield takeEvery('USERS_FETCH', fetchUsers) }

사용자 목록과 rootSaga 를 가져오는 두 개의 생성기 함수를 정의했습니다. api.fetchUsers 를 직접 호출하지 않고 대신 호출 개체에서 생성했음을 주목하세요. 이것은 Redux-saga가 호출 객체를 가로채고 그 안에 포함된 함수를 실행하여 순수한 환경을 만들기 때문입니다(제너레이터에 관한 한).

rootSagatakeEvery, 유형으로 전달된 모든 작업을 수행하고 fetchUsers USERS_FETCH 를 호출합니다. 우리가 볼 수 있듯이 이것은 Redux에 대해 매우 예측 가능한 부작용 모델을 생성하므로 테스트하기 쉽습니다!

사가 테스트

생성기를 사용하여 saga를 쉽게 테스트할 수 있는 방법을 살펴보겠습니다. 우리는 이 부분에서 mocha를 사용하여 단위 테스트를 실행하고 chai를 어설션에 사용할 것입니다.

sagas는 일반 JavaScript 개체를 생성하고 생성기 내에서 실행되기 때문에 모의 작업 없이 올바른 동작을 수행하는지 쉽게 테스트할 수 있습니다! call , take , put 등은 Redux-saga 미들웨어가 가로채는 평범한 JavaScript 객체임을 명심하십시오.

 import { take, call } from 'redux-saga/effects' import { expect } from 'chai' import { rootSaga, fetchUsers } from '../rootSaga' describe('saga unit test', () => { it('should take every USERS_FETCH action', () => { const gen = rootSaga() // create our generator iterable expect(gen.next().value).to.be.eql(take('USERS_FETCH')) // assert the yield block does have the expected value expect(gen.next().done).to.be.equal(false) // assert that the generator loops infinitely }) it('should fetch the users if successful', () => { const gen = fetchUsers() expect(gen.next().value).to.be.eql(call(api.fetchUsers)) // expect that the call effect was yielded const users = [ user1, user2 ] // some mock response expect(gen.next(users).value).to.be.eql(put(usersFetched(users)) }) it('should fail if API fails', () => { const gen = fetchUsers() expect(gen.next().value).to.be.eql(call(api.fetchUsers)) // expect that the call effect was yielded const err = { message: 'authentication failed' } // some mock error expect(gen.throw(err).value).to.be.eql(put(usersFetchFailed(err)) }) })

React로 작업하기

Redux는 특정 컴패니언 라이브러리에 연결되어 있지 않지만 React 구성 요소는 상태를 입력으로 사용하고 가상 DOM을 출력으로 생성하는 순수 함수이기 때문에 특히 React.js에서 잘 작동합니다.

React-Redux는 React와 Redux를 연결하는 대부분의 수고를 덜어주는 도우미 라이브러리입니다. React-Redux를 가장 효과적으로 사용하기 위해 프리젠테이션 구성 요소와 컨테이너 구성 요소의 개념을 살펴보겠습니다.

프리젠테이션 구성 요소는 렌더링할 소품에만 의존하여 사물이 시각적으로 어떻게 보여야 하는지 설명합니다. 그들은 소품에서 콜백을 호출하여 액션을 디스패치합니다. 그것들은 손으로 작성되었으며 완전히 순수하며 Redux와 같은 상태 관리 시스템에 묶여 있지 않습니다.

반면 컨테이너 구성 요소는 작동 방식을 설명하고 Redux를 인식하고 Redux 작업을 직접 전달하여 돌연변이를 수행하고 일반적으로 React-Redux에서 생성됩니다. 그것들은 종종 프리젠테이션 컴포넌트와 짝을 이루어 소품을 제공합니다.

Redux의 프레젠테이션 구성 요소 및 컨테이너 구성 요소.

프레젠테이션 구성 요소를 작성하고 React-Redux를 통해 Redux에 연결해 보겠습니다.

 const HelloWorld = ({ count, onButtonClicked }) => ( <div> <span>Hello! You've clicked the button {count} times!</span> <button onClick={onButtonClicked}>Click me</button> </div> ) HelloWorld.propTypes = { count: PropTypes.number.isRequired, onButtonClicked: PropTypes.func.isRequired, }

이것은 props에 완전히 의존하여 작동하는 "멍청한" 구성 요소입니다. 이것은 React 구성 요소를 테스트하기 쉽고 구성하기 쉽기 때문에 훌륭합니다. 이제 이 구성 요소를 Redux에 연결하는 방법을 살펴보겠습니다. 하지만 먼저 Higher Order Component가 무엇인지 살펴보겠습니다.

고차 부품

React-Redux는 Redux를 인식하는 "멍청한" React 구성 요소에서 고차 구성 요소를 생성하는 connect( .. ) 라는 도우미 함수를 제공합니다.

React는 구성 요소를 다른 구성 요소로 감싸는 구성을 통해 확장성과 재사용성을 강조합니다. 이러한 구성 요소를 래핑하면 동작을 변경하거나 새로운 기능을 추가할 수 있습니다. 컨테이너 구성 요소인 Redux를 인식하는 프레젠테이션 구성 요소에서 고차 구성 요소를 만드는 방법을 살펴보겠습니다.

방법은 다음과 같습니다.

 import { connect } from 'react-redux' const mapStateToProps = state => { // state is the state of our store // return the props that we want to use for our component return { count: state.count, } } const mapDispatchToProps = dispatch => { // dispatch is our store dispatch function // return the props that we want to use for our component return { onButtonClicked: () => { dispatch({ type: 'BUTTON_CLICKED' }) }, } } // create our enhancer function const enhancer = connect(mapStateToProps, mapDispatchToProps) // wrap our "dumb" component with the enhancer const HelloWorldContainer = enhancer(HelloWorld) // and finally we export it export default HelloWorldContainer

mapStateToPropsmapDispatchToProps 라는 두 가지 함수를 정의했습니다.

mapStateToProps 는 Redux 상태에서 계산된 객체를 반환하는 (state: Object)의 순수 함수입니다. 이 개체는 래핑된 구성 요소에 전달된 소품과 병합됩니다. 이것은 컴포넌트의 소품에 병합될 Redux 상태의 일부를 선택하기 때문에 선택기라고도 합니다.

mapDispatchToProps 도 순수 함수이지만 Redux 디스패치 함수에서 계산된 객체를 반환하는 (dispatch: (Action) => void) 중 하나입니다. 이 객체는 마찬가지로 래핑된 구성 요소에 전달된 소품과 병합됩니다.

이제 컨테이너 구성 요소를 사용하려면 React-Redux의 Provider 구성 요소를 사용하여 컨테이너 구성 요소에 사용할 저장소를 알려야 합니다.

 import { Provider } from 'react-redux' import { render } from 'react-dom' import store from './store' // where ever your Redux store resides import HelloWorld from './HelloWorld' render( ( <Provider store={store}> <HelloWorld /> </Provider> ), document.getElementById('container') )

Provider 구성 요소는 저장소를 Redux 저장소를 구독하는 모든 하위 구성 요소로 전파하여 모든 것을 한 곳에 유지하고 오류 또는 변형 지점을 줄입니다!

Redux로 코드 신뢰 구축

Redux, 수많은 지원 라이브러리 및 React.js와의 프레임워크 연결에 대한 이 새로운 지식을 통해 상태 제어를 통해 애플리케이션의 변형 수를 쉽게 제한할 수 있습니다. 강력한 상태 제어를 통해 더 빠르게 이동하고 더 자신 있게 견고한 코드 기반을 생성할 수 있습니다.