Redux 상태 관리를 통한 최상위 제어: ClojureScript 자습서
게시 됨: 2022-03-11ClojureScript 발굴의 흥미진진한 두 번째 기사로 돌아오신 것을 환영합니다! 이 게시물에서 저는 ClojureScript를 본격적으로 사용하기 위한 다음 단계인 상태 관리(이 경우에는 React를 사용)에 대해 다룰 것입니다.
프론트엔드 소프트웨어에서는 상태 관리가 중요합니다. 기본적으로 React에서 상태를 처리하는 몇 가지 방법이 있습니다.
- 상태를 최상위 수준으로 유지하고 이 상태(또는 특정 상태 부분에 대한 핸들러)를 하위 구성 요소로 전달합니다.
- 순수성을 창 밖으로 던지고 전역 변수 또는 일부 Lovecraftian 형식의 종속성 주입을 사용합니다.
일반적으로 이 중 어느 것도 훌륭하지 않습니다. 상태를 최상위 수준으로 유지하는 것은 매우 간단하지만 이를 필요로 하는 모든 구성 요소에 응용 프로그램 상태를 전달하는 데 많은 오버헤드가 있습니다.
이에 비해 전역 변수(또는 기타 순진한 상태 버전)를 사용하면 추적하기 어려운 동시성 문제가 발생할 수 있으며, 이로 인해 구성 요소가 예상할 때 업데이트되지 않거나 그 반대의 경우도 마찬가지입니다.
그렇다면 이 문제를 어떻게 해결할 수 있습니까? React에 익숙한 사람이라면 JavaScript 앱의 상태 컨테이너인 Redux를 사용해 본 적이 있을 것입니다. 상태를 유지 관리할 수 있는 관리 가능한 시스템을 과감하게 검색하면서 자신의 의지로 이것을 발견했을 수 있습니다. 또는 JavaScript 및 기타 웹 도구에 대해 읽는 동안 우연히 발견했을 수도 있습니다.
사람들이 Redux를 어떻게 보게 되든 내 경험에 따르면 일반적으로 두 가지 생각으로 끝납니다.
- “이걸 써봐야 하는 거라 모두들 말하니까 꼭 써봐야 겠다는 생각이 들어요.”
- "왜 이것이 더 나은지 완전히 이해하지 못합니다."
일반적으로 Redux는 상태 관리가 React의 반응적 특성에 맞도록 하는 추상화를 제공합니다. 모든 상태를 Redux와 같은 시스템으로 오프로드함으로써 React의 순수성 을 보존할 수 있습니다. 따라서 두통이 훨씬 줄어들고 일반적으로 추론하기가 훨씬 더 쉽습니다.
Clojure를 처음 사용하는 사람들을 위해
이것이 ClojureScript를 처음부터 완전히 배우는 데 도움이 되지 않을 수도 있지만 여기에서는 적어도 Clojure[Script]의 몇 가지 기본 상태 개념을 요약할 것입니다. 이미 노련한 Clojurian이라면 이 부분을 건너뛰어도 됩니다!
ClojureScript에도 적용되는 Clojure 기본 사항 중 하나를 상기하십시오. 기본적으로 데이터는 변경할 수 없습니다. 이것은 개발에 매우 유용하며 timestep N에서 생성한 내용이 timestep > N에서도 동일하다는 보장을 받습니다. 또한 ClojureScript는 atom
개념을 통해 필요한 경우 변경 가능한 상태를 가질 수 있는 편리한 방법을 제공합니다.
ClojureScript의 atom
는 Java의 AtomicReference
와 매우 유사합니다. 동시성 보장으로 내용을 잠그는 새 개체를 제공합니다. Java에서와 마찬가지로 이 객체에 원하는 모든 것을 배치할 수 있습니다. 그때부터 해당 원자는 원하는 모든 것에 대한 원자 참조가 됩니다.
원자가 있으면 재설정을 사용하여 atom
적으로 새 값을 설정할 수 있습니다 reset!
함수(함수의 !
에 유의하세요. Clojure 언어에서 이는 작업이 상태 저장 또는 순수하지 않음을 나타내는 데 자주 사용됩니다).
또한 Java와 달리 Clojure는 atom
에 무엇을 넣든 상관하지 않습니다. 문자열, 목록 또는 개체일 수 있습니다. 다이내믹한 타이핑, 베이비!
(def my-mutable-map (atom {})) ; recall that {} means an empty map in Clojure (println @my-mutable-map) ; You 'dereference' an atom using @ ; -> this prints {} (reset! my-mutable-map {:hello "there"}) ; atomically set the atom (reset! my-mutable-map "hello, there!") ; don't forget Clojure is dynamic :)
시약은 이 원자 개념을 자체 atom
확장합니다. (Reagent에 익숙하지 않다면 이 전에 게시물을 확인하세요.) 이것은 React의 내장 상태 저장소와 마찬가지로 Reagent에서 렌더링 이벤트도 트리거한다는 점을 제외하고 ClojureScript atom
과 동일하게 작동합니다.
예:
(ns example (:require [reagent.core :refer [atom]])) ; in this module, atom now refers ; to reagent's atom. (def my-atom (atom "world!")) (defn component [] [:div [:span "Hello, " @my-atom] [:input {:type "button" :value "Press Me!" :on-click #(reset! My-atom "there!")}]])
이것은 "Hello, world!"라고 말하는 <span>
을 포함하는 단일 <div>
를 표시합니다. 그리고 버튼이 있습니다. 해당 버튼을 누르면 my-atom
이 "there!"
를 포함하도록 원자적으로 변형됩니다. . 그러면 구성 요소의 다시 그리기가 트리거되어 "Hello, there!"라는 메시지가 표시됩니다. 대신에.
이것은 로컬 구성 요소 수준 변형에 대해 충분히 간단해 보이지만 여러 수준의 추상화를 포함하는 더 복잡한 응용 프로그램이 있는 경우에는 어떻게 될까요? 또는 여러 하위 구성 요소와 해당 하위 구성 요소 간에 공통 상태를 공유해야 하는 경우?
더 복잡한 예
예를 들어 이를 살펴보겠습니다. 여기서 우리는 조잡한 로그인 페이지를 구현할 것입니다:
(ns unearthing-clojurescript.login (:require [reagent.core :as reagent :refer [atom]])) ;; -- STATE -- (def username (atom nil)) (def password (atom nil)) ;; -- VIEW -- (defn component [on-login] [:div [:b "Username"] [:input {:type "text" :value @username :on-change #(reset! username (-> % .-target .-value))}] [:b "Password"] [:input {:type "password" :value @password :on-change #(reset! password (-> % .-target .-value))}] [:input {:type "button" :value "Login!" :on-click #(on-login @username @password)}]])
그런 다음 다음과 같이 기본 app.cljs
내에서 이 로그인 구성 요소를 호스팅합니다.
(ns unearthing-clojurescript.app (:require [unearthing-clojurescript.login :as login])) ;; -- STATE (def token (atom nil)) ;; -- LOGIC -- (defn- do-login-io [username password] (let [t (complicated-io-login-operation username password)] (reset! token t))) ;; -- VIEW -- (defn component [] [:div [login/component do-login-io]])
따라서 예상되는 워크플로는 다음과 같습니다.
- 사용자가 사용자 이름과 비밀번호를 입력하고 제출을 누르기를 기다립니다.
- 이것은 상위 구성 요소에서
do-login-io
기능을 트리거합니다. -
do-login-io
함수는 일부 I/O 작업(예: 서버에 로그인 및 토큰 검색)을 수행합니다.
이 작업이 차단되면 애플리케이션이 정지되어 이미 문제가 많은 것입니다. 그렇지 않은 경우 걱정할 비동기가 있습니다!
또한 이제 서버에 쿼리를 수행하려는 모든 하위 구성 요소에 이 토큰을 제공해야 합니다. 코드 리팩토링이 훨씬 더 어려워졌습니다!
마지막으로, 우리의 구성 요소는 이제 더 이상 순전히 반응 적이지 않습니다. 이제 나머지 응용 프로그램의 상태를 관리하는 데 공모하여 I/O를 트리거하고 일반적으로 약간의 골칫거리가 됩니다.
ClojureScript 튜토리얼: Redux 시작
Redux는 국가 기반의 모든 꿈을 실현하는 마술 지팡이입니다. 적절하게 구현되면 안전하고 빠르며 사용하기 쉬운 상태 공유 추상화를 제공합니다.
Redux의 내부 작동(및 그 이면의 이론)은 이 기사의 범위를 다소 벗어납니다. 대신 ClojureScript를 사용하여 작업 예제를 자세히 살펴보겠습니다. ClojureScript는 무엇을 할 수 있는지 보여주기 위해 어떤 식으로든 갈 것입니다!
우리의 맥락에서 Redux는 사용 가능한 많은 ClojureScript 라이브러리 중 하나에 의해 구현됩니다. 이것을 리프레임이라고 합니다. Redux 주변에 Clojure로 통합된 래퍼를 제공하므로 (제 생각에는) 사용하기 정말 좋습니다.
기초
Redux는 애플리케이션 상태를 끌어내어 구성 요소를 가볍게 유지합니다. Reduxified 구성 요소는 다음 사항만 생각하면 됩니다.
- 어떻게 생겼는지
- 어떤 데이터를 소비하는지
- 트리거하는 이벤트
나머지는 무대 뒤에서 처리됩니다.
이 점을 강조하기 위해 위의 로그인 페이지를 Reduxify합시다.
데이터베이스
가장 먼저 해야 할 일: 애플리케이션 모델이 어떻게 생겼는지 결정해야 합니다. 앱 전체에서 액세스할 수 있는 데이터인 데이터의 모양 을 정의하여 이를 수행합니다.

경험에 따르면 데이터가 여러 Redux 구성 요소에서 사용되어야 하거나 토큰처럼 오래 지속되어야 하는 경우 데이터베이스에 저장해야 합니다. 대조적으로 데이터가 구성 요소에 로컬인 경우(예: 사용자 이름 및 암호 필드) 데이터베이스에 저장되지 않고 로컬 구성 요소 상태로 있어야 합니다.
데이터베이스 상용구를 만들고 토큰을 지정해 보겠습니다.
(ns unearthing-clojurescript.state.db (:require [cljs.spec.alpha :as s] [re-frame.core :as re-frame])) (s/def ::token string?) (s/def ::db (s/keys :opt-un [::token])) (def default-db {:token nil})
여기에서 주목할 가치가 있는 몇 가지 흥미로운 점이 있습니다.
- 우리는 Clojure의
spec
라이브러리를 사용하여 데이터가 어떻게 보여야 하는지 설명 합니다. 이는 Clojure[Script]와 같은 동적 언어에 특히 적합합니다. - 이 예에서는 사용자가 로그인한 후 나타내는 전역 토큰만 추적합니다. 이 토큰은 간단한 문자열입니다.
- 그러나 사용자가 로그인하기 전에는 토큰이 없습니다. 이것은 "선택적, 비정규화"를 의미하는
:opt-un
키워드로 표시됩니다. (Clojure에서 일반 키워드는:cat
과 같은 반면에 한정된 키워드는:animal/cat
과 같을 수 있습니다. 한정은 일반적으로 모듈 수준에서 이루어집니다. 이렇게 하면 서로 다른 모듈의 키워드가 서로 방해하지 않습니다.) - 마지막으로 초기화 방법인 데이터베이스의 기본 상태를 지정합니다.
어느 시점에서든 데이터베이스의 데이터가 여기의 사양과 일치한다고 확신해야 합니다.
구독
이제 데이터 모델을 설명했으므로 뷰 가 해당 데이터를 표시하는 방식을 반영해야 합니다. Redux 구성 요소에서 뷰가 어떻게 보이는지 이미 설명했습니다. 이제 뷰를 데이터베이스에 연결하기만 하면 됩니다.
Redux를 사용하면 데이터베이스에 직접 액세스하지 않으므로 수명 주기 및 동시성 문제가 발생할 수 있습니다. 대신 구독 을 통해 데이터베이스 측면과의 관계를 등록합니다.
구독은 우리가 데이터베이스의 일부에 의존하고 있음을 리프레임(및 시약)에 알리고 해당 부분이 변경되면 Redux 구성 요소를 다시 렌더링해야 합니다.
구독은 매우 간단하게 정의할 수 있습니다.
(ns unearthing-clojurescript.state.subs (:require [re-frame.core :refer [reg-sub]])) (reg-sub :token ; <- the name of the subscription (fn [{:keys [token] :as db} _] ; first argument is the database, second argument is any token)) ; args passed to the subscribe function (not used here)
여기에서 토큰 자체에 대한 단일 구독을 등록합니다. 구독은 단순히 구독의 이름이며 데이터베이스에서 해당 항목을 추출하는 함수입니다. 우리는 그 값에 대해 우리가 원하는 무엇이든 할 수 있고 여기에서 우리가 원하는 만큼 뷰를 변경할 수 있습니다. 그러나 이 경우에는 단순히 데이터베이스에서 토큰을 추출하여 반환합니다.
재렌더링에 대한 더 좁은 범위를 위해 데이터베이스의 하위 섹션에 대한 보기를 정의하는 것과 같이 구독으로 할 수 있는 일이 훨씬 더 많지만 지금은 간단하게 유지하겠습니다!
이벤트
우리에게는 데이터베이스가 있고 데이터베이스에 대한 관점이 있습니다. 이제 몇 가지 이벤트를 트리거해야 합니다! 이 예에는 두 가지 종류의 이벤트가 있습니다.
- 데이터베이스에 새 토큰을 쓰는 순수한 이벤트(부작용 없음 )입니다.
- 나가서 일부 클라이언트 상호 작용을 통해 토큰을 요청하는 I/O 이벤트(부작용 있음 ).
쉬운 것부터 시작하겠습니다. Re-frame은 이러한 종류의 이벤트에 대해 정확히 기능을 제공합니다.
(ns unearthing-clojurescript.state.events (:require [re-frame.core :refer [reg-event-db reg-event-fx reg-fx] :as rf] [unearthing-clojurescript.state.db :refer [default-db]])) ; our start up event that initialises the database. ; we'll trigger this in our core.cljs (reg-event-db :initialise-db (fn [_ _] default-db)) ; a simple event that places a token in the database (reg-event-db :store-login (fn [db [_ token]] (assoc db :token token)))
여기서도 매우 간단합니다. 두 개의 이벤트를 정의했습니다. 첫 번째는 데이터베이스를 초기화하는 것입니다. (두 인수를 모두 무시하는 방법을 참조하십시오. 우리는 항상 default-db
를 사용하여 데이터베이스를 초기화합니다!) 두 번째는 토큰을 얻은 후 저장하기 위한 것입니다.
이러한 이벤트에는 외부 호출이나 I/O가 전혀 없는 부작용이 없습니다. 이것은 신성한 Redux 프로세스의 신성함을 보존하는 데 매우 중요합니다. Redux의 분노가 당신에게 임하지 않도록 그것을 불순하게 만들지 마십시오.
마지막으로 로그인 이벤트가 필요합니다. 우리는 그것을 다른 것들 아래에 둘 것입니다:
(reg-event-fx :login (fn [{:keys [db]} [_ credentials]] {:request-token credentials})) (reg-fx :request-token (fn [{:keys [username password]}] (let [token (complicated-io-login-operation username password)] (rf/dispatch [:store-login token]))))
reg-event-fx
기능은 약간의 차이가 있지만 reg-event-db
와 대체로 유사합니다.
- 첫 번째 인수는 더 이상 데이터베이스 자체가 아닙니다. 여기에는 애플리케이션 상태를 관리하는 데 사용할 수 있는 기타 여러 가지가 포함되어 있습니다.
- 두 번째 인수는
reg-event-db
와 매우 유사합니다. - 새로운
db
를 반환하는 대신 이 이벤트에 대해 발생해야 하는 모든 효과("fx")를 나타내는 맵을 반환합니다. 이 경우 아래에 정의된:request-token
효과를 간단히 호출합니다. 다른 유효한 효과 중 하나는 단순히 다른 이벤트를 호출하는:dispatch
입니다.
효과가 전달되면 장기 실행 I/O 로그인 작업을 수행하는 :request-token
효과가 호출됩니다. 이 작업이 완료되면 결과를 이벤트 루프로 다시 전달하여 주기를 완료합니다!
ClojureScript 튜토리얼: 최종 결과
그래서! 스토리지 추상화를 정의했습니다. 이제 구성 요소가 어떻게 생겼습니까?
(ns unearthing-clojurescript.login (:require [reagent.core :as reagent :refer [atom]] [re-frame.core :as rf])) ;; -- STATE -- (def username (atom nil)) (def password (atom nil)) ;; -- VIEW -- (defn component [] [:div [:b "Username"] [:input {:type "text" :value @username :on-change #(reset! username (-> % .-target .-value))}] [:b "Password"] [:input {:type "password" :value @password :on-change #(reset! password (-> % .-target .-value))}] [:input {:type "button" :value "Login!" :on-click #(rf/dispatch [:login {:username @username :password @password]})}]])
그리고 앱 구성 요소:
(ns unearthing-clojurescript.app (:require [unearthing-clojurescript.login :as login])) ;; -- VIEW -- (defn component [] [:div [login/component]])
마지막으로 원격 구성 요소에서 토큰에 액세스하는 것은 다음과 같이 간단합니다.
(let [token @(rf/subscribe [:token])] ; ... )
함께 모아서:
소란, 소란 없습니다.
Redux/Re-frame을 사용한 구성 요소 분리는 깨끗한 상태 관리를 의미합니다.
Redux를 사용하여(재 프레임을 통해) 상태 처리의 혼란에서 뷰 구성 요소를 성공적으로 분리했습니다. 상태 추상화를 확장하는 것은 이제 케이크 조각입니다!
ClojureScript의 Redux 는 정말 쉽습니다. 시도하지 않을 이유가 없습니다.
크래킹할 준비가 되었다면 환상적인 re-frame 문서와 간단한 작업 예제를 확인하는 것이 좋습니다. 아래의 이 ClojureScript 튜토리얼에 대한 귀하의 의견을 기다리겠습니다. 행운을 빕니다!