본문 바로가기

FrontEnd

[번역] 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-redux-part-2/

 

Idiomatic Redux: The Tao of Redux, Part 2 - Practice and Philosophy

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

blog.isquaredsoftware.com

 

리덕스 활용

Redux 앱에서 사용되는 일반적인 접근 방식과 "모범 사례"의 긴 목록이 있습니다.
"보일러 플레이트"에 대한 빈번한 불만 중 많은 부분이 이러한 개념과 관련되어 있습니다.
이러한 관행을 살펴보고 일반적이고 일반적으로 권장되는 이유를 검토해 보겠습니다.

액션, 액션 상수, 액션 생성자

이 세 가지 개념은 아마도 "보일러 플레이트"에 대한 불만에서 가장 일반적으로 언급되는 개념일 것입니다.
각각을 개별적으로 그리고 자세히 다루려고 했지만 이러한 개념이 존재하는 이유와 이것이 좋은 이유를 설명하는 잘 작성된 문서가 이미 있다는 것을 깨달았습니다.
Redux 문서의 "Reducing Boilerplate" 페이지 입니다. (the "Reducing Boilerplate" page in the Redux docs)
요점을 빠르게 요약하겠습니다.
  • 액션: 일반 직렬화 가능한 액션 개체로 업데이트를 설명하면 시간 여행 디버깅 및 핫 리로딩이 가능합니다.
  • 액션 상수를 통해 : 일관된 명명 규칙을 지원하고, 애플리케이션에서 사용되는 액션 타입을 보다 쉽게 ​​확인하고, 오타를 방지하고, IDE에서 코드의 정적 분석을 허용합니다. 
  • 액션 생성자: 액션 생성과 관련된 로직을 캡슐화하고, 중복을 줄이고, 컴포넌트 안에서 로직을 이동하도록 허용하고, API 역할을 합니다.
Part 1에서 언급했듯이 Redux의 많은 규칙은 원래 Flux Architecture 개념에서 상속되며
Flux 문서의 Flux Actions 및 Dispatcher 섹션에서 이러한 아이디어를 설명합니다. (Flux Actions and the Dispatcher)
이 시리즈의 이전 게시물인 Idiomatic Redux: 왜 Action Creators를 사용합니까?(Idiomatic Redux: Why Use Action Creators?)에서도
Action Creator를 사용하는 것이 좋은 방법인 이유에 대해 자세히 설명합니다.
작년에 Dan이 "액션 생성자는 필요하지 않음"에 대한 기사에 응답한 흥미로운 Twitter 스레드(Dan responded to an article about "no action creators needed")도 있었습니다.
Redux 문서에 액션 생성자를 포함하지 않는 것이 더 나은지 궁금합니다.
사람들은 어쨌든 그것들을 생각해 내더라도, Redux를 비난하지는 않을 것입니다. 

액션과 리듀서의 분리는 Redux가 존재하는 이유입니다.
Read You Might Not Need Redux를 읽어보세요.

...
우리는 사람들이 "디스패칭" 또는 "액션 생성자"를 두렵게 볼 만큼 충분히 설명하지 못했습니다.
당신을 비난하려는 것은 아닙니다. 책임은 우리에게 있습니다.
그것은 우리가 라이브러리와 개념을 문서화하고 설명하지 못했다는 것을 의미합니다.

"디스패칭"은 Redux의 유일한 기능입니다.
사람들이 그것을 숨기려 한다는 것은 우리가 Redux가 존재하는 이유를 잘 설명하지 못했다는 것을 의미합니다.
리듀서와 액션 생성자를 마치 액션이 "로컬"인 것처럼 통합하는 수백 개의 라이브러리에서도 알 수 있습니다.
내 생각에 이 모든 것의 기저에는 Redux를 언제 사용해야 하는지에 대한 기본적인 오해가 있으며 이는 우리의 잘못입니다.
일반 객체 액션은 Redux의 핵심 설계 결정입니다.
액션 상수와 액션 생성자의 사용은 개발자의 몫이지만,
둘 다 캡슐화와 같은 좋은 소프트웨어 엔지니어링 원칙과
항상 인기 있는 "자신을 반복하지 말라(Don't Repeat Yourself)" 만트라에서 파생됩니다.

별도의 파일에 액션, 액션 생성자, 리듀서 정의하기

파일/폴더 구조에 대한 Redux FAQ에서 언급했듯이 (Redux FAQ on file/folder structure)
Redux 자체는 파일을 구성하는 방법을 전혀 신경 쓰지 않습니다.
그것은 전적으로 당신에게 달려 있으며, 당신의 프로젝트에 가장 잘 맞는 것은 무엇이든 자유롭게 해야 합니다.
다른 파일에 다른 타입의 코드를 정의하는 것은 개발자가 따라야 하는 자연스러운 패턴입니다.
 
최소한 Redux 관련 코드를 React 컴포넌트와 별도의 파일에 작성하고 싶을 것입니다.
리듀서는 고유한 것이므로 하나의 파일에 넣는 것이 합리적입니다.
 
프로젝트에서 액션 생성자를 사용하기로 선택한 경우,
이는 다른 종류이므로 두 번째 파일로 이동합니다.
액션 생성자 파일과 리듀서 파일 모두에서 동일한 액션 타입 문자열을 사용해야 하므로
두 위치에서 가져올 수 있는 별도의 파일로 추출하는 것이 좋습니다.
 
따라서 이 접근 방식은 어떤 식으로든 필수 사항은 아니지만 따라야 할 매우 자연스럽고 합리적인 접근 방식입니다.
커뮤니티에서 정의한 "ducks" 패턴(community-defined "ducks" pattern)은
일반적으로 일종의 도메인 개념 및 논리를 나타내는 액션 생성자, 상수 및 리듀서를 하나의 파일에 함께 넣는 것을 제안합니다.
다시 말하지만 Redux 자체는 당신이 그렇게 해도 상관하지 않으며
기능을 업데이트할 때 건드려야 하는 파일 수를 최소화하는 이점이 있습니다.

