원문보기

Redux와 같은 Flux구현체는 앱 상태에 대해 생각하고 모델링하는 데 시간을 쓰기를 명시적으로 권장하고 있다. 그것이 사소한 일이 아니라는 것으로 밝혀졌다. 카오스 이론의 고전적인 예다. 겉으로 보기에는 무해한 가벼운 날개짓이 나중에는 우발적인 복잡성의 허리케인을 유발할 수 있다. 다음은 비즈니스 로직과 자기 자신을 가능한한 정상적으로 지켜낼 수 있는 방법에 대한 실용적인 팁들의 목록이다.

앱 상태란 정확히 무엇인가

Wikipedia에 따르면 컴퓨터 프로그램은 메모리에 변수를 나타내는 특정 위치에 데이터를 저장한다. 이러한 메모리 위치의 내용을 프로그램 실행 시 특정 시점의 상태라고 한다.

우리는 이 맥락에 추가로 minimal이라는 단어를 추가하는 것이 중요하다. 명시적인 제어를 위해 앱 상태를 모델링할 때 상태를 표현하는데 필요한 최소한의 데이터만 처리하고 이 핵심적인 데이터 외의 다른 부수적인 변수들은 무시하는 데 최선을 다해야 한다.

Flux 애플리케이션에서는 앱 상태를 스토어에 둔다. 디스패치된 액션이 이 상태의 변경을 유발한다. 그리고 이 상태 변경을 구독한 뷰가 그에 따라 다시 렌더링된다.

이 논의를 위해 우리가 선택한 Flux 구현체인 Redux에서는 몇 가지 엄격한 요구사항을 추가했다. 모든 앱 상태를 하나의, 불변적인, 스토어에 두는 것이다. 그리고 상태는 보통 직렬화 가능한 상태로 둔다.

아래에 설명한 팁들은 Redux를 사용하지 않는 경우에도 관련이 있다. 혹은 Flux를 전혀 사용하지 않더라도 보면 좋은 내용이 있다.

1. server API 호출로부터 상태를 모델링하는 것을 피하라

앱의 로컬한 상태는 종종 서버로부터 비롯된다. 원격 서버에서 도착한 데이터를 표시할 때, 데이터 구조를 그대로(as-is) 유지하는 것이 좋아 보일 수 있다.

전자상거래 상점 관리 앱을 생각해보자. 판매자는 이 앱을 사용해서 상점의 인벤토리를 관리하기 때문에, 제품 목록을 표시하는 것이 핵심 기능이다. 제품 목록은 서버에서 비롯되었지만 내부에서 렌더링하기위해 애플리케이션 상태에 로컬로 저장해야 한다. 서버에서 제품 목록을 검색하는 기본 API가 다음 JSON 결과를 리턴한다고 가정하자:

제품 목록이 객체 배열로 도착하는데, 객체 배열로 저장하는 게 어떨까?

서버 API의 결정에는 많은 다른 고려사항이 뒤따른다. 로컬 앱 상태 구조가 달성하고자 하는 목표는 이와 일치하지 않을 수 있다. 위 경우에는, 배열로 보내는 서버의 선택이 고려하고 있는 것은 전체 목록을 작은 덩어리로 분할하여 필요에 따라 데이터를 다운로드하고 대역폭을 절약하는 것 이상으로 동일한 데이터를 재전송하는 것도 역시 피하기 위해 페이지별로 응답하는 것이다. 모두 유효한 네트워크 고려사항이겠지만 - 일반적으로는 우리 앱의 로컬 상태가 고려해야할 사항은 아니다.

2. 배열 보다는 맵을 선호하라

일반적으로 배열은 상태를 유지하는데 편하지 않다. 특정 제품을 업데이트하거나 검색해야할 때 어떤 일이 발생하는지 고려해보라. 예를들어 앱이 가격을 수정하는 데 사용되거나 서버의 데이터를 리프레시하는 경우가 여기에 해당된다. 특정 제품을 찾기 위해 큰 배열을 이터레이팅하는 것은 ID에 따라 제품을 쿼리하는 것 보다 훨씬 덜 편리하다.

권장되는 접근법은 무엇인가? 쿼리에 사용되는 프라이머리 키를 index로 맵을 사용하는 것이다.

즉, 위 예제의 데이터를 앱 상태로는 다음과 같은 구조로 저장할 수 있다:

정렬 순서가 중요한 경우는 어떨까? 예를 들어, 서버에서 반환한 순서가 사용자에게 보여야하는 순서와 동일한 경우다. 이런 경우에는 ID의 배열만 추가로 저장할 수 있다.

흥미로운 참고사항: 만약 React Native의 ListView 컴포넌트에 데이터를 표시하려는 계획이 있다면, 이 구조는 실제로 매우 잘 작동한다. 안정적인 행 ID를 지원하는데 추천하는 cloneWIthRows 버전은 정확히 이 형식을 기대한다.

