리덕스를 올바르게 사용하는 방법을 배워봅시다. 1편에서는 리덕스의 구현과 그렇게 구현한 의도를 다룹니다.
2편 링크 : https://itchallenger.tistory.com/687
원문 : https://blog.isquaredsoftware.com/2017/05/idiomatic-redux-tao-of-redux-part-1/
Redux는 기본적으로 믿을 수 없을 정도로 단순한 패턴입니다.
현재 값을 저장하고 필요할 때 해당 값을 업데이트하는 단일 함수를 실행하고 구독자에게 변경 사항이 있음을 알립니다.
그 단순성에도 불구하고 또는 아마도 그것 때문에 Redux를 사용하는 방법에 대한 다양한 접근 방식, 의견 및 태도가 있습니다.
이러한 접근 방식의 대부분은 공식 문서에 있는 개념 및 예제와 크게 다릅니다.
- "보일러플레이트가 너무 많다"
- "액션 상수와 액션 생성자는 불필요하다"
- "기능을 추가하려면 너무 많은 파일을 편집해야 한다"
- "쓰기 로직을 왜 다른 파일에서 파일을 전환해야 합니까?"
- "용어와 이름이 배우기 너무 어렵거나 혼란스럽습니다."
- 그리고 훨씬 더 많습니다
기초 다지기
세가지 원칙
Redux의 세 가지 원칙(Three Principles of Redux)을 살펴보는 것으로 시작하겠습니다.
- 단일 진실 원천 : 전체 애플리케이션의 상태가 단일 저장소 내의 객체 트리에 저장됩니다.
- 상태는 읽기 전용입니다 : 상태를 변경하는 유일한 방법은 발생한 일을 설명하는 객체인 액션을 내보내는 것입니다.
- 변경은 순수 함수에서 발생합니다 : 상태 트리가 액션에 의해 변환되는 방식을 지정하려면 순수 함수 리듀서를 작성합니다.
현실적인 의미에서, 그 진술들 각각은 거짓말입니다!
(또는 제다이의 귀환의 고전적인 대사를 빌리자면 "그들은 사실입니다... 특정 관점에서.")
- "Single source of truth"는 잘못된 것입니다.
- 모든 것을 Redux에 넣을 필요가 없고 (you don't have to put everything into Redux)
- 저장소 상태가 객체일 필요도 없고
- 단일 스토어를 가질 필요도 없습니다 (don't even have to have a single store)
- "상태는 읽기 전용입니다"는 잘못된 것입니다.
- 실제로 애플리케이션이 현재 상태 트리를 수정하는 것을 리덕스가 막지는 않기 때문입니다.
- "변경은 순수 함수에서 발생합니다 "는 잘못된 것입니다.
- 리듀서 함수가 상태 트리를 직접 변경하거나 다른 부작용을 일으킬 수 있기 때문입니다.
이러한 진술이 완전히 사실이 아니라면 왜 이런 진술을 하고 있나요??
이러한 원칙은 Redux의 구현에 관한 내용이 아닙니다.
Redux를 어떻게 사용해야 하는지에 대한 설명입니다.
언어 vs 메타 언어
리덕스는 실제로 어떻게 동작하나요?
리덕스 코어 : createStore
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" 예를 인용할 가치가 있습니다.
(Dan Abramov's pull request for the "counter-vanilla" example)
새로운 Counter Vanilla 예제는
Redux가 Webpack, React, 핫 리로딩, sagas, 액션 생성자, 상수, Babel, npm, CSS 모듈, 데코레이터, 유창한 라틴어,
Egghead 구독, PhD 또는 웹 온톨로지 랭귀지 이상의 수준이 필요하다는 신화를 없애기 위함입니다.
필요한것은 단지, HTML, 약간 특이한 스크립트 태그 및 평범한 오래된 DOM 조작입니다. 즐기세요!
- 저장소에 도달하는 작업은 일반 개체여야 하고
- 액션에는 정의되지 않은 타입 필드가 있어야 합니다.
이러한 제약 조건은 모두 원래 "플럭스 아키텍처" 개념에서 기원합니다. Flux 문서의 Flux Actions 및 Dispatcher(Flux Actions and the Dispatcher) 섹션을 인용합니다:
애플리케이션과 상호 작용하는 사람을 통해 또는 웹 API 호출을 통해 새 데이터가 시스템에 입력되면,
해당 데이터는 새 데이터 필드와 특정 액션 타입을 포함하는 개체 리터럴인 액션으로 패키지됩니다.
액션 객체를 생성할 뿐만 아니라 디스패처에 액션을 전달하는 ActionCreators라는
도우미 메서드 라이브러리를 만드는 경우가 많습니다.
다른 액션은 타입 속성으로 식별됩니다.
모든 스토어가 액션을 수신하면 일반적으로 이 타입 속성을 사용하여 응답해야 하는지 여부와 방법을 결정합니다.
Flux 애플리케이션에서 스토어와 뷰는 모두 스스로를 제어합니다.
그들은 외부 물체에 의해 작동되지 않습니다.
액션은 setter 메소드가 아니라 정의하고 등록하는 콜백을 통해 스토어로 흐릅니다.
내장 유틸리티: CombineReducers
여기에서 더 많은 사람들에게 친숙한 몇 가지 제약 조건이 보이기 시작합니다.
CombineReducers는 제공된 각 슬라이스 리듀서가 기본 상태(default state)를 반환하여
알 수 없는 액션에 "정확하게" 응답하고 undefined를 반환하지 않을 것을 기대합니다.
또한 현재 상태 값이 일반 JS 객체이고 현재 상태 객체의 키와 리듀서 함수 객체 사이에 정확한 대응이 있을 것으로 예상합니다.
마지막으로 모든 슬라이스 리듀서가 이전 값을 반환했는지 확인하기 위해 동등 비교를 사용합니다.
반환된 모든 값이 동일한 것으로 나타나면 실제로 아무 것도 변경되지 않은 것으로 가정하고
잠재적인 최적화로 원래 루트 상태 개체를 반환합니다.
진짜 셀링 포인트 : Redux DevTools
- 리듀서 함수가 상태를 변경하면 디버거에서 작업 간에 점프하면 값이 일관되지 않습니다.
- 리듀서에 부작용이 있는 경우 DevTools에서 액션을 재생할 때마다 해당 부작용이 다시 실행됩니다.
주 UI 바인딩 : 리액트 리덕스와 connect
일반적으로 같이 쓰는 라이브러리 : React와 Reselect
Redux의 기술 요구 사항 요약
- 액션은 일반 객체여야 하고
- 정의된 타입 필드를 포함해야 합니다.
- 구현에서 리듀서 함수가 상태를 변경하거나 AJAX 호출을 트리거하는 것은 가능합니다.
- 구현에서 애플리케이션의 각 부분이 getState()를 호출하고 상태 트리의 내용을 직접 수정하는 것은 가능합니다.
리덕스의 설계 의도
리덕스가 영향 받은 것, 리덕스의 목적
Redux 문서의 "소개" 섹션은 동기, 핵심 개념 및 선행 기술 주제에서 Redux의 개발 및 개념에 대한 몇 가지 주요 영향을 설명합니다.
(Motivation, Core Concepts, and Prior Art topics.)
요약하자면:
- Redux의 주요 목표는 업데이트가 발생할 수 있는 방법과 시기에 대한 제한을 부과하여 상태 돌연변이를 예측 가능하게 만드는 것입니다.
- 업데이트 논리를 애플리케이션의 나머지 부분과 분리하고 일반적인 액션 객체를 사용하여 발생해야 하는 변경 사항을 설명하는 "플럭스 아키텍처" 아이디어를 차용합니다.
- 시간 여행 디버깅과 같은 개발 경험 기능은 Redux의 주요 사용 사례로 의도되었습니다.
- 따라서 이러한 개발 사용 사례를 가능하게 하고 개발자가 데이터 흐름 및 업데이트 논리를 쉽게 추적할 수 있도록 하기 위해 불변성 및 직렬화 가능성과 같은 제약 조건이 크게 존재합니다.
- Redux는 모든 실제 상태 업데이트 로직이 동기가 되기를 원하고 비동기 동작이 상태 업데이트 동작과 별도로 유지되기를 원합니다.
- Flux 아키텍처는 서로 다른 유형의 데이터에 대해 여러 개의 개별 "저장소"를 가질 것을 제안했습니다.
- Redux는 이러한 여러 "저장소"를 단일 상태 트리로 결합하여 디버깅, 상태 지속성 및 실행 취소/다시 실행과 같은 기능을 보다 쉽게 작업할 수 있도록 합니다.
- 단일 루트 리듀서 함수는 그 자체로 많은 작은 리듀서 함수로 구성될 수 있습니다.
- 이를 통해 종속성 체인을 설정하기 위해 Flux의 store.waitFor() 이벤트 이미터와 같은 메커니즘에 의존하는 대신 한 상태 슬라이스를 업데이트할 때 다른 슬라이스를 먼저 계산해야 하는 종속성 순서를 포함하여 데이터 처리 방법을 명시적으로 제어할 수 있습니다.
- 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는 너무 많은 추상적인 FP 개념에 사용자를 압도하거나
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는 최대한 적어야 합니다.
최고의 API는 종종 API가 없는 것입니다.
미들웨어 및 고차 스토어에 대한 현재 제안은 Redux 코어에서 특별한 처리가 필요하지 않다는 엄청난 이점이 있습니다.
각각 dispatch() 및 createStore()를 둘러싼 래퍼일 뿐입니다. 1.0이 출시되기 전인 오늘도 사용할 수도 있습니다.
이는 확장성과 신속한 혁신을 위한 거대한 승리입니다.
엄격하고 특별한 API보다 패턴과 규칙을 선호해야 합니다.
내가 NuclearJS를 사용하는 대신 Redux를 작성하기로 선택한 이유는 다음과 같습니다.
- ImmutableJS에 대한 강한 의존성을 원하지 않습니다.
- 나는 가능한 한 적은 API를 원한다
- 더 나은 것이 나올 때 Redux에서 쉽게 벗어나고 싶습니다.
Redux를 사용하면 상태에 일반 객체, 배열 등을 사용할 수 있습니다.
createStore와 같은 API는 특정 구현에 바인딩되기 때문에 피하려고 열심히 노력했습니다.
대신 각 엔터티(Reducer, Action Creator)를 Redux에 의존하지 않고 노출할 수 있는 최소한의 방법을 찾으려 했습니다.
Redux를 임포트하고 리덕스이 크게 의존하는 유일한 코드는 루트 컴포넌트와 이를 구독하는 컴포넌트 입니다.
Redux는 가능한 한 확장 가능해야 합니다
비동기 작업을 계속 지원하고 외부 플러그인 및 도구에 대한 확장점을 제공하기 위해
몇 가지 일반적인 액션 미들웨어, 미들웨어 작성 헬퍼 함수 작성에 관한 방법과,
확장 라이브러리 작성자가 코딩을 쉽게 만드는 방법에 대한 문서를 제공할 수 있습니다.
나는 그것이 대부분의 Redux 앱이 갖고 싶어할 자연스러운 기능이라는 데 동의하지만,
일단 우리가 그것을 코어에 넣으면 모든 사람들이
그것이 작동해야 하는 방식에 관해 바이크셰딩(시간낭비)하기 시작할 것입니다.
이것이 Flummox에서 저에게 일어난 일입니다.
우리는 코어를 최대한 최소화하고 유연하게 유지하여
빠르게 반복하고 다른 사람들이 그 위에 빌드할 수 있도록 하려고 노력하고 있습니다.
Dan이 한 번 말했듯이 (어디가... 아마도 Slack인지 기억나지 않습니다)
우리는 Flux 라이브러리의 Koa처럼 되는 것을 목표로 합니다.
결국 커뮤니티가 더 성숙해지면 reduxjs GitHub 조직에서 "축복받은" 플러그인 및 확장 모음을 유지 관리할 계획입니다.
우리는 많은 사람들이 기본적인 비동기 작업을 수행하는 Rx 연산자를 배우는 데 익숙하지 않다는 것을 알고 있기 때문에
Redux 자체에서 이와 같은 것을 처방하고 싶지 않았습니다.
비동기 로직이 복잡할 때 Rx가 유리하지만
모든 Redux 사용자가 Rx를 배우도록 강요하고 싶지 않았기 때문에 의도적으로 미들웨어를 더 유연하게 유지했습니다.
미들웨어 API가 등장한 이유는 비동기에 대한 특정 솔루션을 명시적으로 규정하고 싶지 않았기 때문입니다.
이전 Flux 라이브러리인 Flummox에는 프로미스 미들웨어가 내장되어 있었습니다.
내장되어 있기 때문에 동작을 변경하거나 옵트아웃할 수 없었습니다.
Redux를 사용하면 커뮤니티에서 우리가 구축할 수 있었던 것보다
더 나은 비동기 솔루션을 많이 만들 수 있다는 것을 알았습니다.
마지막 생각정리
Further Information 🔗︎
해당 글을 작성하는데 참고한 게시물들을 보고 싶으면 직접 원문을 확인하세요!
'FrontEnd' 카테고리의 다른 글
[번역] Idiomatic Redux: The Tao of Redux, Part 2 - Practice and Philosophy (0) | 2022.08.28 |
---|---|
[Redux] 액션 생성자를 사용해야 하는 이유 (Idiomatic Redux: Why use action creators?) (0) | 2022.08.27 |
[번역] "메타 언어 길들이기" 이해하기(Understanding "Taming the Meta Language") (0) | 2022.08.26 |
리액트에서 이미지(image) 태그 잘 사용하기 (4) | 2022.08.25 |
정규화된 상태 업데이트하기 [Managing Normalized Data][Redux][프론트엔드 상태관리] (0) | 2022.08.21 |