본문 바로가기

react

[react] FLUX 디자인 패턴의 이해2 : 내 방식대로 설명하기

이전 글에서는 Flux 디자인 패턴을 이해하기 위해 데이터 바인딩에 대해 다루었다.
https://chartist1206.tistory.com/66

 

이번에는 Flux 디자인 패턴에 대해 다룬다. 이 패턴에 대해 다루는 정석 내용은 대개 이런 키워드를 다룰 것 이다.

  • 페이스북 팀에서 리엑트 기반 웹 프론트를 유지보수할 때 상태 업데이트를 제대로 예측하지 못해 몇 가지 고생을 했고, 이에 대한 대안으로 만든 상태 관리 디자인 패턴이다.
  • 기존의 상태 관리 디자인 패턴은 model과 view가 흩어져 있고 서로 간에 참조 및 데이터 전달 방향과 범위가 불명확하여 예측이 어려웠다.
  • flux 패턴은 model과 데이터 바인딩 로직을 중앙 집중화 하여 문제를 해결하였다.
  • 구체적인 데이터 흐름은 action => dispather => store => controller view
  • 전역 상태 관리 라이브러리인 redux가 해당 패턴을 사용하지만 기존의 state + props에 useReducer로도 구현 가능한 디자인 패턴이다.

이런 설명들은 가치가 있다. 다만, 내 경우에는 MVC 개념이나 view-model에 대해 잘 알지 못했어서 flux를 이해하고 실제로 직접 활용하는 데 많은 시간과 경험이 필요했다. 따라서, 내 방식대로 flux 디자인 패턴에 대해 설명해보려고 한다.

 

FLUX 패턴의 등장 배경


FLUX는 새로운 문법이나 API가 아니라 디자인 패턴이다.

리엑트의 기본적인 API는 컴포넌트 디자인 + 단방향 데이터 바인딩 + 단방향 데이터 흐름이다. 

각 용어의 개념은 아래와 같다.

  • 컴포넌트 디자인
    • 하나의 페이지는 form, header, navigation 등의 서로 다른 관심사를 가진 html + javasciprt로 구분된다.
    • 리엑트의 소스 코드는 하나의 페이지를 모두 작성하는 대신, 컴포넌트라는 단위로 분리한다.
    • 컴포넌트는 model(useState, useEffect처럼 데이터를 읽고, 변환하고, 저장하는 로직)과 view(model 혹은 state의 데이터를 가져와서 jsx형식으로 html을 렌더링하는 부분)로 구성된다. view-model이라는 용어는 컴포넌트 단위로 두 가지 관심사가 한 데 있는 것과 관련 있다.
  • 단방향 데이터 바인딩
    • setState로 state가 변경되면 자동으로 컴포넌트 함수가 리렌더링된다.(클래스형 컴포넌트도 있지만, 여기서는 함수형으로 언급하겠다) 그에 따라 state를 참조하고 있던 view의 jsx에도 새롭게 갱신된 값이 렌더링된다.
    • 반대로 view에서 model을 변경하는 것은 개발자가 setState를 이용하여 수동으로 코드를 작성해야 한다.
  • 단방향 데이터 흐름
    • 컴포넌트는 더 작은 컴포넌트로 분해 가능하다. 예컨대, header는 menu, mainLogo, search bar 등으로 분해가능하다. 사실, 페이지도 사용자가 보는 화면에서 가장 큰 컴포넌트에 속한다.
    • 때로는 컴포넌트 간에 state와 state 변경 로직을 공유해야 할 때가 있다. 예컨대, form에서 사용자의 입력 값을 DataRequest 라는 컴포넌트가 데이터 요청을 위해 공유하여 api를 요청하고, api의 응답을 Modal 컴포넌트가 참조하여 사용자에게 응답을 표출하는 식이 될 수 있다.
    • 컴포넌트는 부모 컴포넌트에서 자식 컴포넌트에게 props로 데이터를 전달할 수 있지만 반대로는 하기 어렵다.

실무에서 하나의 페이지는 여러 개의 페이지와 각 페이지를 구성하는 수많은 컴포넌트, 그리고 각 컴포넌트에 있는 state, 경우에 따라 props를 이용한 데이터 공유를 한다.

이 때, 파생 상태를 다루는 문제가 발생한다.

 

파생 상태란?

파생 상태란 하나의 상태(state)가 변경될 때, 변경되는 상태값에 의존하여 함께 변경되어야 하는 상태를 의미한다.

앞서 단방향 데이터 흐름 예시에서 form의 사용자 입력 데이터, api 요청 파라미터, 모달에 표출될 api 응답은 적힌 순서대로 파생 관계에 있다.

그러면 코드 상에서 한 상태가 변경될 때 그것을 감시하여 다른 상태의 변경을 할 수 있는 방법은 무엇일까?
여러 가지가 있지만 하나는 useEffect다.

useEffect는 두 개의 인자를 받으며 첫 번째 인자는 감시 대상이 변경 시 실행될 콜백, 두 번째 인자는 감시 대상 배열이다.

감시 대상 배열에 한 state를 넣고 콜백에서 파생 상태를 변경하면 된다.

 

문제: 데이터의 변경 흐름과 영향 범위를 예측하기 어렵다.

