[Pro react] 7장. 플럭스를 이용한 리액트 어플리케이션 설계

앞에서 설명했지만 부모 컴포넌트에서 자식으로 속성을 통한 단방향 데이터 흐름은 리액트의 핵심 철학 중 하나다. 자식이 부모 컴포넌트로 데이터를 전달해야 할 때는 자식이 이용할 콜백 함수속성을 통해 전달할 수 있다.

이 단방향 데이터 흐름은 명확하고 이해하기 쉬운 코드를 작성하도록 도움을 준다. 즉, 리액트 어플의 실행을 처음부터 끝까지 추적할 때 어떤 상황에 어떤 코드가 실행되는지 쉽게 알 수 있다.

이 아키텍처 패턴은 장점도 많지만 몃 가지 해결해야 할 과제도 안고 있다. 리액트 어플은 일반적으로 최상위 컴포넌트가 컨테이너로서 작동하며, 그 아래에 다수의 순수 순수 컴포넌트가 인터페이스 트리의 리프처럼 작동하는 다중 중첩 단계로 성장한다. 상태가 계층의 취상위 수준에 유지되므로 속성을 통해 여러 단계로 전달해야 하는 콜백을 만들어야 하는 경우가 많은데, 이 작업은 반복적이며 오류가 발생할 가능성이 높다.

리액트 개발자는 데이터와 콜백을 속성을 통해 다단계로 전달하는 방식을 드릴링이라고 비유했다.

여러 중첩된 리액트 컴포넌트가 UI를 구성하는 훌륭한 방법이라는 점은 확실하게 해둘 필요가 있다. 리액트 컴포넌트를 중첩해 구성하면 복잡성을 줄이고 관심사를 분리하는 데 도움이 되며, 코드를 확장하고 유지 관리하기도 편하다. 리액트는 반응형 렌더링의 개념 기반을 두고 작동하므로, 컴포넌트의 상태나 속성의 모든 변경에 반응해 DOM을 업데이트한다. 즉, 아주 간단한 개념을 바탕으로 개발하면서 부수적으로 탁월한 성능까지 얻을 수 있다.

이 장에서 중첩된 컴포넌트 기반의 복잡한 어플리케이션을 개발할 때 데이터와 이러한 데이터를 조작하는 콜백을 해당하는 각 컴포넌트에 밀접하게 관리하는 방법이다. 이를 위해 등장한 것이 플럭스다.

플럭스란?

플럭스는 웹 어플을 개발하기 위한 아키텍처 가이드라인으로 페이스북에서 만들었다. 리액트 전용은 아니며 리액트의 일부도 아니지만 리액트와 아주 잘 어울린다.

플럭스의 핵심 개념은 어플에서 단방향 데이터 흐름을 지원한다는 것이다.

플럭스는 기본적으로 액션(Action),스토어(Store),디스패처(dispatcher)의 세 부분으로 구성된다. 각 부분을 자세히 살펴보자.

스토어

앞에서 언급했듯이, 플러스의 핵심은 데이터를 애플리케이션의 각 컴포넌트와 밀접하게 관리하는 것이다. 우리가 생각하는 이상적인 세계의 뷰는 이러하다. 데이터는 컴포넌트와 완전히 분리되지만 데이터가 변경될 때마다 다시 렌더링할 수 있도록 알림을 받는다.

데이터 => 뷰(리액트 컴포넌트)

스토어가 하는 일이 바로 이것이다. 스토어는 애플리케이션의 모든 상태(데이터와 UI상태를 포함)를 유지하며 상태가 변경되면 이벤트를 발송(dispatch)한다. 뷰(리액트 컴포넌트)는 필요한 데이터를 포함하는 스토어를 구독하며 데이터가 변경되면 자신을 다시 렌더링한다.

스토어 => 뷰(리액트 컴포넌트)

스토어는 완전히 폐쇄된 블랙박스라는 중요한 특징을 갖고 있다. 데이터에 접근하기 위한 공용 접근자메서드(getter)를 제공하지만, 뷰는 물론이고 플럭스의 다른 부분에서도 스토어의 데이터를 변경, 갱신, 삽입할 수 없다. 스토어 자체만 데이터를 변경할 수 있다.

MVC 턴의 모델과 비슷한 측면이 있지만, 스토어는 접근자 메서드만 제공한다는 점이 가장 큰 차이다. 즉, 외부에서는 스토어의 값을 변경할 수 없다.

