본문 바로가기

FrontEnd

[번역] Idiomatic Redux: The Tao of Redux, Part 1 - Implementation and Intent

반응형

리덕스를 올바르게 사용하는 방법을 배워봅시다. 1편에서는 리덕스의 구현과 그렇게 구현한 의도를 다룹니다.

2편 링크 : https://itchallenger.tistory.com/687

 

[번역] Idiomatic Redux: The Tao of Redux, Part 2 - Practice and Philosophy

실무에서의 리덕스의 활용방법과 리덕스의 철학에 대해 좀 더 자세히 알아봅시다. 1편 번역 : https://itchallenger.tistory.com/685 원문 : https://blog.isquaredsoftware.com/2017/05/idiomatic-redux-tao-of-r..

itchallenger.tistory.com

원문 : https://blog.isquaredsoftware.com/2017/05/idiomatic-redux-tao-of-redux-part-1/

 

Idiomatic Redux: The Tao of Redux, Part 1 - Implementation and Intent

Thoughts on what Redux requires, how Redux is intended to be used, and what is possible with Redux

blog.isquaredsoftware.com

 

Redux는 기본적으로 믿을 수 없을 정도로 단순한 패턴입니다.

현재 값을 저장하고 필요할 때 해당 값을 업데이트하는 단일 함수를 실행하고 구독자에게 변경 사항이 있음을 알립니다.

그 단순성에도 불구하고 또는 아마도 그것 때문에 Redux를 사용하는 방법에 대한 다양한 접근 방식, 의견 및 태도가 있습니다.

이러한 접근 방식의 대부분은 공식 문서에 있는 개념 및 예제와 크게 다릅니다.

동시에 Redux가 특정 방식으로 작업을 수행하도록 "강제"하는 방법에 대한 지속적인 불만이 있습니다.
많은 불만 사항은 실제로 Redux 라이브러리 자체의 문제가 아니라,
Redux가 일반적으로 사용되는 방식과 관련된 개념과 관련이 있습니다.
 
아래는 한 해커뉴스 스레드에서만 발췌한 불만입니다.
  • "보일러플레이트가 너무 많다"
  • "액션 상수와 액션 생성자는 불필요하다"
  • "기능을 추가하려면 너무 많은 파일을 편집해야 한다"
  • "쓰기 로직을 왜 다른 파일에서 파일을 전환해야 합니까?"
  • "용어와 이름이 배우기 너무 어렵거나 혼란스럽습니다."
  • 그리고 훨씬 더 많습니다
Redux가 실제로 작동하는 방식과 Redux가 의도된 방식을 구별하는 것이 중요하다는 결론을 내렸습니다.
Redux를 무지성으로 사용할 수 있는 거의 무한한 방법이 있습니다.
 
특정 Redux 사용 패턴과 관행이 존재하는 이유,
Redux의 철학과 의도, 그리고 내가 "관용적" 및 "비관용적" Redux 사용이라고 생각하는 것을 설명하고자 합니다.
이 게시물은 두 부분으로 나뉩니다.
 
1부 - 구현 및 의도에서는 Redux의 실제 구현, 필요한 특정 제한 사항 및 제약 사항, 이러한 제한 사항이 존재하는 이유를 살펴보겠습니다. 그리고 저자(특히 초기 개발 프로세스 동안)의 토론과 진술을 기반으로 Redux의 원래 의도와 설계 목표를 검토합니다.
2부 - 활용과 철학에서는 Redux 앱에서 널리 사용되는 일반적인 관행을 조사하고 이러한 관행이 애초에 존재하는 이유를 설명합니다.
마지막으로 Redux를 사용하기 위한 여러 "대안" 접근 방식을 검토하고
많은 방법이 가능하지만 반드시 "관용적"인 것은 아닌 이유에 대해 논의합니다.

기초 다지기

세가지 원칙

Redux의 세 가지 원칙(Three Principles of Redux)을 살펴보는 것으로 시작하겠습니다.

  • 단일 진실 원천 : 전체 애플리케이션의 상태가 단일 저장소 내의 객체 트리에 저장됩니다.
  • 상태는 읽기 전용입니다 : 상태를 변경하는 유일한 방법은 발생한 일을 설명하는 객체인 액션을 내보내는 것입니다.
  • 변경은 순수 함수에서 발생합니다 : 상태 트리가 액션에 의해 변환되는 방식을 지정하려면 순수 함수 리듀서를 작성합니다.

현실적인 의미에서, 그 진술들 각각은 거짓말입니다!

(또는 제다이의 귀환의 고전적인 대사를 빌리자면 "그들은 사실입니다... 특정 관점에서.")

  • "Single source of truth"는 잘못된 것입니다.
  • "상태는 읽기 전용입니다"는 잘못된 것입니다.
    • 실제로 애플리케이션이 현재 상태 트리를 수정하는 것을 리덕스가 막지는 않기 때문입니다.
  • "변경은 순수 함수에서 발생합니다 "는 잘못된 것입니다.
    • 리듀서 함수가 상태 트리를 직접 변경하거나 다른 부작용을 일으킬 수 있기 때문입니다.