제 생각에 "ducks" 패턴에는 몇 가지 개념적 단점이 있습니다.

하나는 동일한 액션에 독립적으로 응답하는 여러 슬라이스 리듀서의 개념에서 벗어나도록 한다는 것입니다.
"ducks" 패턴은 여러 리듀서가 응답하는 것을 방지하지는 않지만,
모든 것을 하나의 파일에 포함하는 것은 자체 포함적이라는 것이며,
시스템의 다른 부분과 상호 작용할 가능성이 낮다는 것을 암시합니다.
(주 : 개발자가 이런 식으로 상호작용성을 낮추며 개발할 가능성이 있음)
 
또한 의존성 체인 및 임포트의 일부 측면이 관련되어 있습니다.
시스템의 다른 부분이 덕스에서 액션 생성자를 가져오려는 경우,
리듀서 로직 또한 암시적으로 임포트할 것입니다.
 
그것이 당신을 괴롭히지 않는다면 자유롭게 "덕스 패턴"를 사용하십시오.

슬라이스 리듀서와 리듀서 합성

이것은 다른 타입의 데이터에 대해 여러 "저장소"를 갖는 원래 Flux 아이디어를 기반으로 합니다.
또한 Flux에서 각 Store는 Dispatcher에 등록되었으며
액션이 전달될 때마다 Dispatcher는 각 Store를 반복하여 해당 액션에 응답할 기회를 제공합니다.

Redux가 다양한 데이터를 하나의 트리에 저장하기를 원하는 경우,

간단한 접근 방식은 상태 트리의 데이터 카테고리별로 최상위 키 또는 "슬라이스"를 사용하는 것입니다.

문제는 해당 상태를 초기화하는 방법과 상태를 업데이트하기 위한 로직을 합성하는 방법입니다.

Redux 문서를 위해 작성한 리듀서 구조화 - 기본 리듀서 구조 섹션에서 논의했듯이
전체 애플리케이션에는 실제로 하나의 단일 리듀서 함수,
즉 첫 번째 인수로 createStore에 전달한 함수만 있다는 것을 이해하는 것이 중요합니다.
그 루트 리듀서는 (state, action) => newState 서명이 필요한 유일한 것입니다.
하나의 함수가 상태 트리의 모든 조각을 초기화하고 업데이트된 상태로 유지하는 직접적인 책임을 지도록 하는 것은 전적으로 가능하지만, 이는 다시 기본 소프트웨어 엔지니어링 원칙으로 이어집니다.
더 나은 유지 관리를 위해 코드의 큰 섹션을 더 작은 섹션으로 분할합니다. .
이 패턴은 Flux에서 Redux로의 변환의 직접적인 결과이며
절대적으로 권장되고 관용적인 접근 방식입니다.

Switch 구문

어떤 이유에서인지 이것은 많은 사람들이 Redux 코드에서 가장 싫어하는 측면 중 하나이며 아직 그 이유를 파악하지 못했습니다.
리듀서는 액션을 검사하고 일종의 조건부 논리를 사용하여 관심 있는 항목인지 여부를 결정해야 합니다.
if/else 문을 사용할 수 있지만 단일 필드를 조사할땐 별로입니다.
 
switch 문은 단일 필드에 대한 가능한 값에 초점을 맞춘 단순한 if/else이므로
action.type의 내용을 보는 가장 직접적인 접근 방식입니다.
스위치와 if 문은 의미적으로 동일하며 액션 상수로 키가 지정된 조회 테이블도 마찬가지입니다.
Reducing Boilerplate 문서 페이지(Structuring Reducers - Refactoring Reducers Example)는 다양한 경우를 처리하기 위해
리듀서의 룩업 테이블을 허용하는 함수를 작성하는 방법을 명시적으로 보여주며,
이 개념은 리듀서 구조화 - 리팩터링 예제에서도 논의됩니다.
(스위치의 default 케이스에서 리듀서 로직을 실행하거나
action.type 필드가 아닌 다른 필드를 키로 이용하는 것과 같이
조회 테이블이 잘 처리하지 못하는 몇 가지 구조가 있습니다. Dan은 redux#1024에서 몇 가지 예를 제시했습니다.)
    default: // <------------- how do you express this with a function map?
      const { productId } = action // <------------- predicate by action field, not by action type!
      if (productId) {
        return {
          ...state,
          [productId]: products(state[productId], action) // <------------- or this call?
        }
      }
      return state
  }​
그럼에도 불구하고 어떻게든 Redux 커뮤니티는 이것이 계속해서 재발명되어야 하는 바퀴라고 생각하는 것 같습니다.
작년에 게시한 트윗을 확장하면:
시간 여행자의 게시판에 대한 고전적인 SF 유머 이야기가 있는데,
항상 첫번째 포스터는 히틀러를 죽이기 위해 시간 여행하는 것입니다.
(주 : 모든 시간 여행자는 항상 히틀러 죽이기를 먼저 하게 된다는 것)

Redux에 상응하는 것은 다음과 같습니다 :
모든 신규 사용자는 리듀서를 룩업 테이블 조회 함수로 작성합니다.
switch 문은 괜찮습니다. if/else 문은 괜찮습니다. 룩업 테이블도 괜찮습니다. 하나를 선택하고 계속 진행하십시오 :)
흥미로운 참고 사항: Redux가 나온 지 몇 달 후 누군가가 switch 문 사용을 포함하여 React+Redux가 고전적인 Win32 WndProc 기능과 어떻게 유사한지에 대한 기사를 작성했습니다. (React+Redux is kind of like the classic Win32 WndProc function)
흥미로운 비유이며, 좋은 기사입니다.
HN 코멘트에서 좋은 토론이 있었습니다.(actually some good discussion in the HN comments)