액션

액션을 느슨하게 정의한다면 “앱에서 일어나는 일”이라고 할 수 있다. 액션은 어플의 거의 모든 부분에서 생성될 수 있으며, 사용자 상호작용(예: 버튼 클릭, 댓글 달기, 검색 요청 등)에서 주로 생성되지만 AJAX요청, 타이머, 웹 소켓 이벤트 등을 통해서도 생성된다.

모든 액션은 타입(고유한이름)과 선택적인 페이로드를 포함한다.. 스토어는 액션이 발송되면 자신의 데이터를 업데이트한다

스토어 -> 뷰 -> 액션

|——————-|

이로써 플럭스의중요한 부분을 거의 모두 소개했다. 리액트 컴포넌트가 액션을 생성하면(예:사용자가 텍스트필드에 이름을 입력) 액션이 스토어로 전달되며, 이 특정 액션과 관계가 있는 스토어가 자신의 데이터를 업데이트하고 변경 이벤트를 발송한다. 그러면 마지막으로 뷰가 해당 스토어의 이벤트에 반응해 최신 데이터로 자신을 다시 렌더링한다. 그런데 이 다이어그램에는 디스패처라는 마지막 조각이 빠져있다.

디스패처

디스패처는 액션을 스토어로 전달하는 과정을 조율하고 스토어의 액션 핸들러가 올바른 순서로 실행되도록 관리한다.

KakaoTalk_Photo_2017-06-29-12-48-54_46

디스패처는 플럭스 아키텍처에서 핵심적인 역할을 하지만 개발자는 디스패처가 하는 일에 대해 거의 고려할 필요가 없다. 디스패처로 사용할 인스턴스를 만들기만 하면 디스패처 구현이 모든 작업을 알아서 처리한다.

실용성이 없는 최소 플럭스 앱

플럭스는 복잡한 어플을 개발할 때 코드를 읽고, 유지 관리하며, 규모를 키우기 쉽게 만들어준다. 즉, 복잡성을 낮추므로 결과적으로 작성해야 하는 코드 줄 수가 줄어든 것이 일반적이다. 그런데 이 장점은 첫 번째 예제 프로젝트에는 해당하지 않는다. 첫 번쨰 예제는 플럭스 예제의 모든 요소를 설명하기 이한 것이므로 오히려 작성할 코드가 늘어난다. 플럭스는 초보자에게 다소 어려울 수 있으므로 실용적인 UI는 겅의 없고 플러스 + 리액트 어플의 모든 요소를 명확하고 잘 정의된 방식으로 이용하는 아주 간단한 프로젝트를 첫 번쨰 예제로 작성해보자. 이보다 현실적인 예제는 그 다음절에서 다뤼보자.

은행계좌 애플리케이션

플럭스의 액션과 스토어를 설명하는 방법으로 은행계좌를 이용하는 예제는 제레미 도렐이 “과거를 잊는 자는 디버그를 하게 되리니..”라는 프리젠테이션에서 처음 소개했으며, 이 책에는 그의 허락을 맡고 실었다

은행계좌는 거래(Translation)과 잔고(balance)라는 두 가지 상태로 정의한다.

다음 예제는 이러한 항목으로 구성된다.

  1. constants.js 모든 액션은 앱 전체에서 고유하게 식별 가능한 이름이 있어야 하므로 이러한 이름을 여기에서 상수로 저장한다
  2. 표준 AppDispatcher.js
  3. BankActions.js 파일 세 액션 생성자(CreateAccount, despositIntoAccount, WithdrawFromAccount)를 포함한다. 액션도 사실은 객체이므로 이러한 객체를 생성하고 발송하는 메서드를 “액션 생성자”라고 부르기로 하자

  4. BankBalanceStore.js 파일 사용자의 잔고를 추적한다

  5. App.js 이프로젝트에서 이용할 UI컴포넌트 하나를 포함한다.
애플리케이션의 상수

constants.js 파일을 정의하면, export default { CREATE_ACCOUNT: ‘created account’, WITHDREW_FROM_ACCOUNT: ‘withdrew from account’, DEPOSITED_INTO_ACCOUNT: ‘deposited into account’ }

디스패처

다음으로 애플리케이션의 디스패처를 정의한다. 앞서 언급했듯이, 디스패처에 대해서는 크게 고려할 필요가 없다. 즉, AppDispatcher파일에서 간단하게 플럭스 디스패처의 인스턴스를 생성하기만 해도 된다.