3. 어떤 뷰가 소비하는 상태로 모델링하는 것을 피하라

앱 상태의 최종 목적은 뷰로 전파되어 사용자의 기호에 맞게 렌더링되는 것이다. 추가적인 변환 비용을 피하기 위해서 정확히 어떤 뷰가 받기를 원하는 구조로 상태를 스토어에 저장하는 것이 꽤 매력적으로 보인다.

전자 상거래 매장 관리 사례로 돌아가보자. 모든 제품이 재고가 있거나 혹은 없을 수 있다. 이 데이터를 boolean 형태로 저장할 수 있다.

우리 애플리케이션은 재고가 없는 모든 제품 목록을 표시해야 한다. 위에서 언급했듯이 React Native의 ListView 컴포넌트는 conleWithRows 메소드에 2개의 아규먼트를 요구한다: rows 맵과 rowId의 배열. 상태를 모델링해서 준비할 때, 이 목록을 명시적으로 두고 싶다. 이렇게하면 추가 변환 없이 ListView에 두 인수를 모두 제공할 수 있다. 결국 상태는 다음과 같은 구조가 될 것이다:

썩 괜찮은 아이디어 같지 않나? 음.. 그렇지 않다는 것이 밝혀졌다.

그 이유는 바로, 이전과 같이, 뷰는 다른 종류의 고려사항들에 의해 가이드된다는 것이다. 뷰는 상태를 최소화 상태로 유지하는 데는 관심이 없다. 사용자를 위해서 데이터가 배치되어야 하기 때문에 완전히 반대쪽을 선호한다. 다른 뷰들은 같은 상태 데이터를 다른 식으로 표현하기 때문에 데이터 중복 없이 그들 모두를 만족시키는 것은 절대 불가능하다.

이제 이 문제를 통해 다음 사항으로 넘어가보자

4. 앱 상태에 절대 중복된 데이터를 두지 말아라

상태가 중복 데이터를 보유하고 있는지 여부를 테스트하는 좋은 방법은 일관성을 유지해야할 때, 두 곳을 한번에 업데이트하는지 확인해보는 것이다. 위의 재고가 없는 제품에 대한 예제에서 첫 번째 제품이 갑자기 재고가 없어졌다고 가정해보자. 이 업데이트를 처리하려면 맵 내의 outOfStock 필드와 outOfStockProductIds에 이 Id를 추가해야 한다. - 두 개의 업데이트다.

중복 데이터를 처리하는 것은 간단하다. 인스턴스 중에서 하나를 제거하면 된다. 단일 지식의 원천 원칙에 의해, 데이터가 한 번만 저장되면, 더 이상 일관성이 유지되지 않는 상태에 도달할 수 없다.

outOfStockProductIds 배열을 제거하면 뷰가 소비할 이 데이터를 준비할 방법을 찾아야 한다. 이 변환은 뷰에 제공되기 직전에 런타임에 수행되야 한다. Redux 애플리케이션에서 추천하는 방식은 이를 셀렉터에 구현하는 것이다.

셀렉터는 상태를 입력으로 변환된 상태 일부분을 반환하는 순수 함수다. Dan Abramov는 일반적으로 리듀서와 셀렉터가 단단히 결합되기 때문에 같은 파일에 둘 것을 권장한다. 뷰의 mapStateToProps 내부에서도 셀렉터를 사용할 것이다.

배열을 제거하는 또다른 대안은 모든 제품에서 outOfStok 속성을 제거하는 것이다. 이 대체 접근법에서는 어레이를 단일 지식의 원천으로 유지할 수 있다. 실제로, #2 팁에 따르면 배열을 맵으로 변경하는 것이 더 나을 것이다.

5. 앱 상태에 절대 파생 데이터를 두지 마라

단일 지식의 원천 원칙은 복제된 데이터 뿐만이 아니다. 스토어에서 어떠한 파생 데이터라도 발견된다면 일관성을 유지하기 위해 여러 위치를 업데이트해야 하기 때문에 이 원칙을 위반한다.

매장 관리 사례에 다른 요구사항을 추가해보자. 제품을 판매하고 가격에 할인을 추가할 수 있는 기능이다. 앱은 모든 제품, 할인 안되는 제품, 할인되는 제품 을 각각 필터링해서 사용자에게 보여야 한다.

흔히 저지르는 실수는 스토어에 3개의 배열을 유지하는 것이다. 각 배열에는 각 제품에 대한 관련 제품의 ID가 들어있다. 3개의 배열은 현재 제품 맵과 현재 필터 양쪽에 의해 파생될 수 있으므로, 더 나은 접근법은 이전처럼 셀렉터를 통해 이를 생성하는 것이다.