비동기 로직을 ​​위한 미들웨어

이것은 기존 문서 및 게시물에서 자세히 다룬 또 다른 주제입니다.
특히 비동기 로직에 대한 Redux FAQ와(Redux FAQ question on async logic)
비동기 동작에 미들웨어가 사용되는 이유와 타임아웃과 같은 비동기 로직을 ​​처리하는 방법에 대한
Dan Abramov의 두 스택 오버플로 답변이 상황을 잘 설명합니다.
(why middleware are used for async behavior)  (how to handle async logic like timeouts)
아이디어 요약: 비동기 논리를 컴포넌트에 직접 넣을 수 있습니다.
그러나 재사용성을 위해 컴포넌트에서 해당 로직을 별도의 함수로 추출하고 싶을 것입니다.
connected된 컴포넌트는 잠재적으로 디스패치에 액세스할 수 있지만 mapStateToProps를 통해 추출된 상태에만 액세스할 수 있습니다.
개별 함수는 스토어를 직접 가져오고 참조하는 경우에만 dispatch 또는 getState에 액세스할 수 있습니다.
즉, 함수로 추출된 비동기 논리는 저장소와 상호 작용할 수 있는 방법이 필요합니다.
1부에서 논의한 바와 같이 Flummox와 같은 일부 Flux 라이브러리에는 다양한 형태의 비동기 처리가 내장되어 있지만
해당 라이브러리 내에 이미 포함된 것으로 제한되었습니다.
 
Redux의 경우 미들웨어는 명시적으로 Redux 위에 모든 종류의 비동기 로직을 추가하는
커스터마이징 방법으로 의도되었습니다.

미들웨어는 디스패치 주변의 파이프라인을 형성하고

해당 파이프라인을 통해 들어오는 모든 것을 수정/차단/상호작용할 수 있고

미들웨어에 저장소의 dispatch 및 getState 메서드에 대한 참조가 제공되기 때문에

비동기 동작이 발생할 수 있지만 여전히 스토어와 상호 작용하는 지점을 형성합니다.


Thunks, Sagas, and Observables, Oh My!

Redux에는 부작용을 관리하기 위한 수십 개의 기존 라이브러리가 있습니다.
그것은 자바스크립트에서 비동기 로직을 ​​작성하고 관리하는 방법이 많고, 그렇게 하는 방법에 대한 선호도와 아이디어가 모두 다르기 때문입니다. 앞서 언급했듯이 Redux 앱에서 비동기 로직을 ​​사용하기 위해 미들웨어가 필요하지는 않지만 권장되는 관용적 접근 방식입니다.
거기에서 어떤 사용 사례가 있고 비동기 논리를 작성하기 위한 기본 설정이 무엇인지에 대한 질문입니다.
부작용 접근법은 일반적으로 다섯 가지 범주로 분류할 수 있습니다.
Redux 애드온 카탈로그의 관련 섹션에 링크하고 각 카테고리에서 가장 인기 있는 라이브러리를 공유하겠습다.
 
  • Functions : 실행하려는 비동기 논리에 대한 일반 도구로 일반 함수을 사용합니다.
    • redux-thunk : 단순히 함수를 디스패치에 전달한 다음 호출되고 디스패치 및 getState를 인수로 제공하는 것을 허용합니다. 썽크는 Redux의 부작용에 대한 "최소 실행 가능한 접근 방식"으로 간주되며 복잡한 동기 논리 및 간단한 비동기 동작(예: fire-and-forget AJAX 호출)에 가장 유용합니다.
  • Promises: 일반적으로 Promise가 resolved되거나 rejected될 때 액션을 전달하기 위해 디스패치에 대한 인수로 Promise를 사용합니다.
  • Generators : 비동기 흐름을 제어하기 위해 ES6 생성기 함수를 사용합니다.
    • redux-saga는 백그라운드 스레드와 유사한 "saga" 기능을 통해 복잡한 비동기 워크플로를 가능하게 하는 강력한 Redux 지향 흐름 제어 라이브러리입니다.
  • Observables : Observables: 비동기 로직의 파이프라인을 생성하기 위해 RxJS와 같은 Observable/Functional Reactive Programming 라이브러리를 사용합니다. 인기 있는 선택: redux-observable 및 redux-logic, 둘 다 RxJS를 기반으로 하지만 RxJS 및 Redux와 상호 작용하기 위해 서로 다른 API를 제공합니다.
  • Other : 비동기 동작에 대한 다양한 접근 방식
    • redux-loop : Elm 언어가 작동하는 방식과 유사하게 리듀서가 부작용에 대한 설명을 반환할 수 있도록 합니다.
게시물의 많은 Redux 부작용 라이브러리에 대한 비교가 특히 좋습니다.
 
이 모든 것은 Redux에서 부작용을 관리하기 위한 실행 가능한 도구이자 접근 방식입니다.
이 시점에서 가장 인기 있는 두 가지는 썽크(thunks)와 사가(saga)이지만 실제로 무엇을 사용할 것인지 결정하는 것은 사용자의 몫입니다. (저는 각자의 가치가 있기 때문에 thunks와 sagas를 사용하며 다른 것은 사용하지 않습니다.)

3-Phase 비동기 작업 디스패치

 
REQUEST_START, REQUEST_SUCCEEDED 및 REQUEST_FAILED와 같은 AJAX 요청을 수행하는 동안
여러 액션을 전달하는 Redux 애플리케이션을 보는 것은 매우 일반적입니다.
이것은 명시적으로 추적된 상태(Redux로 이어짐) 측면에서 가능한 한 많이 설명하는 것에 대한 React 세계의 강조와 관련이 있습니다.
 
일종의 "로드 중..." 스피너를 표시하려면 React 앱에서 $("#loadingSpinner").toggle()을 호출하면 안 됩니다.
대신 어떤 종류의 상태를 업데이트하고 이를 사용하여 스피너를 표시할 필요가 있는지 결정해야 합니다.
(주 : 상태 > 여부)
 
