Redux 문서에 액션 생성자를 포함하지 않는 것이 더 나은지 궁금합니다. 사람들은 어쨌든 그것들을 생각해 내더라도, Redux를 비난하지는 않을 것입니다.
액션과 리듀서의 분리는 Redux가 존재하는 이유입니다. Read You Might Not Need Redux를 읽어보세요.
... 우리는 사람들이 "디스패칭" 또는 "액션 생성자"를 두렵게 볼 만큼 충분히 설명하지 못했습니다. 당신을 비난하려는 것은 아닙니다. 책임은 우리에게 있습니다. 그것은 우리가 라이브러리와 개념을 문서화하고 설명하지 못했다는 것을 의미합니다.
"디스패칭"은 Redux의 유일한 기능입니다. 사람들이 그것을 숨기려 한다는 것은 우리가 Redux가 존재하는 이유를 잘 설명하지 못했다는 것을 의미합니다. 리듀서와 액션 생성자를 마치 액션이 "로컬"인 것처럼 통합하는 수백 개의 라이브러리에서도 알 수 있습니다. 내 생각에 이 모든 것의 기저에는 Redux를 언제 사용해야 하는지에 대한 기본적인 오해가 있으며 이는 우리의 잘못입니다.
일반 객체 액션은 Redux의 핵심 설계 결정입니다.
액션 상수와 액션 생성자의 사용은 개발자의 몫이지만,
둘 다 캡슐화와 같은 좋은 소프트웨어 엔지니어링 원칙과
항상 인기 있는 "자신을 반복하지 말라(Don't Repeat Yourself)" 만트라에서 파생됩니다.
조회 테이블이 잘 처리하지 못하는 몇 가지 구조가 있습니다. 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 문은 괜찮습니다. 룩업 테이블도 괜찮습니다. 하나를 선택하고 계속 진행하십시오 :)
connected된 컴포넌트는 잠재적으로 디스패치에 액세스할 수 있지만 mapStateToProps를 통해 추출된 상태에만 액세스할 수 있습니다.
개별 함수는 스토어를 직접 가져오고 참조하는 경우에만 dispatch 또는 getState에 액세스할 수 있습니다.
즉, 함수로 추출된 비동기 논리는 저장소와 상호 작용할 수 있는 방법이 필요합니다.
1부에서 논의한 바와 같이 Flummox와 같은 일부 Flux 라이브러리에는 다양한 형태의 비동기 처리가 내장되어 있지만
해당 라이브러리 내에 이미 포함된 것으로 제한되었습니다.
Redux의 경우 미들웨어는 명시적으로 Redux 위에 모든 종류의 비동기 로직을 추가하는
커스터마이징 방법으로 의도되었습니다.
미들웨어는 디스패치 주변의 파이프라인을 형성하고
해당 파이프라인을 통해 들어오는 모든 것을 수정/차단/상호작용할 수 있고
미들웨어에 저장소의 dispatch 및 getState 메서드에 대한 참조가 제공되기 때문에
비동기 동작이 발생할 수 있지만 여전히 스토어와 상호 작용하는 지점을 형성합니다.
Thunks, Sagas, and Observables, Oh My!
Redux에는 부작용을 관리하기 위한 수십 개의 기존 라이브러리가 있습니다.
그것은 자바스크립트에서 비동기 로직을 작성하고 관리하는 방법이 많고, 그렇게 하는 방법에 대한 선호도와 아이디어가 모두 다르기 때문입니다. 앞서 언급했듯이 Redux 앱에서 비동기 로직을 사용하기 위해 미들웨어가 필요하지는 않지만 권장되는 관용적 접근 방식입니다.
거기에서 어떤 사용 사례가 있고 비동기 논리를 작성하기 위한 기본 설정이 무엇인지에 대한 질문입니다.
부작용 접근법은 일반적으로 다섯 가지 범주로 분류할 수 있습니다.
Redux 애드온 카탈로그의 관련 섹션에 링크하고 각 카테고리에서 가장 인기 있는 라이브러리를 공유하겠습다.
redux-thunk : 단순히 함수를 디스패치에 전달한 다음 호출되고 디스패치 및 getState를 인수로 제공하는 것을 허용합니다. 썽크는 Redux의 부작용에 대한 "최소 실행 가능한 접근 방식"으로 간주되며 복잡한 동기 논리 및 간단한 비동기 동작(예: fire-and-forget AJAX 호출)에 가장 유용합니다.
Promises: 일반적으로 Promise가 resolved되거나 rejected될 때 액션을 전달하기 위해 디스패치에 대한 인수로 Promise를 사용합니다.
redux-saga는 백그라운드 스레드와 유사한 "saga" 기능을 통해 복잡한 비동기 워크플로를 가능하게 하는 강력한 Redux 지향 흐름 제어 라이브러리입니다.
Observables : Observables: 비동기 로직의 파이프라인을 생성하기 위해 RxJS와 같은 Observable/Functional Reactive Programming 라이브러리를 사용합니다. 인기 있는 선택: redux-observable 및 redux-logic, 둘 다 RxJS를 기반으로 하지만 RxJS 및 Redux와 상호 작용하기 위해 서로 다른 API를 제공합니다.
각 항목은 한 곳에서만 정의되기 때문에 해당 항목이 업데이트되면 여러 곳에서 변경을 시도할 필요가 없습니다.
리듀서 로직은 깊은 수준의 중첩을 처리할 필요가 없으므로 훨씬 더 간단할 것입니다.
주어진 항목을 검색하거나 업데이트하기 위한 논리가 이제 상당히 간단하고 일관성이 있습니다. 항목의 타입과 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, andmapDispatch
스토어를 모든 컴포넌트 파일로 직접 임포트하고,
컴포넌트가 스토어를 구독하도록 코드를 작성하고
저장소가 업데이트될 때마다 필요한 데이터를 추출하고 컴포넌트의 다시 렌더링을 트리거할 수 있습니다.
그 과정 중 어느 것도 "마법"이 아닙니다.
그러나 이 게시물의 주제 중 하나를 반복하면
Redux 사용자가 반복적인 논리를 직접 작성하는 일을 처리할 필요가 없도록
프로세스를 캡슐화하는 것이 좋은 소프트웨어 엔지니어링입니다.
React-Redux connect 함수는 여러 용도로 사용됩니다.
스토어 구독 과정을 자동으로 처리합니다.
래핑된 컴포넌트가 필요로 하는 상태 조각을 추출하는 논리를 구현합니다.
래핑된 컴포넌트가 필요할 때만 다시 렌더링되도록 합니다.
래핑된 컴포넌트가 실제로 저장소가 존재하는지 또는 해당 소품이 실제로 Redux에서 오는지 알 필요가 없도록 보호합니다.
주 : 훅 아키텍처에서는, 훅을 다른 파일로 분리해서, 해당 파일 내부에서 해당 컴포넌트를 위한 훅을 만들어 컴포넌트에서 임포트하면 됩니다.
mapState 함수는 기본적으로 항상 전체 상태를 하나의 인수로 수신하고,
래핑된 컴포넌트의 자체 프롭을 두 번째 인수로 수신할 수 있으며, 항상 객체를 반환해야 하는 특수 셀렉터 함수입니다.
반환된 객체의 내용은 이름에 따라 래핑된 컴포넌트의 프롭으로 바뀝니다.
mapDispatch 함수를 사용하면 스토어의 디스패치 메서드를 삽입할 수 있으므로
컴포넌트에서 액션을 디스패치하는 로직 및 함수를 생성할 수 있습니다.
mapDispatch 함수가 제공되지 않으면 디폴트 동작은 디스패치 자체를 프롭으로 삽입하여 컴포넌트 자체가 액션을 디스패치할 수 있도록 하는 것입니다.
mapDispatch에 의해 반환된 객체는 props로 바뀝니다.
가장 일반적인 사용 사례는 액션 생성자를 래핑하여 반환된 액션이 디스패치로 바로 전달되도록 하는 것이므로
connect는 "객체 약식" 구문을 허용합니다.
즉, 액션 생성자로 가득 찬 객체는 실제 mapDispatch 함수 대신 전달할 수 있습니다.
생각해 보면 connect와 connect가 생성하는 래퍼 컴포넌트는 가벼운 종속성 주입 메커니즘처럼 작동합니다.
(특히 중첩 컴포넌트에서 스토어 참조를 사용할 수 있도록 하기 위한 React의 컨텍스트 메커니즘 사용을 통해).
이렇게 하면 컴포넌트가 특정 스토어 인스턴스에 의존하지 않기 때문에 더 쉽게 테스트할 수 있고,
컴포넌트 내부의 서브 컴포넌트 트리에서 다른 스토어를 사용할 수도 있습니다.
일반적인 패턴 요약
전반적으로 이러한 공통 패턴 및 접근 방식은
일반적으로 핵심 Redux 설계 결정 또는 캡슐화, 중복 제거 및 재사용성과 같은
간단한 소프트웨어 엔지니어링 원칙의 결과로 볼 수 있습니다.
일반 액션 객체를 제외하고 이러한 모든 개념은 실제로 선택 사항이며
사용하지 않으려면 사용하지 않아도 되지만 존재하는 데에는 그럴만한 이유가 있습니다.
철학과 다양한 활용 방법
다양한 Redux 관련 코드를 살펴보았습니다.
수많은 라이브러리, 애플리케이션 코드, 자습서 및 기사를 읽었으며 매우 다양한 스타일, 접근 방식 및 구현을 보았습니다.
이를 기반으로, 그리고 Redux 메인테이너로서의 나의 지위에 근거해
나는 Redux 전문가라고 말하는 것이 타당하다고 생각합니다.
그것은 또한 서론에서 말했듯이 "좋은 Redux 코드"가 어떻게 생겼는지,
그리고 무엇이 "관용적인 Redux "코드"인지에 대한 의견이 있다는 것을 의미합니다.
이제 제가 말하고픈 부분에 도달했습니다.
따라서 이 마지막 섹션에서는 Redux를 사용하는 방법의 여러 변형을 살펴보고 이러한 것들이
Redux의 정신에 부합하는지 여부에 대한 제 생각을 제시하겠습니다.
독립적인 슬라이스 리듀서 vs 한번에 전부 업데이트
앞서 논의한 바와 같이, 기본 의도된 리듀서 구조는 함께 합쳐지는 슬라이스 리듀서입니다.
그러나 리듀서는 함수일 뿐이므로 리듀서 로직을 작성하고 합성하는 방법은 무한히 다양합니다.
나는 슬라이스 리듀서 간의 데이터 공유, 시퀀싱 종속성, 기존 리듀서를 추가 기능으로 래핑하는
개발자로서 모든 상태 업데이트 액션 트리거에 대한 개요를 알고 싶습니다. 내 코드의 한 곳에서. 한 줄씩. 코드의 어딘가에 포괄적인 요약이 있으면 특히 쓰기 액션의 순서와 관련된 경우 상태 업데이트를 올바르게 얻을 가능성이 극대화됩니다!
현재 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 업데이트 쿼리는 일반적으로 업데이트해야 하는 행에 대한 세부 정보를 제공합니다.
궁극적으로 여기에 절대적인 "정답"이 있다고 생각하지 않습니다.
이 주제는 정말 바이크셰딩에 빠지기 쉬운 주제라고 생각하고, 그 시간을 유용한 무언가를 만드는 데 보내고 싶습니다. :)
다회 디스패치
내가 작업한 몇 가지 응용 프로그램에서 일반적으로 더 큰 시퀀스의 일부로 전달되는 액션을 여러 개 조합하는 것을 발견했습니다.
결국, 나는 이러한 우려에 대해 걱정하지 않습니다. 성능이 문제인 경우 다양한 일괄 디스패치 접근 방식 중 하나로 처리할 수 있습니다. 액션으로 인해 일부 "불안정한" 상태가 발생할 수 있는 경우 어떻게 처리되고 있는지 다시 생각하고 싶을 것입니다. (또는 동일한 일괄 처리 개념을 적용하여 "트랜잭션"을 구현)
다양한 리듀서
커서 및 "올인원" 리듀서
"커서"는 (대략) 특정 중첩 데이터 조각에 대한 읽기/쓰기 액세스를 제공하는 함수입니다.
예를 들어, 개념적인 커서 라이브러리가 있고 const nestedCursor = cursor(state).refine(["a", "b", "c"])과 같은 커서를 만든 경우
반환된 커서 개체를 사용하면 상태를 조작할 수 있습니다. state.a.b.c 중간 레이어에 대해 스스로 걱정할 필요가 없습니다.
많은 사람들이 실제로 커서 라이브러리를 통해 직접
또는 액션에서 상태 키 경로와 새 값을 지정할 수 있도록 하는 리듀서를 통해 Redux 상태 업데이트에 이 접근 방식을 적용하려고 했습니다.
나는 또한 사람들이 "단순한 리듀서"를 작성하려고 하는 것을 보았습니다.
여기에서 전체 응용 프로그램의 유일한 액션은 "SET_DATA" 뿐이고
유일한 리듀서의 기능은 단순히 {...state, ...action.payload}를 반환하는 것입니다.
Redux가 제공하지 않는 것은 쓰기 커서입니다. 이것은 이유가 있는 핵심 설계 결정입니다. Redux를 사용하면 합성을 사용하여 상태를 관리할 수 있습니다. 데이터는 해당 데이터를 관리하는 리듀서(현재 문서의 "저장소") 없이는 절대 존재하지 않습니다. 이렇게 하면 데이터가 잘못된 경우 누가 변경했는지 항상 추적할 수 있습니다. 또한 어떤 액션이 데이터를 변경했는지 항상 추적할 수도 있습니다.
쓰기 커서를 사용하면 그러한 보장이 없습니다. 코드의 많은 부분이 커서를 통해 동일한 경로를 참조하고 원하는 경우 업데이트할 수 있습니다.