import {Dispatcher} from 'flux';
export default new Dispatcher();

표준 디스패처를 확장해 원하는 작업을 하는 것도 가능하다. 예제 6-2와 같이 발송하는 모든 액션을 로깅하면 디스패처의 역할을 이해하는 데 도움이 된다.

액션 생성자

이제 본격적으로 뱅킹어플을 시작해보자. 우선 액션을 생성하는 함수를 정의해야 한다. 플럭스 어플에서 액션은 타입과 선택적 데이터 페이로드를 포함하는 객체다. 아직 더 좋은 용어가 없기 때문에 액션을 정의하고 발송하는 함수를 액션 생성자라고 하자. 다음 세 액션 생성자를 포함하는 BankActions.js파일을 만든다.

import AppDispatcher from './AppDispatcher'
import bankConstants from './constants'
let BankActions = {
    /**
     * 빈 값으로 계좌를 개설
     */
    createAccount(){
        AppDispatcher.dispatch({
            type: bankConstants.CREATE_ACCOUNT,
            amount: 0
        });
    },

    /**
     * amount 매개변수는 입금할 금액이다
     */

    depositIntoAccount(amount){
        AppDispatcher.dispatch({
            type: bankConstants.DEPOSITED_INTO_ACCOUNT,
            amount: amount
        })
    },

    /**
     * amount 매개변수는 출금할 금액이다
     */
    withdrawFromAccount(amount){
        AppDispatcher.dispatch({
            type: bankConstants.WITHDREW_FROM_ACCOUNT,
            amount: amount
        });
    }
};

export default BankActions;
스토어

다음은 BankBalanceStore파일을 정의할 차례다. 플럭스 애플리케이션에서 스토어는 소유한 상태를 저장하며 자신을 디스패처에 등록한다. 액션이 발송될 때마다 모든 스토어가 호출되며 해당 액션과 연관되는지 여부를 결정할 수 있다. 연관되는 경우 스토어는 내부 상태를 변경하고 이벤트를 생성해 스토어가 변경된 것을 뷰에 알린다.

이벤트를 내보내려면 npm을 통해 node.js의 이벤트 방출기 패키지를 가져와야 하는데 기본 이벤트 방출기는 브라우저에서 지원되지 않는다. npm에는 브라우저에서 Node의 이벤트 시스템을 재구현하는 다양한 패키지가 있으며, 페이스북도 속도와 단순함에 중점을 두는 간단한 오픈소스 패키지 구현을 가지고 있다. npm install –save fbemitter 명으로 이 패키지를 설치한다.

BankBalanceStore의 기본 구조에서는 이벤트 방출기 인스턴스를 생성하고 스토어 변경 이벤트를 구독하기 위한 addListener 메서드를 제공한다. 또한 애플리케이션 디스패처를 임포트하여 발송된 모든 액션에 대해 호출되는 콜백을 제공하고 스토어를 등록한다.

import {EventEmitter} from 'fbemitter';
import AppDispatcher from './AppDispatcher'
import bankConstants from './constants'

const CHANGE_EVENT = 'change';
let __emitter = new EventEmitter();

let BankBalanceStore = {
    addListener: (callback) => {
        return __emitter.addListener(CHANGE_EVENT, callback);
    },
};

BankBalanceStore._dispatchToken = AppDispatcher.register((action) => {
    switch (action.type){

    }
});

export default BankBalanceStore;

이 코드에서는 콜백 함수를 전달하고 디스패처의 register메서드를 호출한다. 콜백 함수는 액션이 발송 될 때마다 호출되며, 발송된 특정 액션 타입과 현재 스토어가 연관되는지 여부를 여기에서 결정할 수 있다.

또한 디스패처의 register 메서드는 스토어의 업데이트 순서를 조율하는 데 이용할 수 있는 식별자인 디스패치 토큰을 반환하는데, 이에 대해서는 뒷부분에서 알아본다.

그 다음은 계좌 잔고를 저장하는 변수(그리고 이값에 접근하는 접근자 메서드)를 정의하고 CREATE_ACCOUNT, DEPOSITED_INTO_ACCOUNT, WITHDREW_FROM_ACCOUNT 액션에 응답하도록 switch문을 작성해야 한다. 또한 계좌 잔고의 내부 값을 변경한 후에는 수동으로 변경 이벤트를 방출해야 한다