마찬가지로 Redux를 사용하면서 AJAX 요청을 할 때 이와 같은 액션을 전달할 필요가 없지만
이러한 액션과 해당 상태 값을 갖는 것은 UI 또는 애플리케이션의 다른 측면을 업데이트하는 데 유용할 수 있습니다.

데이터 정규화

나는 Redux 문서의 Structuring Reducers - Normalizing State Shape 섹션(Structuring Reducers - Normalizing State Shape)에서 정규화의 주요 이유와 이점을 다루었습니다.
가장 관련성이 높은 섹션을 여기에 붙여넣겠습니다.
정규화된 상태 모양은 다음과 같은 여러 면에서 중첩된 상태 구조를 개선한 것입니다.
  • 각 항목은 한 곳에서만 정의되기 때문에 해당 항목이 업데이트되면 여러 곳에서 변경을 시도할 필요가 없습니다.
  • 리듀서 로직은 깊은 수준의 중첩을 처리할 필요가 없으므로 훨씬 더 간단할 것입니다.
  • 주어진 항목을 검색하거나 업데이트하기 위한 논리가 이제 상당히 간단하고 일관성이 있습니다. 항목의 타입과 ID가 주어지면 찾기 위해 다른 개체를 파헤칠 필요 없이 몇 가지 간단한 단계를 통해 직접 검색할 수 있습니다.
  • 각 데이터 타입이 분리되어 있기 때문에 주석 텍스트를 변경하는 것과 같은 업데이트는 트리의 "comments > byId > comment" 부분의 새 복사본만 필요합니다. 이는 일반적으로 데이터가 변경되었기 때문에 업데이트해야 하는 UI 부분이 더 적음을 의미합니다. 대조적으로, 원래의 중첩된 모양에서 코멘트를 업데이트하려면 코멘트 개체, 상위 게시물 개체, 모든 게시물 개체의 배열을 업데이트해야 했으며 자기 자신 뿐만 아니라, UI의 모든 Post 컴포넌트 Comment 컴포넌트가 다시 렌더링되도록 할 수 있습니다.
정규화된 상태 구조는 일반적으로 더 많은 컴포넌트가 연결되어 있고
각 컴포넌트가 자체 데이터를 조회하는 책임이 있다는 것을 의미합니
다.
 
연결된 부모 컴포넌트가 연결된 자식에게 단순히 항목 ID만 전달하는 것은
React Redux 애플리케이션에서 UI 성능을 최적화하는 좋은 패턴이므로
상태를 정규화하여 유지하는 것이 성능 향상에 중요한 역할을 합니다.
 
(내 포스트 Practical Redux, Part 6: Connected Lists, Forms, and Performance에서
정규화가 성능에 얼마나 중요한지에 대해 더 자세히 이야기했습니다.)

셀렉터 함수

const item = state.a.b.c.d와 같이 상태 트리의 중첩된 부분에 직접 액세스하는 코드를 작성하는 것은 가능합니다.
그러나 추상화 및 캡슐화의 표준 소프트웨어 엔지니어링 원칙이 다시 한 번 작동합니다.
 
특정 필드를 찾는 것이 복잡할 수 있는 경우 해당 작업을 함수로 캡슐화하는 것이 좋습니다.
애플리케이션의 다른 부분이 상태의 정확한 구조나 특정 데이터 조각을 찾을 위치와 관련해서는 안 되는 경우
해당 조회 프로세스를 함수로 캡슐화하는 것이 좋습니다.
따라서 "셀렉터 함수"는 상태 트리의 일부를 읽고 일부 하위 집합 또는 파생 데이터를 반환하는 단순히 함수입니다.

Redux에서 셀렉터 함수는 원래 NuclearJS의 "게터(getters)" 아이디어에서 영감을 받았습니다.
특정 상태 키의 변경 사항을 구독하고 데이터를 파생할 수 있게 해주었습니다.
비슷한 접근 방식이 Redux 개발 중에 제안되었으며 개념은 Reselect 라이브러리로 바뀌었습니다.

셀렉터 함수는 단순히 일반 함수일 수 있지만 Reselect는 여러 다른 선택기 기능을 입력으로 쉽게 사용할 수 있는 셀렉터 함수를 생성해
입력이 변경될 때만 출력 선택기가 실행되도록 메모합니다.
 
이것은 두 가지 면에서 성능에 중요한 요소입니다.
  • 첫째, 출력 선택기에서 값비싼 필터링 또는 기타 유사한 작업은 필요하지 않는 한 다시 실행되지 않습니다.
  • 둘째, 메모화된 선택기는 이전 결과 개체를 반환하기 때문에 connect(그리고 아마도 PureComponent 또는 shouldComponentUpdate)에서 얕은 동등성/참조 검사와 함께 잘 작동할 것입니다.


React-Redux: connect, mapState, and mapDispatch

스토어를 모든 컴포넌트 파일로 직접 임포트하고,
컴포넌트가 스토어를 구독하도록 코드를 작성하고
저장소가 업데이트될 때마다 필요한 데이터를 추출하고 컴포넌트의 다시 렌더링을 트리거할 수 있습니다.
그 과정 중 어느 것도 "마법"이 아닙니다.
 
그러나 이 게시물의 주제 중 하나를 반복하면
Redux 사용자가 반복적인 논리를 직접 작성하는 일을 처리할 필요가 없도록
프로세스를 캡슐화하는 것이 좋은 소프트웨어 엔지니어링입니다.
React-Redux connect 함수는 여러 용도로 사용됩니다.
  • 스토어 구독 과정을 자동으로 처리합니다.
  • 래핑된 컴포넌트가 필요로 하는 상태 조각을 추출하는 논리를 구현합니다.
  • 래핑된 컴포넌트가 필요할 때만 다시 렌더링되도록 합니다.
  • 래핑된 컴포넌트가 실제로 저장소가 존재하는지 또는 해당 소품이 실제로 Redux에서 오는지 알 필요가 없도록 보호합니다.