셀렉터는 모든 상태 변경에 대해서 뷰가 재 렌더링되기 직전에 실행된다. 셀렉터가 계산해야 하는 내용이 많고 이에 대한 성능이 우려되는 경우에는 메모이제이션을 적용하여 결과를 캐시헤라. 이러한 최적화를 구현하는 Reselect 라이브러리를 살펴보라.

6. 중첩된(nested) 객체를 정규화하라

일반적으로 이 팁들에 대한 동기는 단순하다. 상태는 시간이 지남에 따라 관리되어야 하며 가능하면 이를 고통스럽지 않게 만들고 싶다는 것이다. 데이터 객체가 독립적일 때는 단순성을 유지하기 쉽지만, 상호 연결된 데이터를 가지게 된다면 어떨까?

상점관리 앱에서 다음 예를 고려해보라. 여러 개의 제품을 한 주문으로 구매하는 기능을 추가한다고 해보자. 다음 JSON을 주문 목록으로 리턴하는 서버 API를 가정해보자:

order는 여러 개의 제품을 포함하고 있기 때문에, 모델링해야할 둘 사이에 관계가 있게 된다. 팁 #1에서 API 응답 구조를 그대로 사용해서는 안된다는 것을 이미 알고 있다. 실제로 제품 데이터의 중복을 초래할 수 있으므로 문제가 되는 것 같다.

이 경우 좋은 접근법은 데이터를 정규화하고 별도로 두 개의 맵을 유지하는 것이다—하나는 제품용, 하나는 주문용. 양쪽 타입의 객체는 모두 고유의 ID에 기반하고, ID 속성을 사용하여 특정 연결을 지정할 수 있다. 결과적으로 앱 상태는 다음과 같다:

특정 주문의 일부인 모든 제품을 찾길 원한다면, products의 key로 이터레이트하면 된다. 각 키는 product ID다. 이 아이디로 productsById를 엑세스하면 제품의 세부 정보가 제공된다. 이 주문과 관련된 추가적인 제품 세부정보(예: giftWrap)은 주문에 따라 products 맵 아래의 값들에서 찾을 수 있다.

API의 응답을 정규화하는 과정이 지루한 경우에는 normalizr처럼 스키마를 취해서 정규화 과정을 제공하는 헬퍼 라이브러리를 살펴보라.

7. 앱 상태는 in-memory 데이터베이스로 간주될 수 있다

지금까지 살펴본 다양한 모델링 팁은 우리에게 친숙해야만 한다. 왜냐면 우리는 DBA라고 써진 모자를 쓰고 전통적인 데이터베이스 설계를 할 때와 비슷한 선택을 했기 때문이다.

전통적인 데이터베이스 구조를 모델링할 때, 중복 및 파생물을 피하고, 기본 키(ID)를 사용하여 맵이랑 비슷한 테이블에서 데이터를 인덱싱하고 여러 테이블 간의 관계를 정규화한다. 우리가 지금까지 이야기했던 꽤 모든 것들과 유사하다.

in-memory 데이터베이스처럼 앱 상태를 처리하면 올바를 구조에 대한 결정을 내릴 수 있는 정확한 마음가짐을 갖는 것에 도움이 될 것이다.

앱 상태를 퍼스트 클래스 시티즌으로 간주하라

이 포스트에서 한 가지만 취하라면 아마 이것일 것이다.

명령형 프로그래밍에서는 코드를 왕으로 취급하는 경향이 있으며 상태와 같은 내부적이고 묵시적인 데이터에 대해 “올바른” 모델에 대해 걱정하는 데 적은 시간을 보냈다. 우리 앱 상태는 일반적으로 여기저기 흩어진 다양한 매니저들이나 컨트롤러들 사이에서 유기적으로 자라나는 사유 재산으로써 찾아낼 수 있었다.

선언적인 패러다임 아래서는 모든것이 다르다. React와 같은 환경에서는 시스템이 상태에 반응한다. 이 상태는 이제 퍼스트 클래스 시티즌이며 우리가 작성한 코드만큼 중요하다. 상태는 Flux의 액션들의 목적이며, Flux의 View들의 진실의 원천이다. Redux와 같은 라이브러리는 상태를 중심으로 돌며 immutablity와 같은 도구를 고용하여 더 예측가능할 수 있게 한다.

우리는 앱 상태를 생각하는데 많은 시간을 할애해야 한다. 우리는 살태를 관리하는 코드가 얼마나 에너지가 들고 얼마나 복잡해지는지 조심해야 한다. 그리고 우리는 코드가 부패하기 시작하는 징후를 보일 때, 코드를 분명히 리팩토링 해야 한다.

리액트의 도(Tao of React)

[원문보기](https://alexkondov.com/tao-of-react/)저는 2016년 부터 리액트를 가지고 작업을 해왔지만 여전히 어플리케이션 구조나 설계에 대한 하나의 모범 사례는 없는 것 같습니다.마이크로 레벨의 모범 사례는 있었지만...… Continue reading