이러한 진술이 완전히 사실이 아니라면 왜 이런 진술을 하고 있나요??

이러한 원칙은 Redux의 구현에 관한 내용이 아닙니다.

Redux를 어떻게 사용해야 하는지에 대한 설명입니다.

 

이 주제는 이 토론의 나머지 부분에서 계속될 것입니다.
Redux는 구현 측면에서 최소한의 라이브러리이기 때문에 기술 수준에서 실제로 필요로 하거나 강제하는 것은 거의 없습니다.
 

언어 vs 메타 언어

 
Cheng Lou는 "메타 언어 길들이기"에 대한 ReactConf 2017 강연(ReactConf 2017 talk on "Taming the Meta Language")에서
소스 코드만 "언어"이고
댓글, 테스트, 문서, 자습서, 블로그 게시물 및 회의와 같은 다른 모든 것은 "메타 언어"라고 설명했습니다.
즉, 소스 코드 자체는 일정량의 정보만 전달할 수 있습니다.
사람들이 "언어"를 이해하도록 돕기 위해서는 인간 수준의 정보 전달에 대한 많은 추가 계층이 필요합니다.
Cheng Lou의 연설은 추가 개념을 실제 프로그래밍 언어 자체로 옮겨,
아이디어를 전달하기 위해 "메타 언어"를 사용하지 않고도
소스 코드를 통해 더 많은 정보를 표현할 수 있는 방법에 대해 계속 논의합니다.
그런 관점에서 Redux는 아주 작은 "언어"이고
어떻게 사용해야 하는지에 대한 거의 모든 정보는 실제로 "메타 언어"입니다.
"언어"(Redux 코어 라이브러리)는 최소한의 표현력을 가지므로 Redux를 둘러싼 개념, 규범 및 아이디어는 모두 "메타 언어" 수준입니다.
이것이 의미하는 것은 특정 관행이 코드 레벨이 아닌 주변(문서, 주석)에 존재하는 이유를 이해해야 한다는 것입니다.
Redux와 "관용적"인 것과 그렇지 않은 것에 대한 결정은 소스 코드를 기반으로 한 결정보다는
의견과 토론을 포함할 것입니다.



리덕스는 실제로 어떻게 동작하나요?

우리가 사물의 철학적 측면에 대해 더 깊이 들어가기 전에 Redux가 실제로 어떤 기술적 기대치를 가지고 있는지 이해하는 것이 중요합니다. 내부 및 구현을 살펴보면 유익합니다.

리덕스 코어 : createStore

createStore 기능은 Redux 기능의 핵심입니다.
코멘트, 오류 검사, 스토어 인핸서 및 옵저버블과 같은 몇 가지 고급 기능에 대한 코드를 제거하면
createStore가 다음과 같이 나타납니다.
(Hacking Redux 튜토리얼에서 가져온 코드 샘플입니다.)
function createStore(reducer) {
    var state;
    var listeners = []

    function getState() {
        return state
    }
    
    function subscribe(listener) {
        listeners.push(listener)
        return function unsubscribe() {
            var index = listeners.indexOf(listener)
            listeners.splice(index, 1)
        }
    }
    
    function dispatch(action) {
        state = reducer(state, action)
        listeners.forEach(listener => listener())
    }

    dispatch({})

    return { dispatch, subscribe, getState }
}

약 25줄의 코드이지만 핵심 기능이 포함되어 있습니다.

  • 현재 상태 값과 여러 구독자를 추적하며,
  • 값을 업데이트하고
  • 액션이 전달될 때 구독자에게 알리고
  • 스토어 API를 노출합니다.
이 스니펫에 포함되지 않은 모든 사항을 고려하세요.
  • 불변성
  • "순수 함수"
  • 미들웨어
  • 인핸서
  • 정규화
  • 셀렉터
  • Thunks
  • Sagas
  • 액션 타입이 문자열 또는 기호여야 하는지 여부, 액션 타입을 상수로 정의해야 하는지 또는 인라인으로 작성해야 하는지 여부
  • 액션 생성자를 사용해야 하는지 여부
  • 스토어에 직렬화 불가능한 항목이 포함되어야 하는지 여부
    • Promise 또는 클래스 인스턴스
  • 정규화 또는 중첩하여 저장할지 여부
  • 비동기 논리가 있어야 하는 위치

그런 맥락에서 Dan Abramov의 "counter-vanilla" 예를 인용할 가치가 있습니다.