주 : 훅 아키텍처에서는, 훅을 다른 파일로 분리해서, 해당 파일 내부에서 해당 컴포넌트를 위한 훅을 만들어 컴포넌트에서 임포트하면 됩니다.
mapState 함수는 기본적으로 항상 전체 상태를 하나의 인수로 수신하고,
래핑된 컴포넌트의 자체 프롭을 두 번째 인수로 수신할 수 있으며, 항상 객체를 반환해야 하는 특수 셀렉터 함수입니다.
반환된 객체의 내용은 이름에 따라 래핑된 컴포넌트의 프롭으로 바뀝니다.
 
mapDispatch 함수를 사용하면 스토어의 디스패치 메서드를 삽입할 수 있으므로
컴포넌트에서 액션을 디스패치하는 로직 및 함수를 생성할 수 있습니다.
mapDispatch 함수가 제공되지 않으면 디폴트 동작은 디스패치 자체를 프롭으로 삽입하여 컴포넌트 자체가 액션을 디스패치할 수 있도록 하는 것입니다.
mapDispatch에 의해 반환된 객체는 props로 바뀝니다.
가장 일반적인 사용 사례는 액션 생성자를 래핑하여 반환된 액션이 디스패치로 바로 전달되도록 하는 것이므로
connect는 "객체 약식" 구문을 허용합니다.
즉, 액션 생성자로 가득 찬 객체는 실제 mapDispatch 함수 대신 전달할 수 있습니다.
생각해 보면 connect와 connect가 생성하는 래퍼 컴포넌트는 가벼운 종속성 주입 메커니즘처럼 작동합니다.
(특히 중첩 컴포넌트에서 스토어 참조를 사용할 수 있도록 하기 위한 React의 컨텍스트 메커니즘 사용을 통해).
이렇게 하면 컴포넌트가 특정 스토어 인스턴스에 의존하지 않기 때문에 더 쉽게 테스트할 수 있고,
컴포넌트 내부의 서브 컴포넌트 트리에서 다른 스토어를 사용할 수도 있습니다.

일반적인 패턴 요약

전반적으로 이러한 공통 패턴 및 접근 방식은
일반적으로 핵심 Redux 설계 결정 또는 캡슐화, 중복 제거 및 재사용성과 같은
간단한 소프트웨어 엔지니어링 원칙의 결과로 볼 수 있습니다.
 
일반 액션 객체를 제외하고 이러한 모든 개념은 실제로 선택 사항이며
사용하지 않으려면 사용하지 않아도 되지만 존재하는 데에는 그럴만한 이유가 있습니다.

철학과 다양한 활용 방법

다양한 Redux 관련 코드를 살펴보았습니다.
수많은 라이브러리, 애플리케이션 코드, 자습서 및 기사를 읽었으며 매우 다양한 스타일, 접근 방식 및 구현을 보았습니다.
이를 기반으로, 그리고 Redux 메인테이너로서의 나의 지위에 근거해
나는 Redux 전문가라고 말하는 것이 타당하다고 생각합니다.
 
그것은 또한 서론에서 말했듯이 "좋은 Redux 코드"가 어떻게 생겼는지,
그리고 무엇이 "관용적인 Redux "코드"인지에 대한 의견이 있다는 것을 의미합니다.
이제 제가 말하고픈 부분에 도달했습니다.
따라서 이 마지막 섹션에서는 Redux를 사용하는 방법의 여러 변형을 살펴보고 이러한 것들이
Redux의 정신에 부합하는지 여부에 대한 제 생각을 제시하겠습니다.

독립적인 슬라이스 리듀서 vs 한번에 전부 업데이트

앞서 논의한 바와 같이, 기본 의도된 리듀서 구조는 함께 합쳐지는 슬라이스 리듀서입니다.
그러나 리듀서는 함수일 뿐이므로 리듀서 로직을 작성하고 합성하는 방법은 무한히 다양합니다.
나는 슬라이스 리듀서 간의 데이터 공유, 시퀀싱 종속성, 기존 리듀서를 추가 기능으로 래핑하는
고차 리듀서 사용에 대한 몇 가지 예를 포함하여
리듀서 구조화 - CombineReducers를 넘어서(Structuring Reducers - Beyond combineReducers)
문서 섹션에서 몇 가지 대체 접근 방식에 대해 논의했습니다.
 
내가 본 한 가지 불만은 동일한 액션에 응답하는 별도의 슬라이스 리듀서가 많으면
해당 액션에 대해 실제로 업데이트되는 내용을 파악하기가 더 어렵다는 것입니다.
Redux가 출시된 직후에 작성된 Problems with Flux 기사가 이에 대한 예입니다.
개발자로서 모든 상태 업데이트 액션 트리거에 대한 개요를 알고 싶습니다.
내 코드의 한 곳에서. 한 줄씩. 코드의 어딘가에 포괄적인 요약이 있으면
특히 쓰기 액션의 순서와 관련된 경우 상태 업데이트를 올바르게 얻을 가능성이 극대화됩니다!

현재 Flux는 잘못된 축에서 스토어 / 액션 관계 매트릭스를 분할한다고 생각합니다.
상태의 한 부분에 영향을 미치는 모든 액션을 쉽게 볼 수 있습니다.
그러나 하나의 액션이 영향을 미치는 모든 상태를 파악하기는 어렵습니다.

제 생각에는 그 반대여야 합니다.
무슨 말을 하는지 이해하며, 어느 정도 맞는 말이라고도 생각합니다.
하지만, 액션 상수를 사용하면, 코드베이스에서 IDE의 "Find All Usages"를 통해 당신이 원하는 것을 정말 쉽게 할 수 있습니다.

