Recoil은 React 세계에 원자 모델을 도입했습니다. 새로운 능력은 가파른 학습 곡선과 부족한 학습 자원을 희생하여 탄생했습니다.
Jotai 와 Zedux는 나중에 이 새로운 모델의 다양한 측면을 단순화하여 많은 새로운 기능을 제공하고 이 놀라운 새로운 패러다임의 한계를 뛰어 넘었습니다.
다른 기사에서는 이러한 도구 간의 차이점에 중점을 둘 것입니다. 이 기사에서는 세 가지 모두 공통적으로 가지고 있는 한 가지 큰 기능에 중점을 둘 것입니다.
그들은 Flux를 고쳤습니다.
Flux를 모른다면 다음의 간단한 요점을 참고하세요.
Redux 외에도 모든 Flux 기반 라이브러리는 기본적으로 다음 패턴을 따랐습니다. 앱에는 여러 개의 스토어가 있습니다. 적절한 순서로 모든 상점에 작업을 제공하는 작업을 수행하는 Dispatcher는 단 한 명뿐입니다. 이 "적절한 순서"는 저장소 간의 종속성을 동적으로 정렬하는 것을 의미합니다.
예를 들어 전자상거래 앱 설정을 살펴보겠습니다.
예를 들어 사용자가 바나나를 카트로 옮기면 PromosStore는 사용 가능한 바나나 쿠폰이 있는지 확인하기 위한 요청을 보내기 전에 CartStore의 상태가 업데이트될 때까지 기다려야 합니다.
아니면 바나나가 사용자 지역으로 배송되지 않을 수도 있습니다. CartStore는 업데이트하기 전에 UserStore를 확인해야 합니다. 아니면 일주일에 한 번만 쿠폰을 사용할 수도 있습니다. PromosStore는 쿠폰 요청을 보내기 전에 UserStore를 확인해야 합니다.
Flux는 이러한 종속성을 좋아하지 않습니다. 레거시 React 문서 에서:
Flux 애플리케이션 내의 개체는 고도로 분리되어 있으며 시스템 내의 각 개체가 시스템의 다른 개체에 대해 가능한 한 적게 알아야 한다는 원칙인 디미터 법칙을 매우 강력하게 준수합니다.
이에 대한 이론은 확고합니다. 100%. 아... 왜 이 다중 매장 버전의 Flux가 죽었나요?
격리된 상태 컨테이너 간의 종속성은 불가피합니다. 실제로 코드를 모듈화하고 DRY하게 유지하려면 다른 저장소를 자주 사용해야 합니다 .
Flux에서는 이러한 종속성이 즉시 생성됩니다.
// This example uses Facebook's own `flux` library PromosStore.dispatchToken = dispatcher.register(payload => { if (payload.actionType === 'add-to-cart') { // wait for CartStore to update first: dispatcher.waitFor([CartStore.dispatchToken]) // now send the request sendPromosRequest(UserStore.userId, CartStore.items).then(promos => { dispatcher.dispatch({ actionType: 'promos-fetched', promos }) }) } if (payload.actionType === 'promos-fetched') { PromosStore.setPromos(payload.promos) } }) CartStore.dispatchToken = dispatcher.register(payload => { if (payload.actionType === 'add-to-cart') { // wait for UserStore to update first: dispatcher.waitFor([UserStore.dispatchToken]) if (UserStore.canBuy(payload.item)) { CartStore.addItem(payload.item) } } })
이 예에서는 종속성이 저장소 간에 직접 선언되지 않고 작업별로 결합되는 방식을 보여줍니다. 이러한 비공식적 종속성을 찾으려면 구현 코드를 자세히 조사해야 합니다.
이것은 매우 간단한 예입니다! 그러나 당신은 이미 Flux가 어떻게 느끼는지 알 수 있습니다. 부작용, 선택 작업 및 상태 업데이트가 모두 함께 엮여 있습니다. 이 코로케이션은 실제로 꽤 좋을 수 있습니다. 그러나 일부 비공식적 종속성을 혼합하고 레시피를 3배로 늘려 상용구에 제공하면 Flux가 빠르게 분해되는 것을 볼 수 있습니다.
Flummox 및 Reflux 와 같은 다른 Flux 구현은 상용구 및 디버깅 환경을 개선했습니다. 매우 유용하지만 종속성 관리는 모든 Flux 구현을 괴롭히는 유일한 문제였습니다. 다른 매장을 이용하면 기분이 나빠졌습니다. 깊게 중첩된 종속성 트리는 따라가기가 어려웠습니다.
이 전자 상거래 앱은 언젠가 OrderHistory, ShippingCalculator, DeliveryEstimate, BananasHoarded 등에 대한 스토어를 보유할 수 있습니다. 대규모 앱에는 쉽게 수백 개의 스토어가 있을 수 있습니다. 모든 매장에서 종속성을 어떻게 최신 상태로 유지합니까? 부작용을 어떻게 추적합니까? 순결은 어떻습니까? 디버깅은 어떻습니까? 바나나는 정말 베리일까요?
Flux가 도입한 프로그래밍 원칙의 경우 단방향 데이터 흐름이 승자였지만 현재로서는 디미터의 법칙이 승자가 아닙니다.
우리 모두는 Redux가 상황을 구하기 위해 어떻게 등장했는지 알고 있습니다. 싱글톤 모델을 선호하여 여러 매장의 개념을 버렸습니다. 이제 모든 것이 "종속성" 없이 다른 모든 것에 액세스할 수 있습니다.
리듀서는 순수하므로 여러 상태 조각을 다루는 모든 로직은 저장소 외부로 나가야 합니다 . 커뮤니티는 부작용과 파생된 상태를 관리하기 위한 표준을 만들었습니다. Redux 저장소는 아름답게 디버그 가능합니다. Redux가 원래 수정하지 못한 유일한 주요 Flux 결함은 상용구였습니다.
RTK는 나중에 Redux의 악명 높은 상용구를 단순화했습니다. 그런 다음 Zustand는 디버깅 성능을 희생하면서 약간의 보풀을 제거했습니다. 이 모든 도구는 React 세계에서 매우 인기가 있습니다.
모듈식 상태를 사용하면 종속성 트리가 자연스럽게 너무 복잡해져서 우리가 생각할 수 있는 최선의 해결책은 "그냥 하지 마세요."였습니다.
그리고 그것은 효과가 있었습니다! 이 새로운 싱글톤 접근 방식은 여전히 대부분의 앱에서 충분히 작동합니다. Flux 원칙은 매우 견고하여 종속성 악몽을 제거하는 것만으로도 문제가 해결되었습니다.
아니면 그랬나요?
싱글톤 접근 방식의 성공은 Flux가 애초에 무엇을 얻었는가라는 질문을 불러일으킵니다. 왜 우리는 여러 매장을 원했습니까?
이것에 대해 좀 알려 드리겠습니다.
여러 매장을 사용하면 상태 조각이 자체 자율 모듈식 컨테이너로 분할됩니다. 이러한 매장은 별도로 테스트할 수 있습니다. 앱과 패키지 간에 쉽게 공유할 수도 있습니다.
이러한 자율 저장소는 별도의 코드 청크로 분할될 수 있습니다. 브라우저에서는 지연 로드되어 즉시 연결될 수 있습니다.
Redux의 리듀서는 코드 분할도 상당히 쉽습니다. replaceReducer
덕분에 유일한 추가 단계는 새로운 결합된 리듀서를 생성하는 것입니다. 그러나 부작용과 미들웨어가 관련된 경우 더 많은 단계가 필요할 수 있습니다.
싱글톤 모델에서는 외부 모듈의 내부 상태를 자신의 상태와 통합하는 방법을 알기가 어렵습니다. Redux 커뮤니티에서는 이를 해결하기 위한 시도로 Ducks 패턴을 도입했습니다. 그리고 약간의 상용구를 사용하여 작동합니다.
여러 저장소를 사용하면 외부 모듈을 통해 간단히 저장소를 노출할 수 있습니다. 예를 들어 양식 라이브러리는 FormStore를 내보낼 수 있습니다. 이것의 장점은 표준이 "공식적"이라는 것입니다. 즉, 사람들이 자신만의 방법론을 만들 가능성이 적다는 것을 의미합니다. 이는 더욱 강력하고 통합된 커뮤니티 및 패키지 생태계로 이어집니다.
싱글톤 모델은 놀라울 정도로 성능이 뛰어납니다. Redux가 이를 입증했습니다. 그러나 그 선택 모델은 특히 상한이 엄격합니다. 나는 이 Reselect 토론 에서 이것에 대한 몇 가지 생각을 썼습니다. 크고 값비싼 선택기 트리는 캐싱을 최대한 제어하는 경우에도 실제로 드래그되기 시작할 수 있습니다.
반면에 여러 저장소를 사용하면 대부분의 상태 업데이트가 상태 트리의 작은 부분으로 격리됩니다. 그들은 시스템의 다른 어떤 것도 건드리지 않습니다. 이는 싱글톤 접근 방식보다 훨씬 확장 가능합니다. 실제로 여러 저장소를 사용하는 경우 사용자 컴퓨터의 메모리 제한에 도달하기 전에 CPU 제한에 도달하는 것은 매우 어렵습니다.
Redux에서는 상태를 파괴하는 것이 그리 어렵지 않습니다. 코드 분할 예제와 마찬가지로 리듀서 계층 구조의 일부를 제거하려면 몇 가지 추가 단계만 필요합니다. 그러나 여러 저장소를 사용하는 것이 더 간단합니다. 이론적으로는 간단히 디스패처에서 저장소를 분리하고 가비지 수집을 허용할 수 있습니다.
이는 Redux, Zustand 및 일반적으로 싱글톤 모델이 잘 처리하지 못하는 큰 문제입니다. 부작용은 상호 작용하는 상태와 분리됩니다. 선택 논리는 모든 것과 분리되어 있습니다. 다중 매장 Flux가 너무 같은 위치에 배치된 반면 Redux는 정반대의 극단으로 나아갔습니다.
여러 자율 매장을 사용하면 이러한 것들이 자연스럽게 결합됩니다. 실제로 Flux에는 모든 것이 엉망진창이 되는 것을 방지하기 위한 몇 가지 표준이 부족했습니다(죄송합니다).
이제 OG Flux 라이브러리를 알고 계시다면 실제로 이 모든 것이 좋지 않았다는 것을 아실 것입니다. 디스패처는 여전히 전역적 접근 방식을 취하여 모든 작업을 모든 매장에 디스패치합니다. 비공식적/암시적 종속성으로 인해 코드 분할 및 파괴가 완벽하지 않게 되었습니다.
그럼에도 불구하고 Flux에는 멋진 기능이 많이 있었습니다. 또한 다중 매장 접근 방식은 제어 역전(Inversion of Control) 및 프랙탈(로컬) 상태 관리와 같은 훨씬 더 많은 기능을 제공할 가능성이 있습니다.
누군가 여신의 이름을 Demeter로 명명하지 않았다면 Flux는 정말 강력한 상태 관리자로 진화했을 수도 있습니다. 난 진심이야! ... 알았어, 난 아니야. 하지만 이제 언급하셨으니 데메테르의 법칙을 좀 더 자세히 살펴볼 가치가 있을 것 같습니다.
소위 "법"이란 정확히 무엇입니까? 위키피디아 에서:
- 각 단위는 다른 단위에 대해 제한된 지식만 가져야 합니다. 즉, 현재 단위와 "밀접하게" 관련된 단위만 알아야 합니다.
- 각 유닛은 자신의 친구들과만 대화해야 합니다. 낯선 사람과 이야기하지 마십시오.
이 법칙은 객체 지향 프로그래밍을 염두에 두고 설계되었지만 React 상태 관리를 포함한 다양한 영역에 적용될 수 있습니다.
기본 아이디어는 상점에서 다음을 방지하는 것입니다.
바나나 용어로 말하자면, 바나나는 다른 바나나의 껍질을 벗겨서는 안 되며, 다른 나무에 있는 바나나와 대화를 해서도 안 됩니다. 그러나 두 나무가 먼저 바나나 전화선을 연결하면 다른 나무와 대화 할 수 있습니다 .
이는 문제의 분리를 장려하고 코드가 모듈식, DRY 및 SOLID를 유지하는 데 도움이 됩니다. 탄탄한 이론! 그렇다면 Flux에는 무엇이 빠졌나요?
글쎄요, 매장 간 종속성은 좋은 모듈형 시스템의 자연스러운 부분입니다. 저장소가 다른 종속성을 추가해야 하는 경우 이를 수행 하고 가능한 한 명시적으로 수행해야 합니다. Flux 코드 중 일부는 다음과 같습니다.
PromosStore.dispatchToken = dispatcher.register(payload => { if (payload.actionType === 'add-to-cart') { // wait for CartStore to update first: dispatcher.waitFor([CartStore.dispatchToken]) // now send the request sendPromosRequest(UserStore.userId, CartStore.items).then(promos => { dispatcher.dispatch({ actionType: 'promos-fetched', promos }) }) } if (payload.actionType === 'promos-fetched') { PromosStore.setPromos(payload.promos) } })
PromosStore에는 다양한 방식으로 선언된 여러 종속성이 있습니다. 즉, CartStore
에서 기다리고 읽고 UserStore
에서 읽습니다. 이러한 종속성을 검색하는 유일한 방법은 PromosStore 구현에서 저장소를 찾는 것입니다.
개발 도구도 이러한 종속성을 더 쉽게 검색할 수 있도록 도와줄 수 없습니다. 즉, 종속성이 너무 암시적입니다.
이것은 매우 간단하고 조작된 예이지만 Flux가 디미터의 법칙을 어떻게 잘못 해석했는지 보여줍니다. 대부분 Flux 구현을 작게 유지하려는 욕구에서 탄생했다고 확신하지만(실제 종속성 관리는 복잡한 작업입니다!) Flux가 부족한 부분이 바로 여기에 있습니다.
이 이야기의 영웅과는 달리:
2020년에는 Recoil이 우연히 등장했습니다. 처음에는 다소 어색했지만 Flux의 다중 매장 접근 방식을 부활시키는 새로운 패턴을 가르쳐주었습니다.
단방향 데이터 흐름이 저장소 자체에서 종속성 그래프로 이동되었습니다. 이제 상점을 아톰(Atom)이라고 불렀습니다. Atom은 적절하게 자율적이고 코드 분할이 가능했습니다. 그들은 서스펜스 지원 및 수분 공급과 같은 새로운 힘을 가졌습니다. 그리고 가장 중요한 것은 원자가 공식적으로 종속성을 선언한다는 것입니다.
원자 모델이 탄생했습니다.
// a Recoil atom const greetingAtom = atom({ key: 'greeting', default: 'Hello, World!', })
Recoil은 비대해진 코드베이스, 메모리 누수, 나쁜 성능, 느린 개발, 불안정한 기능, 특히 부작용으로 인해 어려움을 겪었습니다. 이 중 일부는 천천히 해결될 예정이었지만 그 동안 다른 도서관에서는 Recoil의 아이디어를 채택하여 함께 운영했습니다.
Jotai는 현장에 등장하여 빠르게 추종자를 얻었습니다.
// a Jotai atom const greetingAtom = atom('Hello, World!')
Jotai는 Recoil 크기에 비해 아주 작은 부분일 뿐만 아니라 WeakMap 기반 접근 방식으로 인해 더 나은 성능, 더 매끄러운 API, 메모리 누수 없음을 제공했습니다.
그러나 이는 약간의 성능 저하를 가져왔습니다. WeakMap 접근 방식은 캐시 제어를 어렵게 만들고 여러 창이나 다른 영역 간에 상태를 공유하는 것을 거의 불가능하게 만듭니다. 그리고 문자열 키가 부족하기 때문에 디버깅이 악몽이 됩니다. 대부분의 앱에서는 이러한 기능을 다시 추가해야 하며, 이로 인해 Jotai의 매끈함이 크게 손상됩니다.
// a (better?) Jotai atom const greetingAtom = atom('Hello, World!') greetingAtom.debugLabel = 'greeting'
몇 가지 명예로운 언급으로는 Reatom 과 Nanostores 가 있습니다. 이러한 라이브러리는 원자 모델 뒤에 있는 이론을 더 많이 탐구하고 그 크기와 속도를 한계까지 끌어올리려고 노력하고 있습니다.
원자 모델은 빠르고 확장성이 매우 좋습니다. 그러나 아주 최근까지 어떤 원자 라이브러리도 잘 해결하지 못한 몇 가지 우려 사항이 있었습니다.
학습 곡선. 원자는 다릅니다 . React 개발자가 이러한 개념에 접근하기 쉽게 만들려면 어떻게 해야 할까요?
Dev X 및 디버깅. 원자를 발견 가능하게 만드는 방법은 무엇입니까? 업데이트를 추적하거나 모범 사례를 시행하려면 어떻게 해야 합니까?
기존 코드베이스에 대한 증분 마이그레이션. 외부 스토어에 어떻게 액세스하나요? 기존 논리를 그대로 유지하려면 어떻게 해야 합니까? 전체 재작성을 어떻게 방지합니까?
플러그인. 원자 모델을 어떻게 확장 가능하게 만들 수 있나요? 가능한 모든 상황을 처리 할 수 있나요 ?
의존성 주입. Atom은 자연스럽게 종속성을 정의하지만 테스트 중에 또는 다른 환경에서 교체할 수 있습니까?
데메테르의 법칙. 구현 세부 사항을 숨기고 분산 업데이트를 방지하려면 어떻게 해야 합니까?
이것이 바로 내가 들어오는 곳입니다. 보세요, 저는 또 다른 원자 라이브러리의 주요 작성자입니다.
Zedux는 몇 주 전에 마침내 현장에 들어왔습니다. 제가 일하는 뉴욕의 핀테크 회사에서 개발한 Zedux는 빠르고 확장 가능하도록 설계되었을 뿐만 아니라 매끄러운 개발 및 디버깅 경험을 제공하도록 설계되었습니다.
// a Zedux atom const greetingAtom = atom('greeting', 'Hello, World!')
여기서는 Zedux의 기능에 대해 깊이 다루지 않겠습니다. 앞서 말했듯이 이 문서에서는 이러한 원자 라이브러리 간의 차이점에 초점을 맞추지 않습니다.
Zedux가 위의 모든 문제를 해결한다고 말하면 충분합니다. 예를 들어, 실제 Inversion of Control을 제공하는 최초의 원자 라이브러리이며, 구현 세부 사항을 숨기기 위해 원자 내보내기를 제공함으로써 우리를 Demeter 법칙으로 다시 돌아가게 하는 최초의 원자 라이브러리입니다.
Flux의 마지막 이념이 마침내 부활했습니다. 부활했을 뿐만 아니라 개선되었습니다! - 원자 모델 덕분입니다.
그렇다면 원자 모델은 정확히 무엇 입니까 ?
이러한 원자 라이브러리에는 많은 차이점이 있습니다. "원자"가 의미하는 바에 대한 정의도 다릅니다. 일반적인 합의는 원자가 방향성 비순환 그래프를 통해 반응적으로 업데이트되는 작고 고립된 자율적 상태 컨테이너라는 것입니다.
알아요, 알아요, 복잡하게 들리겠지만 제가 바나나로 설명할 때까지 기다리세요.
농담이야! 실제로는 매우 간단합니다.
그래프를 통해 도탄을 업데이트합니다. 그게 다야!
요점은 구현이나 의미에 관계없이 이러한 모든 원자 라이브러리가 다중 저장소의 개념을 부활시켜 사용 가능하게 만들었을 뿐만 아니라 함께 작업하는 즐거움을 선사했다는 것입니다.
여러 매장을 원하는 6가지 이유는 바로 원자 모델이 그토록 강력한 이유입니다.
간단한 API와 확장성만으로도 원자 라이브러리는 모든 React 앱에 탁월한 선택이 됩니다. Redux보다 더 많은 전력 과 더 적은 상용구? 이게 꿈인가요?
정말 멋진 여행이에요! React 상태 관리의 세계는 계속해서 놀라움을 선사하고 있으며, 이 기회에 탑승하게 되어 매우 기쁩니다.
우리는 이제 막 시작했습니다. 원자에는 혁신의 여지가 많습니다. Zedux를 만들고 사용하는 데 수년을 보낸 후에 원자 모델이 얼마나 강력할 수 있는지 확인했습니다. 사실 그 힘은 아킬레스 건입니다.
개발자들이 원자를 탐구할 때, 그들은 종종 "원자가 이 문제를 얼마나 간단하고 우아하게 해결하는지 봐"라기보다는 "이 미친 복잡한 힘을 봐"라고 말하면서 가능성을 너무 깊이 파헤치는 경우가 많습니다. 나는 이것을 바꾸려고 여기에 왔습니다.
원자 모델과 그 뒤에 있는 이론은 대부분의 React 개발자가 접근할 수 있는 방식으로 가르쳐지지 않았습니다. 어떤 면에서 지금까지 React 세계의 원자 경험은 Flux와 정반대였습니다.
이 기사는 React 개발자가 원자 라이브러리의 작동 방식과 원자 라이브러리를 사용하려는 이유를 이해하는 데 도움을 주기 위해 제가 제작하고 있는 일련의 학습 리소스 중 두 번째입니다. 첫 번째 기사 인 확장성: 잃어버린 React 상태 관리 수준을 확인하세요.
10년이 걸렸지만 Flux가 도입한 견고한 CS 이론은 원자 모델 덕분에 마침내 React 앱에 큰 영향을 미치고 있습니다. 그리고 앞으로도 수년 동안 계속 그렇게 할 것입니다.