(소스 코드 : https://github.com/reduxjs/redux/blob/765a3a5742a591adba7ae8b61e0516c49ca594bd/examples/counter-vanilla/index.html)

(Dan Abramov's pull request for the "counter-vanilla" example)

새로운 Counter Vanilla 예제는
Redux가 Webpack, React, 핫 리로딩, sagas, 액션 생성자, 상수, Babel, npm, CSS 모듈, 데코레이터, 유창한 라틴어,
Egghead 구독, PhD 또는 웹 온톨로지 랭귀지 이상의 수준이 필요하다는 신화를 없애기 위함입니다.
필요한것은 단지, HTML, 약간 특이한 스크립트 태그 및 평범한 오래된 DOM 조작입니다. 즐기세요!
createStore 내부의 디스패치 함수는 단순히 리듀서 함수를 호출하고 반환하는 값을 저장합니다.
하지만 이 아이디어 목록에 있는 항목들은 좋은 Redux 앱이 신경써야 할 개념으로 널리 인정받고 있습니다.
createStore가 신경 쓰지 않는 모든 항목을 나열한 후 실제로 필요한 것이 무엇인지 확인하는 것이 중요합니다.
실제 createStore 함수는 두 가지 특정 제한 사항을 적용합니다.
  • 저장소에 도달하는 작업은 일반 개체여야 하고
  • 액션에는 정의되지 않은 타입 필드가 있어야 합니다.

이러한 제약 조건은 모두 원래 "플럭스 아키텍처" 개념에서 기원합니다. Flux 문서의 Flux Actions 및 Dispatcher(Flux Actions and the Dispatcher) 섹션을 인용합니다:

애플리케이션과 상호 작용하는 사람을 통해 또는 웹 API 호출을 통해 새 데이터가 시스템에 입력되면,
해당 데이터는 새 데이터 필드와 특정 액션 타입을 포함하는 개체 리터럴인 액션으로 패키지됩니다.
액션 객체를 생성할 뿐만 아니라 디스패처에 액션을 전달하는 ActionCreators라는
도우미 메서드 라이브러리를 만드는 경우가 많습니다.

다른 액션은 타입 속성으로 식별됩니다.
모든 스토어가 액션을 수신하면 일반적으로 이 타입 속성을 사용하여 응답해야 하는지 여부와 방법을 결정합니다.
Flux 애플리케이션에서 스토어와 뷰는 모두 스스로를 제어합니다.
그들은 외부 물체에 의해 작동되지 않습니다.
액션은 setter 메소드가 아니라 정의하고 등록하는 콜백을 통해 스토어로 흐릅니다.
Redux는 원래 타입 필드를 구체적으로 요구하지 않았지만
나중에 가능한 오타나 잘못된 액션 상수 가져오기를 포착하고
액션 개체의 기본 구조에 대한 시간 낭비(바이크셰딩)을 피하기 위해 유효성 검사가 추가되었습니다.

내장 유틸리티: CombineReducers 

여기에서 더 많은 사람들에게 친숙한 몇 가지 제약 조건이 보이기 시작합니다.

CombineReducers는 제공된 각 슬라이스 리듀서가 기본 상태(default state)를 반환하여

알 수 없는 액션에 "정확하게" 응답하고 undefined를 반환하지 않을 것을 기대합니다.

또한 현재 상태 값이 일반 JS 객체이고 현재 상태 객체의 키와 리듀서 함수 객체 사이에 정확한 대응이 있을 것으로 예상합니다.

마지막으로 모든 슬라이스 리듀서가 이전 값을 반환했는지 확인하기 위해 동등 비교를 사용합니다.

 

반환된 모든 값이 동일한 것으로 나타나면 실제로 아무 것도 변경되지 않은 것으로 가정하고

잠재적인 최적화로 원래 루트 상태 개체를 반환합니다.


진짜 셀링 포인트 : Redux DevTools

Redux DevTools는 디스패치된 액션 목록을 추적하여 시간 이동 동작을 구현하는 스토어 인핸서와
기록을 보고 조작할 수 있는 UI의 두 가지 주요 부분으로 구성됩니다.
스토어 인핸서 자체는 액션의 내용이나 상태에 신경 쓰지 않고 메모리에 액션을 저장하기만 합니다.
원래 DevTools UI는 애플리케이션의 컴포넌트 트리 내부에서 렌더링하는 컴포넌트이며 액션이나 상태의 내용에도 신경 쓰지 않습니다.
 
그러나 Redux DevTools Extension은 별도의 프로세스(적어도 Chrome에서는)에서 동작하므로
모든 시간 이동 기능이 적절하고 성능 있게 작동하려면 모든 액션과 상태를 직렬화할 수 있어야 합니다.
상태 및 액션을 가져오고 내보내는 함수도 직렬화 가능해야 합니다.
시간 여행 디버깅을 위한 또 다른 요구 사항은 불변성과 순수 함수입니다.
  • 리듀서 함수가 상태를 변경하면 디버거에서 작업 간에 점프하면 값이 일관되지 않습니다.
  • 리듀서에 부작용이 있는 경우 DevTools에서 액션을 재생할 때마다 해당 부작용이 다시 실행됩니다.
두 경우 모두 시간 여행 디버깅이 예상대로 완전히 동작하지 않습니다.

주 UI 바인딩 : 리액트 리덕스와 connect

React-Redux의 connect 함수는 돌연변이가 실제로 문제가 되는 곳입니다.
connect에 의해 생성된 래퍼 컴포넌트는 래핑된 컴포넌트가 실제로 필요할 때만 다시 렌더링되도록 많은 최적화를 구현합니다.
이러한 최적화는 데이터가 실제로 변경되었는지 확인하기 위한 참조 동등성 검사를 중심으로 이루어집니다.
특히 액션이 전달되고 구독자에게 알림이 표시될 때마다 connect는 루트 상태 개체가 변경되었는지 확인합니다.
그렇지 않은 경우 connect는 상태의 다른 것이 변경되지 않은 것으로 가정하고 추가 렌더링 작업을 건너뜁니다.
(이것이 CombineReducers가 가능한 경우 동일한 루트 상태 객체를 반환하려고 시도하는 이유입니다.)
 
루트 상태 객체가 변경되면 connect는 제공된 mapStateToProps 함수를 호출하고
현재 결과와 이전 반환 결과에 대해 얕은 동등성 검사를 수행하여 스토어 데이터의 프롭이 변경되었는지 확인합니다.
 
다시 말하지만, 데이터 내용이 동일한 것으로 보이면 connect는 실제로 래핑된 컴포넌트를 다시 렌더링하지 않습니다.
connect에서 이러한 동등성 검사가 우발적인 상태 돌연변이로 인해 컴포넌트가 다시 렌더링되지 않는 이유입니다.
connect는 데이터가 변경되지 않았으며 다시 렌더링할 필요가 없다고 가정하기 때문입니다.

일반적으로 같이 쓰는 라이브러리 :  React와 Reselect

불변성은 Redux와 함께 일반적으로 사용되는 다른 라이브러리에서도 작동합니다.
 
Reselect 라이브러리는 일반적으로 Redux 상태 트리에서 데이터를 추출하는 데 사용되는 메모된 "셀렉터" 함수를 생성합니다.
메모이제이션은 일반적으로 입력 매개변수가 동일한지 확인하기 위해 참조 동등성 검사에 의존합니다.
유사하게, React 컴포넌트는 원하는 로직을 shouldComponentUpdate에 구현할 수 있지만,
가장 일반적인 구현은 return !shallowEqual(this.props, nextProps)과 같이
현재 props와 들어오는 props의 얕은 동등성 검사에 의존합니다.
두 경우 모두 데이터 변형은 일반적으로 바람직하지 않은 동작을 초래합니다.
메모된 선택기는 적절한 값을 반환하지 않을 가능성이 높으며
최적화된 React 컴포넌트는 필요할 때 다시 렌더링하지 않습니다.

Redux의 기술 요구 사항 요약 

코어 Redux createStore 함수 자체는 코드 작성 방법에 대해 두 가지 제한을 두었습니다.
  • 액션은 일반 객체여야 하고
  • 정의된 타입 필드를 포함해야 합니다.
불변성, 직렬화 가능성 또는 부작용이나 타입 필드의 값이 실제로 무엇인지는 신경 쓰지 않습니다.
(사용자가 신경써야 합니다.)
즉, Redux DevTools, React-Redux, React 및 Reselect를 포함하여
해당 코어 주변에서 일반적으로 사용되는 부분은 불변성, 직렬화 가능한 액션/상태 및 순수 리듀서 함수의 적절한 사용에 의존합니다.
이러한 기대를 무시해도 주요 응용 프로그램 논리가 제대로 작동할 수 있지만
시간 여행 디버깅 및 컴포넌트 리렌더링이 중단될 가능성이 매우 높습니다.
이는 다른 영속성 관련 사용 사례에도 영향을 미칩니다.
불변성, 직렬화 가능성 및 순수 함수는 Redux에 의해 어떤 식으로든 강제되지 않는다는 점에 유의하는 것도 중요합니다.
  • 구현에서 리듀서 함수가 상태를 변경하거나 AJAX 호출을 트리거하는 것은 가능합니다.
  • 구현에서 애플리케이션의 각 부분이 getState()를 호출하고 상태 트리의 내용을 직접 수정하는 것은 가능합니다.
프라미스, 함수, 심볼, 클래스 인스턴스 또는 기타 직렬화할 수 없는 값을 작업이나 상태 트리에 넣는 것은 전적으로 가능합니다.
해서는 안 되지만, 가능합니다.


리덕스의 설계 의도

이러한 기술적 제약을 염두에 두고 Redux를 사용하는 방법에 주의를 기울일 수 있습니다.
그 의도를 더 잘 이해하려면 Redux의 초기 개발을 주도한 아이디어와 영향을 되돌아보는 것이 좋습니다.

리덕스가 영향 받은 것, 리덕스의 목적

Redux 문서의 "소개" 섹션은 동기, 핵심 개념 및 선행 기술 주제에서 Redux의 개발 및 개념에 대한 몇 가지 주요 영향을 설명합니다.

(Motivation, Core Concepts, and Prior Art topics.)

요약하자면:

  • Redux의 주요 목표는 업데이트가 발생할 수 있는 방법과 시기에 대한 제한을 부과하여 상태 돌연변이를 예측 가능하게 만드는 것입니다.
  • 업데이트 논리를 애플리케이션의 나머지 부분과 분리하고 일반적인 액션 객체를 사용하여 발생해야 하는 변경 사항을 설명하는 "플럭스 아키텍처" 아이디어를 차용합니다.
    • 시간 여행 디버깅과 같은 개발 경험 기능은 Redux의 주요 사용 사례로 의도되었습니다.
    • 따라서 이러한 개발 사용 사례를 가능하게 하고 개발자가 데이터 흐름 및 업데이트 논리를 쉽게 추적할 수 있도록 하기 위해 불변성 및 직렬화 가능성과 같은 제약 조건이 크게 존재합니다.
  • Redux는 모든 실제 상태 업데이트 로직이 동기가 되기를 원하고 비동기 동작이 상태 업데이트 동작과 별도로 유지되기를 원합니다.
    • Flux 아키텍처는 서로 다른 유형의 데이터에 대해 여러 개의 개별 "저장소"를 가질 것을 제안했습니다.
    • Redux는 이러한 여러 "저장소"를 단일 상태 트리로 결합하여 디버깅, 상태 지속성 및 실행 취소/다시 실행과 같은 기능을 보다 쉽게 ​​작업할 수 있도록 합니다.
  • 단일 루트 리듀서 함수는 그 자체로 많은 작은 리듀서 함수로 구성될 수 있습니다.
    • 이를 통해 종속성 체인을 설정하기 위해 Flux의 store.waitFor() 이벤트 이미터와 같은 메커니즘에 의존하는 대신 한 상태 슬라이스를 업데이트할 때 다른 슬라이스를 먼저 계산해야 하는 종속성 순서를 포함하여 데이터 처리 방법을 명시적으로 제어할 수 있습니다.
Redux README의 초기 버전에서 명시된 설계 목표를 살펴보는 것도 가치가 있습니다.
  • Redux를 사용하기 위해 함수형 프로그래밍에 대한 책이 필요하지 않습니다.
  • 모든 것(Stores, Action Creators, configuration)은 핫 리로딩이 가능합니다.
  • Flux의 이점을 유지하지만 함수적 특성 덕분에 다른 좋은 속성을 추가합니다.
  • Flux 코드에서 일반적인 일부 안티 패턴을 방지합니다.
  • 싱글톤을 사용하지 않고 데이터를 재수화(rehydration)할 수 있기 때문에 동형(isomophic) 앱에서 훌륭하게 작동합니다.
    • (주 : 같은 코드 같은 데이터면 당연히 잘 동작한다는 의미임)
  • 데이터를 저장하는 방법은 신경 쓰지 않습니다. JS 개체, 배열, ImmutableJS 등을 사용할 수 있습니다.
  • 내부적으로는 모든 데이터를 트리에 보관하지만 그것에 대해 생각할 필요는 없습니다.
  • 개별 스토어보다 세분화된 업데이트를 효율적으로 구독할 수 있습니다.
  • 강력한 devtools(예: 시간 여행, 녹음/재생)를 사용자 비용 없이 구현할 수 있는 연결고리를 제공합니다.
  • 쉽게 약속을 지원하거나 코어 외부에서 상수를 생성할 수 있도록 확장점을 제공합니다. 
  • 스토어 및 액션에 래퍼 호출이 없습니다. 당신의 물건은 당신의 물건입니다.
  • 모의 테스트 없이 개별적으로 테스트하는 것은 매우 쉽습니다.
  • "flat" Store를 사용하거나 컴포넌트를 구성하는 것처럼 Store를 구성하고 재사용할 수 있습니다.
  • API 표면적은 최소입니다.
  • 내가 아직 핫 리로딩에 대해 언급하지 않았나요?

설계 원리와 의도

Redux 문서, 초기 Redux 이슈 스레드, Dan Abramov와 Andrew Clark이 다른 곳에서 작성한 많은 다른 의견을 읽으면

Redux의 의도된 디자인 및 사용에 관한 몇 가지 특정 주제를 볼 수 있습니다.

 

Redux는 Flux 아키텍처 구현으로 구축되었습니다

Redux는 원래 Flux Architecture를 구현하는 또 다른 라이브러리로 의도되었습니다. 결과적으로 Flux에서 많은 개념을 상속받았습니다.

  • "액션 디스패치" 아이디어
  • 액션은 타입 필드가 있는 일반 객체,
  • "액션 생성자 함수"를 사용하여 해당 액션 객체 생성
  • "업데이트 로직"은 애플리케이션의 나머지 부분과 분리되어 중앙 집중화되어 있어야 합니다.

저는 "Redux가 $THING을 수행하는 이유는 무엇입니까?"라는 질문을 자주 봅니다.

이러한 질문 중 많은 부분에 대한 대답은 "Flux 아키텍처와 특정 Flux 라이브러리가 작업을 수행한 방식이기 때문입니다."입니다.

상태 업데이트 유지보수성이 우선입니다

Redux의 거의 모든 측면은 개발자가 주어진 상태가 언제, 왜, 어떻게 변경되었는지 쉽게 이해할 수 있도록 하기 위한 것입니다.
여기에는 실제 구현과 권장 사용이 모두 포함됩니다.
즉, 개발자는 디스패치된 작업을 보고 결과적으로 어떤 상태 변경이 발생했는지 확인하고
해당 액션이 디스패치된 코드베이스의 위치를 ​​역추적할 수 있어야 합니다(특히 액션 타입에 따라).
Redux 저장소의 데이터가 잘못된 경우 디스패치된 작업이 잘못된 상태를 초래한 원인을 추적하고
거기에서 역방향으로 작업할 수 있어야 합니다.
"핫 리로딩"과 "시간 이동 디버깅"에 대한 강조 역시 개발자 생산성과 유지 관리 용이성을 목표로 합니다.
두 가지 모두 개발자가 시스템에서 일어나는 일을 더 빨리 반복하고 더 잘 이해할 수 있게 해주기 때문입니다.

액션 히스토리는 의미론적인(명확한 언어적) 의미를 가져야 합니다.

Redux의 핵심은 액션 타입필드의 실제 값이 무엇인지 신경 쓰지 않지만
분명한 의도는 액션 타입이 어떤 종류의 의미와 정보를 가져야 한다는 것입니다.
Redux DevTools 및 기타 로깅 유틸리티는 디스패치된 각 액션에 대한 타입 필드를 표시하므로
한 눈에 이해할 수 있는 값을 갖는 것이 중요합니다.
이는 정보 전달 측면에서 문자열이 기호나 숫자보다 더 유용하다는 것을 의미합니다.
이는 또한 해당 액션 타입 문자열의 문구가 명확하고 이해할 수 있어야 함을 의미합니다.
이것은 일반적으로 하나 또는 두 개의 액션 타입을 갖는 것보다
개발자 이해를 위해 여러개의 명확한 액션 타입을 갖는 것이 더 낫다는 것을 의미합니다.
 
전체 코드베이스에서 단일 액션 타입(예: SET_DATA)만 사용되는 경우
특정 액션이 전달된 위치를 추적하기 어렵고
히스토리 로그를 읽기 어렵습니다.

리덕스는 함수형 프로그래밍 원리 도입을 의도했습니다.

Redux는 명시적으로 함수형 프로그래밍 개념과 함께 구축 및 사용되며 이러한 개념을 신규 및 숙련된 개발자 모두에게 소개하는 데 도움이 됩니다.
여기에는 불변성 및 순수 함수와 같은 FP 기본 사항뿐만 아니라
더 큰 작업을 달성하기 위해 함수를 함께 합성하는 것과 같은 아이디어도 포함됩니다.

동시에 Redux는 너무 많은 추상적인 FP 개념에 사용자를 압도하거나

"모나드" 또는 "endofunctors"와 같은 심층적인 FP 용어에 대한 논쟁에 빠지지 않고 문제를 해결하고 애플리케이션을 구축하려는 개발자에게 실질적인 가치를 제공하는 데 도움을 주기 위한 것입니다.
 
물론 Redux와 관련된 용어와 개념의 수는 시간이 지남에 따라 증가했으며 그 중 많은 부분이 새로운 학습자에게 혼란을 주지만
FP의 이점을 활용하고 학습자에게 FP를 소개하는 목표는 분명히 원래 디자인과 철학의 일부였습니다.

Redux는 테스트 가능한 코드를 장려합니다.

리듀서를 순수 함수로 하면 시간 여행 디버깅이 가능하기도 하지만,
이는 또한 리듀서 함수를 분리하여 쉽게 테스트할 수 있어야 함을 의미합니다.
 
리듀서를 테스트하려면 특정 인수를 사용하여 호출하고 출력을 확인하기만 하면 됩니다.
AJAX 호출과 같은 것을 모킹할 필요는 없습니다.
 
AJAX 호출 및 기타 부작용은 여전히 ​​애플리케이션의 어딘가에 있어야 하며
이를 사용하는 테스트 코드는 여전히 개발이 필요할 수 있습니다.
그러나 코드베이스의 의미 있는 부분를 순수 함수로 작성하면 테스트의 전반적인 복잡성이 줄어듭니다.

리듀서 함수는 상태 슬라이스와 정돈되어야 합니다.

Redux는 Flux 아키텍처에서 개별 "스토어" 개념을 가져와 단일 combined 저장소로 병합합니다.
Flux와 Redux 간의 가장 간단한 매핑은 각 스토어의 상태 트리에 별도의 최상위 키 또는 "슬라이스"를 만드는 것입니다.
Flux 앱에 별도의 UsersStore, PostsStore 및 CommentsStore가 있는 경우
Redux에 해당하는 항목에는 {users, posts, comments}와 같은 루트 상태 트리가 있을 수 있습니다.
모든 상태 조각을 함께 업데이트하기 위한 모든 논리를 포함하는 단일 함수를 가질 수 있지만
의미 있는 애플리케이션은 유지 보수성을 위해 해당 함수를 더 작은 함수로 나누기를 원할 것입니다.
이를 수행하는 가장 확실한 방법은 업데이트해야 하는 상태 조각에 따라 논리를 분할하는 것입니다.
이것은 각 "슬라이스 리듀서"가 자신의 상태 조각에 대해서만 걱정하면 되며, 알고 있는 한 조각이 모든 상태일 수도 있음을 의미합니다.
이 "리듀서 합성" 패턴은 중첩된 상태 구조에 대한 업데이트를 처리하기 위해 반복적으로 중첩될 수 있으며, 특히 이 패턴을 쉽게 따를 수 있도록 CombineReducers 유틸리티가 Redux에 포함되어 있습니다.
각 슬라이스 리듀서 함수를 개별적으로 호출할 수 있고 자체 상태 슬라이스만 매개변수로 제공하는 경우,
이는 여러 슬라이스 리듀서가 동일한 작업으로 호출될 수 있고 각각이 다른 슬라이스와 독립적으로 자체 상태 슬라이스를 업데이트할 수 있음을 의미합니다.
Dan과 Andrew의 진술에 따르면 단일 액션으로 인해 여러 슬라이스 리듀서에서 업데이트가 발생하는 것이 Redux의 핵심 사용 사례입니다.
이것은 종종 리듀서 함수와 "일대 다" 관계를 갖는 액션이라고 합니다.

업데이트 로직은 명확해야 합니다

Redux에는 "마법"이 포함되어 있지 않습니다.
구현의 몇 가지 측면은 좀 더 고급 FP 원칙(예: applyMiddleware 및 스토어 인핸서)에 익숙하지 않은 한 즉시 파악하기 다소 까다롭습니다. 
그러나 다른 부분은 최소한의 추상화로 명시적이고 명확하며 추적 가능합니다.
 
Redux는 상태 업데이트 논리를 구현하지도 않습니다. 단순히 당신이 제공하는 루트 리듀서 함수에 의존합니다.
상태를 독립적으로 관리하는 슬라이스 리듀서의 의도된 일반적인 사용 사례를 돕기 위해 CombineReducers 유틸리티를 제공하지만,
자신의 요구사항 처리하기 위해 고유한 리듀서 로직을 ​​작성하는 것이 전적으로 권장됩니다.
이것은 또한 리듀서 로직이 단순하거나 복잡하거나 추상화되거나 장황할 수 있음을 의미합니다.
모든 것은 코드 작성 방법에 관한 것입니다.
원래 Flux 아키텍처의 디스패처에서 Stores는 종속성 체인을 설정하는 데 사용할 수 있는 waitFor() 이벤트가 필요했습니다. CommentsStore가 자체 업데이트를 위해 PostsStore의 데이터가 필요한 경우 PostsStore가 업데이트된 후 실행되도록 PostsStore.waitFor()를 호출할 수 있습니다.
불행히도 그 종속성 사슬은 쉽게 시각화되지 않았습니다.
그러나 Redux를 사용하면 특정 리듀서 함수를 순서대로 명시적으로 호출하여 시퀀싱을 간단하게 수행할 수 있습니다.

예를 들어, 다음은 Dan(댄 아브라모프)의 "Combining Stateless Stores" gist에서 발췌한 (약간 수정된) 인용문과 스니펫입니다.

export default function commentsReducer(state = initialState, action, hasPostReallyBeenAdded) {}

// elsewhere
export default function rootReducer(state = initialState, action) {
  const postState = postsReducer(state.post, action);
  const {hasPostReallyBeenAdded} = postState;
  const commentState  = commentsReducer(state.comments, action, hasPostReallyBeenAdded);
  return { post : postState, comments : commentState };
}
이 경우 commentReducer는 더 이상 상태와 액션에만 의존하지 않습니다.
hasCommentReallyBeenAdded에도 의존합니다.

이 매개변수를 API에 추가합니다. 더 이상 "원래 형태(state,action)"로 사용할 수 없지만 이것이 요점입니다.
이제 다른 데이터에 대한 명시적인 종속성이 있습니다.
commentReducer는 탑 레벨 스토어가 아닙니다.
commentReducer를 관리하는 사람은 어떻게든 그 데이터를 제공해야 합니다.
이것은 "고차 리듀서"의 개념에도 적용됩니다.
주어진 슬라이스 리듀서는 실행 취소/다시 실행 또는 페이지네이션과 같은 기능을 추가하기 위해 다른 리듀서로 래핑될 수 있습니다.

리덕스의 API는 최대한 적어야 합니다.

이 목표는 Redux 개발 전반에 걸쳐 Dan과 Andrew가 반복적으로 언급했습니다. 그들의 의견 중 일부를 인용하는 것이 가장 쉽습니다.
최고의 API는 종종 API가 없는 것입니다.
미들웨어 및 고차 스토어에 대한 현재 제안은 Redux 코어에서 특별한 처리가 필요하지 않다는 엄청난 이점이 있습니다.
각각 dispatch() 및 createStore()를 둘러싼 래퍼일 뿐입니다. 1.0이 출시되기 전인 오늘도 사용할 수도 있습니다.
이는 확장성과 신속한 혁신을 위한 거대한 승리입니다.
엄격하고 특별한 API보다 패턴과 규칙을 선호해야 합니다.
내가 NuclearJS를 사용하는 대신 Redux를 작성하기로 선택한 이유는 다음과 같습니다.
- ImmutableJS에 대한 강한 의존성을 원하지 않습니다.
- 나는 가능한 한 적은 API를 원한다
- 더 나은 것이 나올 때 Redux에서 쉽게 벗어나고 싶습니다.
Redux를 사용하면 상태에 일반 객체, 배열 등을 사용할 수 있습니다.

createStore와 같은 API는 특정 구현에 바인딩되기 때문에 피하려고 열심히 노력했습니다.
대신 각 엔터티(Reducer, Action Creator)를 Redux에 의존하지 않고 노출할 수 있는 최소한의 방법을 찾으려 했습니다.
Redux를 임포트하고 리덕스이 크게 의존하는 유일한 코드는 루트 컴포넌트와 이를 구독하는 컴포넌트 입니다.

Redux는 가능한 한 확장 가능해야 합니다

이것은 "최소 API" 목표와 관련이 있습니다.
Andrew의 Flummox lib와 같은 일부 Flux 라이브러리에는 라이브러리 자체에 내장된 비동기 동작 타입이 있습니다
(예: 프로미스에 대한 START/SUCCESS/FAILURE 액션 전달).
코어에 무언가가 내장되어 있다는 것은 항상 사용할 수 있음을 의미했지만 유연성도 제한되었습니다.
 
설계 토론과 Dan과 Andrew의 Hashnode AMA(Hashnode AMA with Dan and Andrew)에서 의견을 인용하는 것이 가장 쉽습니다.
비동기 작업을 계속 지원하고 외부 플러그인 및 도구에 대한 확장점을 제공하기 위해
몇 가지 일반적인 액션 미들웨어, 미들웨어 작성 헬퍼 함수 작성에 관한 방법과,
확장 라이브러리 작성자가 코딩을 쉽게 만드는 방법에 대한 문서를 제공할 수 있습니다.

Andrew - #215:

나는 그것이 대부분의 Redux 앱이 갖고 싶어할 자연스러운 기능이라는 데 동의하지만,
일단 우리가 그것을 코어에 넣으면 모든 사람들이
그것이 작동해야 하는 방식에 관해 바이크셰딩(시간낭비)하기 시작할 것입니다.
이것이 Flummox에서 저에게 일어난 일입니다.

우리는 코어를 최대한 최소화하고 유연하게 유지하여
빠르게 반복하고 다른 사람들이 그 위에 빌드할 수 있도록 하려고 노력하고 있습니다.
Dan이 한 번 말했듯이 (어디가... 아마도 Slack인지 기억나지 않습니다)
우리는 Flux 라이브러리의 Koa처럼 되는 것을 목표로 합니다.
결국 커뮤니티가 더 성숙해지면 reduxjs GitHub 조직에서 "축복받은" 플러그인 및 확장 모음을 유지 관리할 계획입니다.

Dan - Hashnode AMA:

우리는 많은 사람들이 기본적인 비동기 작업을 수행하는 Rx 연산자를 배우는 데 익숙하지 않다는 것을 알고 있기 때문에
Redux 자체에서 이와 같은 것을 처방하고 싶지 않았습니다.

비동기 로직이 복잡할 때 Rx가 유리하지만
모든 Redux 사용자가 Rx를 배우도록 강요하고 싶지 않았기 때문에 의도적으로 미들웨어를 더 유연하게 유지했습니다.

Andrew - Hashnode AMA:

미들웨어 API가 등장한 이유는 비동기에 대한 특정 솔루션을 명시적으로 규정하고 싶지 않았기 때문입니다.
이전 Flux 라이브러리인 Flummox에는 프로미스 미들웨어가 내장되어 있었습니다.
내장되어 있기 때문에 동작을 변경하거나 옵트아웃할 수 없었습니다.

Redux를 사용하면 커뮤니티에서 우리가 구축할 수 있었던 것보다
더 나은 비동기 솔루션을 많이 만들 수 있다는 것을 알았습니다.

마지막 생각정리

나는 이 두 포스트(2편을 포함) 작성을 위한 조사에 많은 시간을 할애했습니다.
초기 문제와 토론을 다시 읽고 Redux가 현재 우리가 알고 있는 것으로 진화하는 것을 보는 것은 매혹적이었습니다.
인용된 README에서 볼 수 있듯이 Redux에 대한 비전은 처음부터 명확했으며 최종 API 및 구현으로 이어진 몇 가지 구체적인 통찰력과 개념적 도약이 있었습니다.
Redux의 내부와 역사에 대한 이 관찰이
Redux가 실제로 어떻게 작동하는지,
왜 이런 방식으로 구축되었는지에 대한 이해에 도움이 되기를 바랍니다.
Redux의 도(Tao of Redux) 파트 2 - Practice and Philosophy(The Tao of Redux, Part 2 - Practice and Philosophy)를 확인하세요.
여기에서 Redux 사용 패턴이 많이 존재하는 이유를 살펴보고
각 패턴의 장단점에 대한 제 생각을 알려 드리겠습니다.

Further Information 🔗︎

해당 글을 작성하는데 참고한 게시물들을 보고 싶으면 직접 원문을 확인하세요!

반응형