나는 합성 슬라이스 리듀서가 여전히 더 관용적이라고 말하고 싶습니다만,
액션을 중심으로 리듀서 로직을 구성하고 싶다면 괜찮습니다. 당신이 하고싶은 대로 할 수 있습니다.

액션 문법

이것은 이런 종류의 일에 깊이 관심을 가진 사람들(특히 Event Sourcing 및 CQRS와 같은 관련 개념에 대한 경험이 있는 사람들)로부터 매우 길고, 매우 복잡하고, 매우 현학적인 토론으로 이어진 주제입니다.
사실 이 분야에 대한 의견이 많지는 않지만 논의의 일부 영역을 강조하려고 노력할 것입니다.
 

액션 시제와 "세터" vs "이벤트"

Redux 동작 상수를 작성할 때 어떤 동사 시제를 사용해야 하는지에 대한 상당한 논쟁이 있습니다.
"LOADED_THING"과 같은 과거형은 "여기에 일이 발생했습니다. 어떻게 대응하시겠습니까?"라고 말하는 것으로 볼 수 있습니다. "LOAD_THING"과 같은 현재 시제는 "go do this"와 같은 명령으로 볼 수 있습니다.
이것은 분명히 내가 막연하게만 알고 있는 Event Sourcing과 CQRS 간의 차이점과 관련이 있습니다.


관련 예로서, 피자와 탄산음료 한 병을 제공하는 피자 콤보를 구입하는 아이디어를 생각해 보십시오.
"PIZZA_BOUGHT"를 디스패치하는 것이 더 낫습니까,
아니면 "PIZZA_INCREMENT" 및 "SODA_INCREMENT"를 디스패치하는 것이 더 낫습니까?
썽크 문제에 대한 트위터 스레드에서 Dan Abramov는 다음과 같이 말했습니다. (Dan Abramov said)
실제로 "영광스러운 세터"는 Redux의 매우 흔한 오용입니다.
액션 생성자 이름이 set*으로 시작하고 여러 번 연속으로 호출하는 경우 Redux를 사용하는 요점을 놓치고 있을 수 있습니다.

요점은 "상태가 어떻게 변경되는지"에서 "발생한 일"을 분리하는 것입니다.
"세터" 액션 생성자는 목적을 무너뜨리고 두 가지를 융합합니다.
(주 : PIZZA_BOUGHT가 낫다는 의미입니다.)
나는 Dan의 요점을 아해하고, 그는 틀리지 않았지만 여기에는 회색 영역(명확하지 않은 부분)이 많이 있습니다.
한편으로는 원하는 내용이 포함된 액션을 전달할 수 있으며,
만약 리듀서 로직의 어떤 부분도 이에 대해 신경 쓰지 않으면 상태가 업데이트되지 않습니다.
반면에 일반적으로 액션을 형식화/배포하는 코드와 해당 액션에 특별히 관심이 있는 리듀서 로직의 일부 사이에는 암시적 계약이 있습니다. 액션의 형식이 올바르게 지정되지 않은 경우 해당 특정 액션 타입을 찾는 리듀서는 이를 무시하거나 존재하지 않는 필드에 액세스하여 중단합니다.
그런 의미에서 Redux는 다른 이벤트 기반 / pub-sub 스타일 시스템과 같습니다.
이벤트를 트리거하거나 액션을 전달하는 것은 "원격 함수 호출"과 같은 역할을 하며,
다른 쪽 끝에 있는 코드에 올바른 매개변수를 제공하지 않으면 작동하지 않습니다.
일반적으로 리듀서 논리의 특정 청크가 특정 액션에 관심이 있을 것으로 예상됩니다.
예를 들어, 상태에 listA, listB, listC가 있고 각각이 동일한 슬라이스 리듀서 함수의 복사본에 의해 관리되는 경우 "LIST_ITEM_INSERT"를 전달하면 적용해야 하는 목록을 구별하기 위해 일종의 추가 정보가 필요합니다. 
대신 "b/LIST_ITEM_INSERT" 타입을 디스패치하거나 액션에 listId : "b"를 추가합니다.
우리는 종종 "스토어을 클라이언트 측 데이터베이스처럼 취급한다"는 생각을 잊어버리며,
확실히 SQL 업데이트 쿼리는 일반적으로 업데이트해야 하는 행에 대한 세부 정보를 제공합니다.
궁극적으로 여기에 절대적인 "정답"이 있다고 생각하지 않습니다.
이 주제는 정말 바이크셰딩에 빠지기 쉬운 주제라고 생각하고, 그 시간을 유용한 무언가를 만드는 데 보내고 싶습니다. :)

다회 디스패치

내가 작업한 몇 가지 응용 프로그램에서 일반적으로 더 큰 시퀀스의 일부로 전달되는 액션을 여러 개 조합하는 것을 발견했습니다.
Practical Redux, Part 8: Form Draft Data Management에서 "파일럿 편집을 중지하고 변경 사항을 저장"하는 논리에는
"PILOT_EDIT_STOP" 액션이 있었습니다.
이 액션은 플래그 재설정 액션, "항목 타입과 ID를 이용해 초안에서 현재로 변경 사항 적용" 액션,
"항목 타입과 ID를 이용해 초안에서 삭제" 액션으로 나누어 질 수 있었습니다.
한 번의 액션으로 세 단계를 모두 처리할 수 있지만 논리를 조합할 때 기본 요소를 먼저 빌드한 다음 함께 구성하는 것이 더 합리적이었습니다.
 
특히 "초안 데이터" 동작을 여러 번 반복해야 했기 때문에 어떤 종류의 항목.
결국, 나는 이러한 우려에 대해 걱정하지 않습니다.
성능이 문제인 경우 다양한 일괄 디스패치 접근 방식 중 하나로 처리할 수 있습니다.
액션으로 인해 일부 "불안정한" 상태가 발생할 수 있는 경우 어떻게 처리되고 있는지 다시 생각하고 싶을 것입니다.
(또는 동일한 일괄 처리 개념을 적용하여 "트랜잭션"을 구현)