한 페이지가 여러 컴포넌트로 분할되고 각 컴포넌트가 state를 가지고 props로 공유를 한다. 때때로 컴포넌트 내 useEffect로 파생 상태를 변경한다. 이런 흐름의 구조가 커지면 문제가 생긴다. 내가 a라는 상태를 변경했을 때, 파생 상태가 어디까지 변경될지 모른다. 더 큰 문제는 서로 연결된 파생 상태들이 올바른 순서대로 변경되는지 확인하기 어렵다. 예컨대 내 예상은 a 변경 시 b => c => d로 변경되는 거지만, 때로는 b => d => c 가 될 수도 있고, 내 코드를 잘 이해하지 못한 다른 동료가 손을 데서, a를 빼먹고 b => c => d만 변경할 수도 있다. 부모 => 자식으로만 변경이 적용되어야 하는 데이터 흐름이 컴포넌트 분할과 state의 분할, 파생 변경 로직의 분할 과정에서 양방향적으로 예측하기 어렵게 흐르는 것이 문제의 주 원인이다. 

 

대안 : 데이터를 다시 단방향으로 흐르게 하자.

그러던 중 누가 대안을 냈다.

  • 부모 컴포넌트가 모든 state를 들고 있자. 자식의 자자식의 자자자식까지 쓸 state를.
  • 부모의 state를 변경할 수 있는 setState는 props로 직접 내리지 않는다. 대신, reducer 함수와 action 함수를 만든다.
    • action은 데이터 변경을 요청하는 함수이다. 이 함수는 직접 setState를 호출하는 로직을 가지고 있지 않다. 대신, 자신이 원하는 작업명과 전달할 데이터를 들고 있다. action을 호출하면 reducer로 action함수의 작업명과 데이터가 전달 된다.
    • reducer는 switch 문으로 action으로부터 전달받은 작업명을 찾는다. 그리고 거기에 일치하는 작업을 실행하고 action이 전달한 데이터를 토대로 setState를 실행한다.

그러면 데이터 바인딩은 다음같이 작동한다.

  1. "view에서의 action 함수로 데이터 변경 요청
  2. action 함수에서의 요청은 모두 부모의 setState 함수로 모임.
  3. setState 로직에는 state와 파생 state가 모두 있음으로 하나의 함수 내에서 state 변경 순서를 조율 가능함.
  4. 부모 컴포넌트의 state가 변경되어 리렌더링되면 props를 타고 자식들에게 새로운 state가 내려지며 리렌더링됨

이러면 이전의 구조에서 한 state 변경 시, 어딘가의 useEffect가 state를 변경하고 또 어딘가의 useEffect가 state를 변경하는, 추적하기 번거로운 구조를 하나의 setState 함수 내에서 감시 가능하다.

요약하자면, 개별적으로 일어나는 다양한 데이터 변경 요청을 하나의 전역적인 허브에서 관리함으로써 데이터 흐름을 한방향으로 만들고, 이를 통해 디버깅을 용이하게 만든다는 것이다. 이것이 flux 디자인의 컨셉이다.

 

FLUX 패턴의 구조


코드 상에서 flux 패턴의 구체적 작동 구조는 아래와 같다.

 

액션(action)

  • 데이터 변경을 요청하는 내용의 객체 값이다.
  • 예컨대, form의 onSubmit 이벤트에서 새로운 계정 생성을 요청할 때는 아래처럼 요청의 목적과 내용을 실어서 디스패처로 보낼 것이다.
{
	type:"CREATE_NEW_ACCOUNT",
	payload:{
		name:"jjang-gu",
        password:1234, 
        nickname:"jjang88"
	}
}
  • 일반적으로 액션은 직접 객체를 생성하기 보단, 액션 생성자 함수를 만들어서 반환 값의 일관성을 보장하는 것이 좋다. 또한, 액션 생성자를 별도의 유틸 함수처럼 한꺼번에 관리하면 상태 관리 api를 파악하는데 용이할 것이다.

 

디스패처(dispatcher)

  • 여러 컴포넌트의 view에서 전달된 액션이 모이는 거대한 함수다.
  • flux 디자인의 컨셉에 맞게 단일한 dispathcer을 사용하기 위해선 context api와 useReducer를 사용하면 된다.
  • 디스패처는 전달된 액션을 모든 스토어에 전달한다. 단, 이 처리방식은 동기적이므로 업데이트 요청이 다발적으로 다른 model을 변경하는 경우 흐름을 조정하는데 도움이 된다.

 

스토어(store)

  • 상태값과 상태값 업데이트 로직(useReducer의 reducer을 생각하면 이해가 쉽다)이 들어있다.
  • 디스패처로 들어온 액션을 타입에 따라 분기 처리하는데, 보통 switch statement를 많이 사용한다.
  • 작업 완료 후 컨트롤러 뷰에게 데이터가 변경 되었음을 이벤트로 알린다.

 

컨트롤러 뷰(controller view)

  • 스토어에서 값을 가져와 view에 props로 전달한다.
  • 스토어의 변경 이벤트를 수신하면 스토어에서 다시 데이터를 가져와서 view에 전달한다.

 

FLUX 패턴의 작동방식


최초 세팅

  1. 스토어는 디스패처에 콜백 함수를 통해서 액션을 전달 받을 수 있도록 설정한다.
  2. 컨트롤러 뷰는 렌더링을 위해 스토어로부터 데이터를 가져와서 자신과 자식 컴포넌트에 전달한다.
  3. 컨트롤러 뷰는 스토어에게 데이터의 변경에 대한 알림을 이벤트를 등록한다.

 

데이터 변경 요청과 흐름

  1.  view에서 유저의 상호작용을 이벤트 핸들러 등으로 감지하고 액션 생성자를 통해 액션을 만들어 디스패처로 보낸다.
  2. 디스패처는 액션의 순서에 따라 알맞은 스토어로 보낸다.
  3. 스토어는 디스패처로부터 모든 액션을 수신받을 수 있지만, 스위칭 문에 따라 필요한 것만 처리하고 데이터를 변경한다.
  4. 데이터 변경 후 스토어는 변경을 컨트롤러 뷰에 알린다.
  5. 컨트롤러 뷰는 데이터를 다시 받아서 리렌더링한다.