다양한 리듀서

커서 및 "올인원" 리듀서
"커서"는 (대략) 특정 중첩 데이터 조각에 대한 읽기/쓰기 액세스를 제공하는 함수입니다.
예를 들어, 개념적인 커서 라이브러리가 있고 const nestedCursor = cursor(state).refine(["a", "b", "c"])과 같은 커서를 만든 경우
반환된 커서 개체를 사용하면 상태를 조작할 수 있습니다. state.a.b.c 중간 레이어에 대해 스스로 걱정할 필요가 없습니다.
많은 사람들이 실제로 커서 라이브러리를 통해 직접
또는 액션에서 상태 키 경로와 새 값을 지정할 수 있도록 하는 리듀서를 통해 Redux 상태 업데이트에 이 접근 방식을 적용하려고 했습니다.
 
나는 또한 사람들이 "단순한 리듀서"를 작성하려고 하는 것을 보았습니다.
여기에서 전체 응용 프로그램의 유일한 액션은 "SET_DATA" 뿐이고
유일한 리듀서의 기능은 단순히 {...state, ...action.payload}를 반환하는 것입니다.
 
Redux가 커서와 어떻게 관련되는지에 대한 토론이 있었습니다. (redux#155)
Dan은 그 스레드에서 몇 가지 훌륭한 의견을 제시했으며 실제로 이러한 접근 방식을 시도하는 사람들을 예상했습니다.
여기에서 그의 의견 중 일부를 인용하겠습니다.
먼저 Redux가 쓰기 커서를 피하는 이유를 설명합니다. (he describes why Redux avoids write cursors)
Redux 위에 읽기 전용 커서를 매우 쉽게 구현할 수 있습니다.
리스너와 셀렉터입니다.

Redux가 제공하지 않는 것은 쓰기 커서입니다. 이것은 이유가 있는 핵심 설계 결정입니다.
Redux를 사용하면 합성을 사용하여 상태를 관리할 수 있습니다.
데이터는 해당 데이터를 관리하는 리듀서(현재 문서의 "저장소") 없이는 절대 존재하지 않습니다.
이렇게 하면 데이터가 잘못된 경우 누가 변경했는지 항상 추적할 수 있습니다.
또한 어떤 액션이 데이터를 변경했는지 항상 추적할 수도 있습니다.

쓰기 커서를 사용하면 그러한 보장이 없습니다.
코드의 많은 부분이 커서를 통해 동일한 경로를 참조하고 원하는 경우 업데이트할 수 있습니다.
나중에 그는 "SET_DATA" 스타일 리듀서에 대한 아이디어를 다루었습니다. (he addressed the idea of "SET_DATA"-style reducers)
Q : 그것을 철학적/패턴적 입장으로 바라보시나요? 아니면 기술적 입장으로 바라보시나요?
다시 말해, Redux가 쓰기 커서를 패턴적으로 거부하는 것인가요?
개발자를 올바른 길로 인도하기 위한 것인가요?

예를 들어 경로를 매개변수로 사용하는 단일 액션 세트를 만들고
이를 컴포넌트에 전달했다면 쓰기 커서와 동일한 기능을 구현한 걸까요?
아니면 여전히 커서와 근본적으로 다를까요?
A : 좋은 질문입니다! 당신은 그렇게 할 수 있습니다.
제 뜻은 커서가 매우 낮은 수준의 API라는 것입니다.

전체 애플리케이션에 대해 단일 React 컴포넌트를 가질 수 있는 것처럼
커서와 유사하게 동작하는 단일 SET 액션과 단일 리듀서를 가질 수 있습니다.
내 경험상 매우 실용적이지는 않지만 확실히 할 수 있습니다.
이 두 가지(커서, 올인원 리듀서)는 모두 Redux에서 확실히 가능한 일이지만
"시스템을 통해 추적할 수 있는 의미 있는 액션"의 의도된 정신에 어긋납니다.

뚱뚱하고 날씬한 리듀서

액션 생성자에 더 많은 로직을 배치하는 것과 리듀서에 더 많은 로직을 배치하는 것에는 유효한 절충점이 있습니다.
내가 최근에 본 한 가지 좋은 점은 리듀서에 더 많은 로직이 있다면 시간 여행 디버깅(일반적으로 좋은 일임)인 경우 다시 실행할 수 있는 것들이 더 많다는 것을 의미한다는 것입니다.
저는 개인적으로 논리를 두 곳에 동시에 두는 경향이 있습니다.
나는 액션이 ​​디스패치되어야 하는지, 그리고 디스패치되어야 한다면 어떤 내용이어야 하는지 결정하는 데 액션 생성자를 씁니다.
액션의 내용을 보고 그에 대한 응답으로 몇 가지 복잡한 상태 업데이트를 수행하는 리듀서를 작성합니다.

나는 일반적으로 {...state, ...action.payload}를 반환하는 장소의 수를 최소화하려고 노력합니다.

양식에서 가능한 여러 필드를 업데이트하는 것과 같은 액션을 수행하고 각 필드에 대해 별도의 updateName / updateAge / updateWhatever 핸들러를 작성하지 않으려는 경우 이 접근 방식이 확실히 도움이 됩니다.

어느쪽이나 괜찮지만, 에러가 발생할 확률을 줄일 수 있습니다.


액션 안에 리듀서 넣기

리듀서를 액션 안에 넣는 몇 가지 경우를 보았습니다.
액션을 전달하는 코드에는 리듀서 또는 상태 업데이트 기능이 포함되어 있으며
루트 리듀서는 단순히 return action.reducer(state)를 호출합니다.
이것은 몇 가지 이유로 나쁜 생각처럼 보입니다.
커서와 같은 방식으로 추적성을 잃을 뿐만 아니라 함수가 제대로 직렬화되지 않기 때문에 시간 여행 디버깅도 중단됩니다.

객체지향 vs 함수형

두 게시물에서 논의된 것처럼 Redux의 핵심 가치는 다음과 같습니다.
  • 플레인 액션 객체 디스패치
  • 액션 및 상태는 시간 여행 및 영속성을 위해 직렬화할 수 있어야 합니다.
  • 순수 함수와 일반 데이터를 사용합니다.
  • 어디에도 클래스가 필요하지 않습니다.
  • 리듀서는 동일한 액션을 독립적으로 듣고 적절하게 대응할 수 있어야 합니다.
함수형 프로그래밍은 OOP에 대한 경험만 있는 많은 프로그래머에게 적응하기 힘든 개념일 수 있습니다.
(나 자신은 여전히 ​​그 스펙트럼의 중간 어딘가에 있습니다. FP의 기본 사용에는 문제가 없지만 높은 수준의 FP 사용은 여전히 ​​대부분 제 머리 위에 있으며, 여전히 더 편안하다고 느끼는 OOP의 측면이 있습니다.)

그 결과 일부 사람들은 Redux 위에 다양한 OOP 계층을 구축하려고 했습니다.
여기에는 Radical, React-Redux-OOP, Tango, Conventional-Redux 및 기타 여러 라이브러리가 포함됩니다.
이러한 라이브러리는 유사한 패턴을 따르는 경향이 있습니다.
  • 메서드가 액션 생성자 및 리듀서인 클래스 정의
  • Redux 저장소에 삽입할 도메인 모델
  • 상태 값을 래핑하는 "상태 클래스"
  • 일반 액션 개체 대신 "함수 전달"과 같은 패턴입니다.
이러한 라이브러리는 일반적으로 대부분 OOP 방식으로 작업을 수행하기 위해 Redux 동작의 측면을 감싸고 숨깁니다.
공통적인 테마는 하나의 액션에 응답하는 많은 리듀서가 Redux의 핵심 개발 의도임에도 불구하고,
이러한 라이브러리가 디스패치된 액션과 리듀서 간의 1:1 관계만 지원한다는 것입니다.
 
"Thinking in React"에 대한 몇 가지 원칙을 제시하는 Reactive, Component-Based UIs라는 훌륭한 슬라이드쇼가 있으며
이러한 원칙은 Redux에도 완전히 적용된다고 생각합니다.
슬라이드 3 인용:
즉, 오른쪽에도 가치가 있으나, 우리는 왼쪽에 좀 더 가치를 둡니다.
- 함수형 > OO
- 무상태 > 상태
- 명확성 > 짧음
모든 프레임워크나 라이브러리에는 코드가 작성되고 구조화되는 방식에 대한 특정 관용구와 기대치가 있다는 점에 유의할 가치가 있습니다. 이러한 관용구에 어긋나는 코드를 작성하기 시작하면 접근 방식에 익숙해지는 사람이 줄어들고 관심을 끌 가능성도 낮아집니다.
나는 특정 OOP Redux 래퍼 라이브러리가 주목받지 못하는 이유에 대해 얼마 전에 HN 코멘트를 썼고 해당 주제에 대해 더 자세히 설명했습니다.
전반적으로 이러한 OOP 래퍼 패턴은 기술 수준에서 잘 동작할 수 있지만, 확실히 Redux의 의도와 정신에 어긋납니다.


생각 정리

과학자들이 원숭이에게 물을 뿌려 사다리를 오르지 말라고 가르쳤고,
나중에 나이 든 원숭이가 사다리를 오르려고 하는 새로운 원숭이를 때린 실험에 대한 외설적인 이야기가 있습니다.
검색은 이야기가 가짜라는 것을 암시하지만 우리는 해당 이야기가 무슨 말을 하려는지 이해할 수 있습니다.
(그렇게 행동하는 근본적인 이유를 이해하지 못하기 때문입니다.)
 
Redux에 대한 많은 사람들의 불만이 이런 종류의 범주를 따른다고 말하고 싶습니다.
그들은 "액션 생성자"와 "불변성", 컨테이너라는 이름의 폴더와 수십 가지의 다양한 부작용 미들웨어를 보았고
Redux 사용과 관련된 상용구에 대해 불평하고 "애초에 이 항목이 필요한 이유는 무엇입니까? 라는 질문을 합니다.
 
글쎄요, Redux의 역사와 그것이 어떻게 사용되도록 의도되었는지를 살펴본 다음
더 큰 응용 프로그램에서 사용하기 위해 꽤 간단한 소프트웨어 엔지니어링 원칙을 적용하면
우리가 알고 있는 일반적인 패턴과 관행을 갖게 됩니다.
  • 플레인 액션 객체
  • 액션 생성자
  • 슬라이스 리듀서
  • 스위치 문
  • 비동기 미들웨어
  • connected component
  • 셀렉터
  • 정규화된 상태
이러한 부분이 실제로 어떻게 서로 맞물리는지에 대한 좋은 예는
대규모 앱의 상태 관리에 대한 Redux의 Mapbox의 최근 기사(Redux for state management in large apps)를 확인하십시오.


이 게시물, Redux FAQ, Reddit 및 HN 및 기타 위치에 대한 의견에서 말했듯이
궁극적으로 애플리케이션과 코드베이스입니다.
이러한 패턴이 마음에 들지 않으면 사용할 필요가 없습니다.
Redux는 다른 "의도하지 않은" 방향으로 비틀 수 있을 만큼 간단하고 유연합니다.
그러나 Chesterton의 울타리처럼 최소한 이러한 관용적 사용 패턴은 정당한 이유가 있음을 이해해야 합니다!
Redux의 역사와 사용법에 대한 이 여정이 유익한 정보가 되었기를 바랍니다.

 

반응형