원문보기

CSS는 이상하다. 15분 내로 기본 내용을 익힐수 있다. 그러나 스타일을 조직하는 좋은 방법을 알아내기까지는 수 년이 걸릴 수 있다.

일부는 언어 그 자체의 단점 때문이다. CSS 그 자체로는 한계가 있다. 변수나 루프, 함수 등이 없다. 그와 동시에, 엘레멘트, 클래스, ID의, 혹은 그들의 어떻게 조합하더라도 스타일링할 수 있도록 허용하고 있다.

혼돈의 Style Sheets

당신 스스로 경험했듯이, 종종 혼돈을 만들어내는 조리법이 있다. SASS나 LESS같은 전처리기는 많은 유용한 피처들을 추가해주지만, 실제로 CSS의 난해함을 멈추기에는 부족하다.

BEM과 같은 방법론에 맡겨진 조직화 업무는 - 유용하지만 - 완전히 선택사항일 뿐이고, 언어나 툴 레벨에서 이를 강제할 수 없다.

CSS의 새 물결

몇년 전부터 자바스크립트 기반의 툴을 사용하려는 새로운 물결이 CSS를 쓰는 방법을 바꿔버리는 식으로 이러한 문제를 발본색원하려고 노력하고 있다.

Styled-Components는 이러한 라이브러리 중 하나다. 그리고 혁신과 친숙함의 조합을 무기로 많은 이들의 관심과 주목을 받았다. 만약 React를 사용한다면(만약 아니라면 내 자바스크립트 스터디 계획React 소개글을 확인하라), 이 새로운 CSS의 대체제를 살펴보는 것이 좋을 것이다.

나는 최근 개인 사이트를 다시 디자인하기 위해서 이것을 사용해보았고, 그 과정에서 배운 몇 가지를 공유하기를 바랐다.

컴포넌트, 스타일링된

Styled-Components에 대해 이해해야만 할 주요 사항은 그 이름을 문자 그대로 대해야 한다는 것이다. 당신은 더이상 HTML 엘레멘트를 스타일링하거나, HTML 엘레멘트나 클래스를 기반으로하는 컴포넌트를 스타일링하지 않는다:

<h1 className="title">Hello World</h1>

h1.title{
  font-size: 1.5em;
  color: purple;
}

이렇게 하는 대신, 자신만의 캡슐화된 스타일을 지닌 styled compoents를 정의한다. 그 다음, 자유롭게 당신의 코드베이스에서 이를 사용한다.

import styled from 'styled-components';

const Title = styled.h1`
  font-size: 1.5em;
  color: purple;
`;

<Title>Hello World</Title>

이것은 사소한 차이처럼 보일 수 있고, 사실 양쪽의 문법은 매우 유사하다. 그러나 중요한 차이점은 스타일이 그 컴포넌트의 일부라는 점이다.

다른 말로 하자면, 컴포넌트와 스타일 사이의 중간단계인 CSS 클래스를 제거하는 것이다.

Styled-Components의 공동 제작자인 Max Stoiber가 말하기를:

“Styled-Components의 기본 아이디어는 스타일과 컴포넌트간의 매핑을 제거함으로써 베스트 프랙티스를 강요하는 것이다”

복잡성 제거하기

처음에는 반-직관적인 것처럼 보일 것이다. HTML 엘레멘트( 태그를 기억하는가?)를 직접 스타일링하는 것 대신 CSS를 사용한다는 것의 요점은 중간에 클래스 계층을 도입하여 마크업과 스타일을 분리하는 것이다.

하지만 이 분리로 인해 많은 복잡성 또한 생겨났고, CSS와 비교할 때, 자바스크립트같은 “진짜” 프로그래밍 언어가 이러한 복잡성을 처리할 수 있는 능력이 훨씬 뛰어나다는 주장이 있다.

클래스 대신 props

노-클래스의 철학을 유지하기위해, Styled-Components는 컴포넌트의 행동을 커스터마이징할 때 클래스 대신 props를 사용하게 한다. 따라서 다음과 같이 하는 대신에:

<h1 className="title primary">Hello World</h1> // will be blue

h1.title{
  font-size: 1.5em;
  color: purple;

  &.primary{
    color: blue;
  }
}

이렇게 작성한다:

const Title = styled.h1`
  font-size: 1.5em;
  color: ${props => props.primary ? 'blue' : 'purple'};
`;

<Title primary>Hello World</Title> // will be blue

보다시피 Styled-Components를 사용하면 모든 CSS와 HTML을(그와 관련된 바깥쪽의 모든 구현 세부사항) 소유함으로써 당신의 React 컴포넌트를 말끔하게 정리해준다.

즉, Styled-Components의 CSS는 여전히 CSS다. 따라서 다음과 같은 (비록 약간 비관용적이지만) 코드도 완전히 유효하다:

const Title = styled.h1`
  font-size: 1.5em;
  color: purple;

  &.primary{
    color: blue;
  }
`;

<Title className="primary">Hello World</Title> // will be blue

이것은 Styled-Components를 쉽게 도입할 수 있는 한 가지 기능이다: 의심스럽다면 언제든지 당신이 아는 상태로 돌아갈 수 있다!

주의사항

Styled-Components는 매우 어린 프로젝트고, 일부 기능은 아직 완전히 지원되지 않는다는 것도 중요하게 언급한다. 예를 들어 부모로부터 하위 컴포넌트에게 스타일을 지정하게 하려면 CSS 클래스에 의존해야 한다(적어도 버전2가 나오기 전까지는)

서버에서 CSS를 미리 렌더링하는 어떠한 “공식적인” 방법도 또한 존재하지 않는다. 스타일을 수동으로 주입함으로써 분명 가능하긴 하지만 말이다.

그리고 Styled-Components가 자체적으로 랜덤한 클래스 이름을 생성한다는 사실은 브라우저의 개발 도구를 사용하여 원래 스타일이 정의된 위치가 어디인지 찾는 것을 어렵게 만든다.

그러나 매우 고무적인 것은 Styled-Components 코어 팀이 이 모든 이슈를 인식하고, 하나씩 하나씩 열심히 수정하고 있다는 것이다. 버전 2가 곧 출시될 예정이고, 나는 굉장히 기대중이다!

자세히 알아보기

내 목표는 Styled-Components가 어떻게 작동하는지 자세히 설명하는 것이 아니라, 작은 것을 보여줌으로써 그것이 가치있는지 스스로 확인할 수 있게 하는 것이다.

만약 내가 호기심을 자극했다면, Styled-Components에 대해 더 알아볼 만한 장소가 몇 군데 있다:

  • Max Stoiber는 최근 Styled-Components에 대한 이유를 Smashing Magazine에 작성했다.
  • Styled-Components 저장소에서 자체적으로 광범위한 문서를 제공하고 있다.
  • Jamie Dixon의 글에서 Styled-Components로 전환하는 것의 이점들에 대해 밝히고 있다
  • 실제로 어떻게 이 라이브러리가 구현된건지 알고 싶다면, Max의 이 글을 읽어보라.

그리고 더 나아가고 싶다면, Glamor라는 또다른 CSS의 새 물결이 있다!

셀프-프로모션 타임: 폼, 데이터 로드, 사용자 계정을 갖춘 풀스택 React & GraphQL 앱을 만드는 가장 쉬운 방법인 Nova를 돕기 위해 오픈 소스 컨트리뷰터를 찾고 있습니다! 우리는 아직 Styled-Components를 사용하고 있지는 않지만 당신이 이를 구현하는 첫 번째 사람이 될 수도 있습니다!

원문보기

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와 같은 도구를 고용하여 더 예측가능할 수 있게 한다.

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

원문보기

코드 재사용을 최대화하는 Redux 스토어 정규화

Redux의 가장 큰 장점 중 하나는 애플리케이션 상태가 하나의 진리의 원천(single source of truth)을 포함한다는 것이다. 그 상태 데이터는 정규화된 구조로 저장된다. 따라서 최소한의 노력으로 비즈니스 로직을 공유하거나 확장할 수 있다. 단점은 Redux가 비일반적인 양의 보일러플레이트를 필요로 한다는 것이다. 그러나 이런 유연성이, 내 의견으로는, 당신으로 하여금 이 보일러플레이트를 구매하게 하는 주요 이점중에 하나다.

MobX와 같은 객체 지향 솔루션은 보일러플레이트를 줄이지만, 테스트가능하고, 응집되어 있으되, 지나치게 결합하지 않은 - 다른 말로는 객체 지향적인 - 애플리케이션 상태를 구성하는 당신 자신만의 방법을 요구한다. 이 선택지가 “더 좋은” 방법인 것은 아니다. 모든 프로젝트가 요구사항에 기초하여 절충해야만 하는 것이다. 강조하기 위해 추가하건대, 나는 MobX를 직장에서 프로페셔널하게 사용하고, 아주 좋아한다. OOP의 오버헤드가 Redux가 요구하는 것 보다 적을 수도 있고, 혹은 Knockout 같이 오래된 OOP 기반 프레임워크로 작성된 레거시 코드는 정규화된 Redux Store 보다는 MobX로 변환하는 것이 더 쉽다.

이 포스트는 Redux의 강점을 강조하는 유스케이스 하나를 보여준다. 일부러 찾으려고 한 것은 아니지만, 내 사이드 프로젝트 중 하나를 시작한 후로, Redux에서 특히 잘 작동하는 피처를 추가하고 있었다. 최근 내가 Redux에 제기된 비판을 본 것이 내가 이 글을 쓰는 동기가 되었다.

이 포스트는 Redux와 스토어를 정규화 하는 것에 익숙한 사람을 독자로 가정했다. 내 이전 포스트에서 이 문제에 대해 자세히 논의한 적이 있다. 아래의 샘플 코드는 메모이징 Redux 셀렉터 작성 툴인 reselect를 자유롭게 사용하고 있다.

유스케이스

이전과 마찬가지로 코드 샘플은 내 북-트랙킹 웹사이트에서 가져온 것이다. 나는 이것을 내 자신만을 위한 라이브러리로 사용하고 또한 여기에다 새로운 것을 시도한다. 나는 이것을 작성함으로써 React를 배웠고, 결과적으로 큰 성공과 함께 팀을 바꿨기 때문에 시간을 잘 보냈다고 할 수 있다 :)

특히, 사용자는 계층적인 주제의 컬렉션을 저장할 수 있다: 예를 들면, 미국 혁명은 미국 역사 아래 두고, 이는 다시 역사 아래 놓을 수 있다. 계층 구조는 Mongo에서 materialized paths로 저장된다.

나는 코드에서 주제들이 어떻게 저장되는지와, 정규화된 상태가 약간 미묘하게 다른 방식으로 재사용될 수 있는지 간략하게 설명할 것이다.

다음과 같은 내용을 강조한다: MobX에서는 이를 많은 추가 작업없이 달성하기가 힘들다는 것이다. 요점은 내가 보여주고 싶은 것이 Redux가 어떻게 잘 동작하는지 보여주는 것 뿐 아니라, 내 의견으로는, 더 나은 대안이 될 수 있다는 것이라는 것이다.

주제 저장하기

여기 내 주요 애플리케이션 레벨의 저장소에 대한 단순화 버전이 있다:

서브젝트가 되돌아왔을 떄, 그들은 해쉬에 평평하게 들어간다. 그러고나서, 이 해쉬를 다시 유용한 콜렉션으로 모양을 바꿔주는 메서드들이 익스포트되고, 애플리케이션 내 다른 부분들에 의해 필요에 따라 사용된다.

예를 들어 도서 목록을 표시하는 애플리케이션에서 대상을 필터에 의해 검색하고 선택하거나, 도서 목록에 도서를 추가하는 부분은 다음과 같다.

동일한 subjectHash가 애플리케이션 상태에서 다른 부분에 의해 추출되고, 그 다음에는 stackAndGetTopLevelSubjects 메서드로 모양을 바꾼다. 그리고 그 결과를 리듀서의 나머지 상태 부분과 결합한다.

코드 확장하기

더 흥미로운 부분은, 서브젝트의 상세 정보를 수정하는 애플리케이션의 또다른 파트다. 서브젝트들이 모두 계층적으로 목록에 표시되고, 사용자는 그들을 새로운 부모에게 드래그앤 드랍한다. 하지만, 서브젝트는 새로운 유효한 부모들에게 호버링 중일 때, 부모는 서브젝트가 하위 자식들로 들어오는 것이 보류 중임을 보여주고, 사용자는 드랍을 완료하기 전에 시각적인 유효성 검사를 볼 수 있게 된다. (스타일링은 언제나처럼 진행 중인 작업이다)

문제의 리듀서는 드랍이 보류 중임을 나타내는 액션을 받아들이고, 새로운 서브젝트 해쉬를 받아들이기 위해 새로운 임시적인 서브젝트를 새로운 부모 아래쪽에 보여주도록 살짝 조정한다.

기본적으로 5줄 미만의 if(currentDropCandidateId){가 서브젝트 해쉬의 shallow copy를 생성하고, 드래깅 중인 서브젝트의 복사본을 드랍 타겟의 자식의 경로를 갖도록 생성하고 추가한다. 그게 전부다.

그리고 currentDropCandidateId가 없으면 임포트된 subjectsSelectoris를 호출한다. 맞다, 이 셀렉터는 그저 stackAndGetTopLevelSubjectsinternally를 호출할 뿐이고, 그래서 같은 메서드를 호출하는 것으로 if 아래에 else 3행을 절약하게 해준다. 하지만 subjectsSelectoris는 개별적으로 메모이즈드되고, 어떤 재계산도 일으키지 않는다.

따라가다보면, 드래그가 있을 때마다 드랍 타겟이 명백히 변경되지 않았더라도 각 서브젝트가 stackAndGetTopLevelSubjects에서 재계산되지만, 이는 ES6 Map과 간단한 캐싱에 의해 아주 쉽게 향상시킬 수 있다. 사실 나는 이런 성능 최적화들이 실제에 어떤 영향을 미칠 수 있을 지는 의심스럽다; 그러나 비합리성과 비효율성이 나를 괴롭히고 있기 떄문에, 그리고 이러한 도구를 가지고 작업하는 것이 좋은 경험이 될 것이기 때문에, 그렇게 한다.

정규화의 이점

MobX로도 이렇게 할 수 있었냐고? 물론이다. 그리고 그렇게 어렵지도 않을 것이다. 아마도 주요 서브젝트들이 옵저버블 어레이—주요 서브젝트들과 현재 drop id를 읽는 계산이 거기서 이루어질 것이다. 만약 dropId가 없으면 주요 서브젝트 어레이가 리턴될 것이다. dropId가 있는 경우에는 대상 서브젝트가 복제되고 대체되야 할 것이다. 즉, 드래그된 서브젝트의 복사본이 대상의 하위 자식으로 추가될 것이다—MobX의 invariants를 위반하는 일 없이: observables는 계산된 속성 정의 안에서는 절대 수정될 수 없다. 그리고, 물론 대상 서브젝트는 얼마나 깊은 계층 구조를 가졌냐에 상관없이 발견되야 할 것이다. 그러므로, 그것을 탐색하기위한 종류의 computed lookup map이 주요 서브젝트 어레이에 정의되어야 한다. 아마도 가능하겠지만, Redux의 솔루션이 더 직관적이라고 생각한다.

아니면 내가 모르는 더 간단한 방법이 있을 수도 있다. 그렇더라도, 이 데모가 보여주듯이, Redux는 근본적으로 유연하고 단순한 패러다임으로 당신을 강제한다.

결론

은탄환은 없다. 두 접근법 모두 장단점이 있고 그것을 이해해야 한다. Redux의 normalized data를 저장하는 접근법은 고유한 유연성을 제공하지만, 많은 양의 보일러 플레이트가 필요하고, 비경험자에게는 코드가 직관적이지 않다.

유연성에 대한 수요가 크지 않은 경우에는 MobX를 사용하는 것이 좋다. 계단식 반응을 이끄는 MobX의 능력은, “스프레드시트”와 같은 사례에 일반적으로 적합하다.

이 글을 쓰는 주된 동기는 Redux에 대한 비판에 대해 싸우려는 것이다. 맞다. 더 많은 코드가 필요하지만, 고유의 이점도 제공한다.

원문보기

May 12, 2016

[8:23 PM] jrajav:: 헤이, <Provider> 컴포넌트가 실제로 어떤 기능인지 궁금해. 완벽히 이해하려는 건 아니고 - 왜 store.subscribe( () => render( <App state={store.getState()} />, document.getElementById('root') ) )처럼 그냥 전달하지 않지?

[8:24 PM] jrajav:: dispatch 함수도 전달하고

[8:24 PM] jrajav:: 다음엔 각 하위 컴포넌트에 상태를 적절하게 나누어서 분할하여 전달하는 거지

[8:25 PM] jrajav:: 만약 모든 컴포넌트가 순수한, stateless 함수형 컴포넌트라면 이 접근방법은 여전히 성능이 좋아야만 하겠지. 안그래?

[9:58 PM] acemarke: @jrajav : 관용적인 Redux 사용법은 약간의 의존성 주입이 일어나. 이상적으로는 컴포넌트와 액션 생성자가 직접 store를 참조하는 일은 없어 [9:58 PM] acemarke: 그게 코드를 더 재사용할 수 있게 하고 테스트 하기도 쉽지

[9:59 PM] acemarke: 이걸 봐바 http://redux.js.org/docs/FAQ.html#store-setup-multiple-stores

[10:00 PM] acemarke: 또한 네가 작성한 스니펫은 App 컴포넌트가 모든 필요한 상태를 아래로 전달하는 것 처럼 보여 [10:00 PM] acemarke: 만약 상태가 하나의 탑레벨 컴포넌트에 둘 수 있을 정도로 작으면 아마도 Redux가 필요할 것 같지도 않네 - 그냥 React의 컴포넌트 상태를 사용해

[10:01 PM] acemarke: 하지만, React Redux에서는 <Provider> 안쪽의 어떤 컴포넌트라도 필요한 state의 조각을 구독할 수 있어. 하위 컴포넌트들이 필요한 데이터를 알고있는 탑레벨 컴포넌트가 없더라도 말이지

[10:25 PM] jrajav:: 맞아 @acemarke, 근데 한편으론 어떤 컴포넌트 트리에서 어떤 데이터를 필요로하는지 알 수 없잖아. 깊이 중첩된 컴포넌트에서 마술처럼 상태조각을 가져가버리는 건 덜 기능적이고 덜 관리가능해 보여

[10:26 PM] jrajav:: 만약 중첩된 컨테이너를 사용해서 상태를 구독해버리면 말이야

[11:56 PM] acemarke: 어.. “마술”이란건 없는데

[11:57 PM] acemarke: connect()를 사용해서 정의된 어떤 컨테이너 클래스라도 필요한 상태 조각을 명확하게 지정할 수 있어

[11:57 PM] acemarke: 여기 전형적인 예제가 있어

[11:57 PM] acemarke: 상태 안에 여러명의 users가 있고

[11:58 PM] acemarke: 탑 레벨의 프레젠테이셔널 컴포넌트를 가지고 있고, LeftSidebar와 RightMainPanel이라는 두개의 프레젠테이셔널 컴포넌트를 렌더링한다고 하자

[11:58 PM] Francois Ward: 내 생각엔 그가 말하는건 폴더 구조를 봤을 때, grep하지도 않고 아무데서나 물건들이 나타나는걸 말하는 것 같아

[11:58 PM] acemarke: LeftSidebar에서는 connected 컴포넌트인 UsersList를 렌더링한다고 하자

[11:58 PM] acemarke: UserItems의 목록을 표시하는 List지

[11:58 PM] acemarke: 아마도 이름이나 몇몇 세부사항을 표시하겠지

[11:58 PM] Francois Ward: 그 모델의 꽤 일반적인 비판이지. Cyclejs/Elm에서는 “모든 것은 맨 위쪽에서부터 아래쪽으로 이어져서 내려온다” 라고 하지. 그건 실용주의 vs 쉽게따라가기/순수성/테스트성의 타협이야.

[11:59 PM] acemarke: 그러고나서, 만약 UserListItem을 선택하기 위해 클릭하면, RightMainPanel에 UserDetails 컴포넌트가 보여지길 바랄거야

[11:59 PM] acemarke: 역시 connected 컴포넌트지 user에 대한 세부 사항을 보는

May 13, 2016

[12:00 AM] acemarke: 하지만 TopLevelComponentㄱ LeftSidebar와 RightMainPanel을 렌더링하지만 그 세부사항에 대해 알 필요는 없어

[12:00 AM] acemarke: 만약 모든 데이터에 대한 책임을 탑레벨 컴포넌트에게 지우고 싶다면 그럴 수 있어

[12:01 AM] acemarke: 하지만 이 시점에서 일반적으로 인정되는 모범사례는 컴포넌트 트리의 하위 부분에 여러 개의 연결 포인트가 있는 것이야 데이터가 실제 필요한 부분에 가깝게 말이지

[12:02 AM] acemarke: 원래 질문에 답하자면 Provider를 사용하는 이유는 하나의 탑레벨 컴포넌트에만 연결 포인트를 두고 그 아래는 pure한 stateless 컴포넌트로 두는 것에 반대하는 것이지

[12:03 AM] acemarke: 오 그리고 @jrajav : 네 질문에 대한 특정한 대답인데, 그게 더 성능이 좋지는 않아

[12:03 AM] acemarke: 하나의 탑 컴포넌트를 연결하는 것은 모든 스토어 변경사항에 대해서 재 렌더링을 할거야

[12:03 AM] acemarke: 그리고 그 하위의 모든 stateless 컴포넌트도 재 렌더링할거야

[12:04 AM] acemarke: 하위 컴포넌트를 여러개 연결하는 것은 서브렌더 트리를 더 작게 만들지

[12:04 AM] acemarke: 그리고 React Redux는 얼마나 많은 연결된 컴포넌트가 실제로 재 렌더링해야하는지에 대한 최적화로 많은 작업들을 해

[12:19 AM] jrajav:: 맞아 @Francois Ward 그게 내가 뜻한 바야

[12:20 AM] jrajav:: connect() 자체의 “마술” 부분이지, 임의의 장소에서 상태를 가져와서 노출시키는 것 말야

[12:21 AM] jrajav:: 그리고 @acemarke 설명해줘서 고마워 - 재 렌더링이 뜻하는 바를 정확히 물어봐도 될까?

[12:22 AM] jrajav:: 네가 말하는 바가 React가 실제로 light DOM에 모든 싱글 컴포넌트를 재 렌더링한다는거야?

[12:22 AM] jrajav:: 아니면 그냥 shouldComponentUpdate에 의해 “숏-서킷”되지 않고 render()함수가 재실행 된다는 거야? [12:22 AM] acemarke: …반대로?

[12:23 AM] acemarke: 만약 네가 mapStateToProps 함수가 state.some.very.nested.field를 파야한다고 걱정되면 “selector”를 이용해서 상태 트리의 특정 필요 부분을 가져오는 식으로 캡슐화하면 돼

[12:24 AM] jrajav:: 아냐, 나는 React + redux + immutable.js가 더 걱정돼 그리고 상태 오브젝트 전체를 <App /> 컴포넌트에 전달하는 패턴이 하나의 상태 필드만 고쳤을 때도 전체 <App />을 DOM에 재 렌더링하는 결과를 내는 것이 더 걱정돼

[12:24 AM] jrajav:: 혹은 React의 virtual DOM diff를 대부분의 파트에서 실행한다거나 하는 것 말야

[12:25 AM] jrajav:: diffing이 어떤식으로 일어나는지는 정확히는 모르지만 말야

[12:27 AM] jrajav:: 예를들어 <Page1 /><Page2 />가 있다고 하고 전체 상태 오브젝트의 서브 프로퍼티인 pageOne을 1로 pageTwo를 2로 전달하려고 해. pageTwo의 중첩된 몇몇 서브 프로퍼티가 변경되서 <Page2 />가 바뀌고, 재 렌더링된다고 하자. pageOne의 어떤 것도 바뀌지 않았기 때문에 <Page1 />은 같은 파라미터와 함께 호출되서 같은 virtual DOM 트리로 결과가 나타나지

[12:28 AM] jrajav:: 나는 모든 React가 최적화되지 않았기 때문에 피할수 있는 모든 render()가 호출된다고는 생각하지 않아 (맞지?) stateless functional 컴포넌트들 말야

[12:28 AM] jrajav:: 하지만 최소한, virtual DOM diffing의 결과에 따라 <Page1 />의 light DOM은 아무것도 갱신되지 않겠지.. 맞지?

[12:29 AM] acemarke: render 함수는 재실행되고 그에 맞는 virtual DOM도 re-diffed 될거야

[12:29 AM] acemarke: 주어진 컴포넌트의 인풋이 아무것도 변경되지 않더라도, 즉 헛된 노력이지

[12:30 AM] jrajav:: 맞아 - 그래도 DOM 업데이트에 비해서 값싼 것이지

[12:30 AM] jrajav:: memoizing 래퍼에 의해 잠재적으로 해결될 수 있고

[12:30 AM] acemarke: 아 잠깐, 네가 쓴걸 좀 읽어볼게

[12:30 AM] acemarke: (wifi를 실행시켜서 약간 연결이 끊기네)

[12:30 AM] jrajav:: 괜찮음

[12:31 AM] jrajav:: 그리고 더 명확히 하자면 light DOM은 “actual DOM”을 이야기한거야

[12:31 AM] jrajav:: (주로 Polymer로 작업할때 shadow DOM 때문에 light DOM이라고 말하곤 하지)

[12:32 AM] acemarke: 이론적으론 맞아

[12:32 AM] acemarke: 하지만 추가적으로 알려줄게 있어

[12:32 AM] acemarke: connect()가 여러가지 편리하게 해주는 것들이 있는데

[12:32 AM] acemarke: 다른 것들 중에서도, 그건 shallow equality check를 하지 mapStateToProps가 리턴한 오브젝트에 대해서 이전에 뭘 리턴했는지랑 말야

[12:33 AM] acemarke: 그래서 컴포넌트가 MSTP로부터 같은 내용을 연달아 리턴하게 된다면 재 렌더링을 넘어갈 수 있어 포장된 컴포넌트의 재 렌더링 말야

[12:34 AM] acemarke: 위쪽에 말야

[12:34 AM] acemarke: 네 Page1/Page2 예제에서

[12:34 AM] jrajav:: 아 그래.

[12:35 AM] acemarke: connect()를 사용하지 않았다고 가정하면, 그리고 직접 shouldComponentUpdate를 구현하지 않았다고 하면, Page2에 관한 상태 트리의 아주 작은 부분이라도 변한다면 Page1 Page2 양쪽과 그 모든 하위 컴포넌트에 대해 적어도 render / diff 사이클을 유발할거야

[12:36 AM] jrajav:: shouldComponentUpdate의 자체 구현에 의한게 네가 말한 memoizing 래퍼 컴포넌트 맞지?

[12:36 AM] jrajav:: 만약 서브컴포넌트를 memoize했다면 VirtualDOM에 대한 diffing을 피할 수 있지?

[12:39 AM] acemarke: data->props 관리하는 것을 추가적으로 해주기 때문에 React Redux의 connect()는 사용하는 것만으로 좋은 성능 향상이 일어나지

[12:39 AM] acemarke: 아.. 아니 보통 SCU는 prop 동등성 체크를 하지

12:41 AM] jrajav:: 음 shallow comparison은 보통 memoizing 래퍼들이 할 건데

[12:41 AM] jrajav:: 난 shouldComponentUpdate가 render()가 같은 컴포넌트를 리턴하는 것과 같은 방식으로 동작하는 건지 알지 못하겠네

[12:41 AM] acemarke: 일반적으로 SCU는 this.props와 nextProps에 대해 shallow 동등성 체크를 해

[12:41 AM] acemarke: 이상적으로는

[12:41 AM] acemarke: 메모이제이션은 안하지

[12:42 AM] acemarke: SCU는 컴포넌트를 리턴하지 않아

[12:42 AM] acemarke: boolean을 리턴하지

[12:42 AM] jrajav:: 그럼 stateless functional 컴포넌트는 중복을 막지 못한다는거야?

[12:42 AM] acemarke: 그건 말 그대로 “이봐 컴포넌트, 여기 네 props가 이렇게 들어왔어. 재 렌더링하길 원해? 예/아니오”

[12:43 AM] acemarke:

shouldComponentUpdate(nextProps) {
    // single-field example.  extrapolate for more props, or do all of them
    return this.props.someField !== nextProps.someField;
}

[12:43 AM] acemarke: 아니

[12:44 AM] acemarke: 네가 React 라이프사이클 메서드를 원한다면 React.createClass()나 MyClass extends React.Component를 사용해야해

[12:44 AM] acemarke: 지금으로써는 functional stateless components는 간편하고 명확할 뿐이지

[12:45 AM] acemarke: 어떤 성능 최적화도 하지 않아(문서에서는 곧 할거라고는 하지만), 효과적으로 재 렌더링이 일어나지 않게할 방법이 없으므로 성능 손실이 발생하지

[12:46 AM] acemarke: 즉, connect()에 어떤 타입이라도 전달한다면 이득을 볼수 있다는 거지

[12:47 AM] acemarke: 왜냐면 그건 네 “진짜” 컴포넌트를 랩핑하는 것이니까

[12:47 AM] jrajav:: 이론적으로는 상태를 추출하지 않아도 connect()를 사용할 수 있다는거네?

[12:48 AM] jrajav:: 그리고 그건 SCU의 shallow 프로퍼티 최적화(Immutable로 충분한)를 해줄 거라는 거고

[12:48 AM] jrajav:: 혹은 내 고차 컴포넌트 함수를 비슷하게 써도 되고

[12:49 AM] jrajav:: 하지만 stateless function에 대해서는 최적화 방법이 없다는 거지

[12:50 AM] acemarke: 어떤 도움이 된건가?

[12:50 AM] jrajav:: 아직 명확하지 않은 것 한 가지가 있어. 모든 것을 virtual DOM으로 재 렌더링하는 비용을 제거하고 싶어

[12:50 AM] jrajav:: virtual DOM diffing은 실제 DOM을 업데이트하는 것을 대부분 막아주지 맞지?

[12:51 AM] jrajav:: 그래서 <Page1 />의 어떤 실제 DOM 부분도 절대 변하지 않는다는 거지? 최적화되지 않은 stateless 컴포넌트라도 예를 들자면말야

[1:00 AM] acemarke: 기본적으로, 맞아

[1:02 AM] acemarke: 맞아 확실해

[1:02 AM] acemarke: 아무 필요없는 비교를 하는 데 꽤 많은 CPU 싸이클을 낭비하겠지

[1:03 AM] acemarke: 컴포넌트가 몇개 없거나 상대적으로 Redux Store의 크기가 작다면 신경쓸 필요 없어

[1:04 AM] acemarke: 이전 시대의 격언인 “일단 동작하게 하고, 그다음 올바르게 만들고, 그다음 빠르게 하라”는 말로 충분하지

[1:06 AM] acemarke: 반대로 꽤 많은 컴포넌트가 필요하다는 것을 알고 있고, 액션도 많다면(특히 controlled input을 store를 업데이트하는 데 사용한다면), 정말 이런 낭비를 막을 필요가 있을거야

[1:07 AM] acemarke: 잘 시간이 지났네. 도움이 됐길 바라 :)

[1:07 AM] jrajav:: 그래, React가 stateless 컴포넌트를 최적화 할때까진 connect()나 memoizing 레이어를 이용해야 한다는 거네

[1:07 AM] jrajav:: 맞아 엄청 도움됐어 @acemarke

[1:07 AM] jrajav:: 시간을 많이 써줘서 고마워

[1:08 AM] jrajav:: (그런데 실제 memoizing이 아니라, React-short-circuiting이나 뭔가 있지 않나)

[9:30 AM] jrajav:: @acemarke https://github.com/acdlite/recompose 의 pure() 함수가 react-redux의 connect()같은 일을 한다는것을 알아냈어

[9:30 AM] jrajav:: 그럼 stateless 컴포넌트에 대해서 connect()를 선호하는 이유가 더 있을까?

[9:31 AM] jrajav:: 만약 통과하는 props에만 의존한다면?

[9:31 AM] jrajav:: 다른 성능 최적화를 제공한다거나 하나?

9:49 AM] acemarke: 맞아, 조금 더 해. 네가 말하는 접근법을 감안하면 아마도 recompose도 사용할 것 같군. 더 명확한 의도로

[9:52 AM] acemarke: (분명히, 난 아직 네가 connect를 여러 곳에서 사용하는 걸 반대하는 것 같아)

[10:06 AM] jrajav:: 난 그냥 더 functional한 방법을 선호할 뿐이야. 데이터 의존성이 명확니까, 합성이나 테스팅도 엄청 쉽지.

[10:07 AM] jrajav:: 내가 더 봐야할게 뭐지? 네가 말한 방식이 recompose가 제공하는 것들에도 있나?

[7:58 PM] acemarke: @jrajav : connect()를 사용하면 내장된 두 가지 최적화를 제공하고 더 많은 최적화를 할 수있는 공간을 제공해. 최근의 스토어 변경이 컴포넌트에 영향을 미치는지 알기 위해서 mapStateToProps가 리턴하는 오브젝트에 대해서 shallow-comparison을 하지. (부모로부터 직접 전달되는 prop은 아니고); 그리고 잘 알려지지 않은 최적화인데 map/dispatch/merged props 중 어떤 것도 변경되지 않은 경우에 React.createElement가 정확히 같은 인스턴스를 반환하는 거야; 그리고 이 모든 것 위에 너만의 shouldComponentUpdate를 추가로 구현할 수도 있어.

Dan은 connect()의 소스코드 대부분을 휼륭한 비디오 코스를 통해서 그게 어떻게 구현되었는지 알려줬지. 지금은 코드가 조금 진화했겠지만, 여전히 좋은 정보야. 여기 그 비디오 링크야 https://youtu.be/VJ38wSFbM3A.

[9:11 PM] jrajav:: @acemarke 그래서 만약 우리가 주입된 상태나 dispatch를 사용하지 않고 그저 immutable.js props로 모든 것을 명시적으로 전달한다면, connect()는 recompose의 pure()가 하는 일 외에 아무 것도 추가하지 않는다는 거지?

[9:11 PM] jrajav:: 맞아?

[9:11 PM] jrajav:: 왜냐면 immutable.js props만을 가지고서 shouldComponentUpdate를 shallow-compare하기면 이 모든 것(pure()가 하는 것)을 덮을 수 있으니까

[9:13 PM] acemarke: 거의, 맞아

[9:38 PM] acemarke: 흠. 이봐, @jrajav. 이 모든 것 말고 고려해야 할 하나가 뭔지 알아? 컴포넌트 트리를 어떻게 두었냐에 따라 이 shouldComponentUpdate의 모든 이득을 취하지 못할 것이라고 생각해.

[9:38 PM] acemarke: 만약 수동으로 위에서 아래로 모든 props를 전달하려고 하면, 트리 윗부분의 몇 개의 컴포넌트가 항상 다시 렌더링될거라는 것을 의미하지

[9:38 PM] acemarke: 아래쪽 근처에 있는 것들은 그렇지 않겠지만

[9:39 PM] acemarke: 하지만 많은 props를 하위 컴포넌트들에게 위임하는 것들은 마찬가지야

[9:39 PM] acemarke: 항상 깊게 중첩된 하위 컴포넌트가 어떤 props를 필요로 하는지 추적해야 하고 A > B > C > D > E로 전달될 수 있도록 생각해야만 해

May 14, 2016

[1:24 AM] jrajav:: 루트로부터 리프까지 경로가 모두 재 렌더링해야할 필요가 있다는 것은 이치에 맞네. 그게 정확이 무슨 일이 일어나고 있는지 반영하는게 아닌가? 어떤 방식의 조직화가 그런걸 피할 수 있지?

[1:27 AM] jrajav:: 그리고 맞아 @acemarke 나는 그 props들을 추적하는 것이 얼마나 번거로운지 알아. 개인적으로는 다음과 같이 생각하고 있어: D가 E를 포함하고 E는 렌더가 필요한 부분이라고 하자. 그러면 D가 E가 필요로 하는 프로퍼티를 필요로하는게 이치에 맞아. 왜냐면 그 프로퍼티는 실제로 D가 렌더링 될 때 필요한 프로퍼티기 때문이지. 그리고 다른 것들도 마찬가지겠지… 나는 오히려 컴포넌트의 함수 시그니처에 렌더를 목적으로 필요한 파라미터를 정확히 요구해야하는 문제라고 봐. 컴포넌트가 마술처럼 스토어의 임의 장소에서 상태를 가져오는 것이 아니라 말이야. 내게는 전역 변수와 싱글톤 클래스로 state를 갖고 올 수 있는게 편할 수 있지만, 코드는 어디서 그것들이 오는지 추론하기 힘들어지고, 테스트하기 어렵고, 검증하기 어렵고, 오류가 발생하기 쉬울 거야.

[1:28 AM] jrajav:: 그리고 react/redux에서는 함수형 패러다임을 사용하는 것처럼 보이는데도 그런 컨테이너/ 컴포넌트를 사용하는 방법이 “모범 사례”라는 것이 나에게 매우 혼란스러워.

[11:37 AM] acemarke: @jrajav : 내가 봤을때는, “container” 패턴이 몇 가지 장점을 갖고 있어. 우선, 개념적 유형에 따른 분리를 허용하지. 그런 방식으로 바라본다면 말야 - 컴포넌트가 레이아웃과 조직화에 중점을 두는 것들과 컴포넌트가 데이터를 가져오는 데 책임을 갖는 것들 (Redux store루부터든지 서버로부터든지 등등).

[11:41 AM] acemarke: 둘째, 성능 향상을 얻을 수 있어. 전체를 탑-다운 하는 방법에선 몇몇 컴포넌트는 항상 강제로 재렌더링될 수밖에 없어. 대부분의 layout 중점의 presentional 컴포넌트는 아마도 전혀 재 렌더링되지 않겠지, 그리고 connect()를 사용하는 것은 효율적으로 트리 형태의 새로운 업데이트를 시작할 수 있을 거야. 특히 컨테이너 컴포넌트가 부모로부터 직접 전달받는 props가 없다면 말이야. 그래, 스토어가 업데이트되면 업데이트는 mapStateToProps가 신경쓰는 어떤 값 이외의 변경에 대해서는 변경되지 않을거야 그럼 컴포넌트는 재 랜더링을 건너뛰게 되고 그 하위 서브트리도 마찬가지지. 간단히 승리한 거야.

[11:43 AM] acemarke: 셋째, connected 컴포넌트는 필요한 데이터를 바로 그곳에서 가져오기 때문에 실제 데이터의 흐름을 보다 쉽게 추적할 수 있게 해. 더 적은 수의 레이어가 필요하기 때문이야. 내 UserListItem에 뭔가 문제가 생겼음을 알았을 때 TopLevelComponent > MainLayout > LeftSidebar > UserList > UserListItem을 통과해가면서 추적하는 것 보다 그냥 상위의 UserList를 보면 되기를 바라지

[11:44 AM] acemarke: 넷째, connected 컴포넌트는 애플리케이션의 컨텍스트 내에서 더 많은 재사용성을 갖지 - LeftSidebar를 BottomPanel로 옮기기로 결정했다면 그냥 props가 최상위 컴포넌트에서 내려오는 흐름을 신경쓰지 않고도 그냥 옮기면 되는거야

[11:46 AM] acemarke: 다섯째, connected 컴포넌트를 테스팅하는 것에 대한 충고는 connected 버전의 컴포넌트를 default로 export하고 “plain” 컴포넌트를 named export한 다음 unconnected 버전의 테스트를 테스트하는 거야. 네가 신경써야 할 것은 props와 lifecycle에 어떻게 반응하는지 뿐이지. 어떻게 props를 얻는지는 문제가 되지 않아. 따라서, 넌 그저 몇몇 props를 테스트 코드에 넣고 거기서 행동을 검증하면 되고, 아마도 mapStateToProps 함수도 원한다면 테스트할 수 있지만, connect()가 올바르게 mapStateToProps를 호출하기만을 가정하면 되는거야. 필요할 때는 뭔가 바뀌었을 때 그 props를 컴포넌트에 전달하면 돼.

[11:50 AM] acemarke: 여섯째: 맞아, Redux가 “전역 변수” 라는 측면이 있지만, 반대로, 관용적인 Redux 코드는 절대로 실제 직접 레퍼런스하지 않아. 모든 것은 의존성 주입으로 얻어내지. connect()는 스토어로부터 props를 전달하고 dispatch()에 대한 참조를 제공하고 미들웨어는 dispatch() 및 getState()를 주입하고, 썽크와 같은 실제 미들웨어들은 액션 생성자에도 이것들을 주입하지. 실제 앱 코드의 어디에서도 직접 스토어를 레퍼런스하지 않아. 스토어를 생성하는 두 줄을 제외하고는. 그리고 바로 <Provider>에 전달하지. 그게 redux-mock-store가 테스팅을 실제로 가능하게 하는 방법이야

[11:50 AM] acemarke: 마지막으로, 조금 헷갈리네: “containers” vs “functional”에 대한 네 걱정은 뭐지?

[11:52 AM] acemarke: 궁극적으로 보면 좋은 아키텍쳐의 구성하는 것이 무엇인지에 대한 조금 다른 철학을 가지고 있는 것처럼 들리네. 난 그저 각자의 관점에서 다른 점이 무엇인지 이해하려고 시도하고 있어.

[2:03 PM] Francois Ward: ““다섯째, connected 컴포넌트를 테스팅하는 것에 대한 충고는 connected 버전의 컴포넌트를 default로 export하고 “plain” 컴포넌트를 named export한 다음 unconnected 버전의 테스트를 테스트하는 거야. 네가 신경써야 할 것은 props와 lifecycle에 어떻게 반응하는지 뿐이지.”

[2:04 PM] Francois Ward: 조심스럽게 저것에 대해 말해보면, 그건 간단하지 않아.. 만약 dumb 컴포넌트가 connected 컴포넌트를 가지고 있다면 어떻게든 반드시 스토어를 제공해야해. 아니면 connected 컴포넌트를 목킹하든지. Jest의 문제가 아니고, shallow render를 쓸때도 그래.

[2:04 PM] Francois Ward: 하지만 항상 옵션인 것은 아니지.

[2:04 PM] acemarke: 맞아 아마도 문서에 경고해야할 몇가지 점이 있지

[2:04 PM] Francois Ward: connected 컴포넌트를 children으로 가지는 것은 뒤쪽에 고통스러움이 있지. 그건 타협이야. 하지만 “어디서든 연결하세요, 무료입니다!”라고 할 건 아니지

[2:05 PM] Francois Ward: 강력한 이점이지만. 또한 높은 비용을 수반하지.

May 15, 2016

[11:39 AM] jrajav:: @acemarke 매우 자세한 답변 고마워! 그리고 맞아. 네가 언급했듯이 모범 사례에 대한 의견차이일 뿐이라고 생각해. 너는 컴포넌트가 어떻게 자신의 프로퍼티를 글로벌 상태(혹은 그것에 가까운 부모)에서 가져오는지 아는 것이 컴포넌트를 재사용하기 쉬게 해주는 이유라고 하고, 나는 컴포넌트 레벨에서는 그것을 인정해. 하지만 임의의 지점에서 글로벌 상태를 다루는 것은 여러 컴포넌트의 조합을 만들고 앱 자체에 대해 추론하기 어렵게 만들고 더 조합하기 어렵게 만들어서(함께 퍼즐을 맞추는 것처럼) 더 에러를 유발한다고 봐. 데이터 의존성에 대해 명시적으로 선언하는 추가 작업(이 경우에는 파라미터를 수동으로 아래로 전달하는 작업)은 그런 것들을 피할 수 있는 좋은 작업이야. 애플리케이션을 더 단순하고 관리가능하게 만들어 주는 거지. 적어도 그게 내가 보는 방법이야.

[11:41 AM] jrajav:: @acemarke 또한, React가 항상 presentaion 계층으로, 스토어에 대해서는 전혀 아는 바 없고, 모든 컴포넌트를 “dumb”, pure하고 실제 라이프사이클을 갖지않게 강제하지. - 라이프사이클은 그저 그들의 input 중에 함수일 뿐이야 - 그게 개발, 테스트, 재사용을 훨씬 쉽게 만들지

[11:42 AM] jrajav:: 그리고 이게 무슨 뜻인지 물어봐도 될까? (edited)

[11:42 AM] jrajav:: > 둘째, 성능 향상을 얻을 수 있어. 전체를 탑-다운 하는 방법에선 몇몇 컴포넌트는 항상 강제로 재렌더링될 수밖에 없어. 대부분의 layout 중점의 presentional 컴포넌트는 아마도 전혀 재 렌더링되지 않겠지, 그리고 connect()를 사용하는 것은 효율적으로 트리 형태의 새로운 업데이트를 시작할 수 있을 거야. 특히 컨테이너 컴포넌트가 부모로부터 직접 전달받는 props가 없다면 말이야. 그래, 스토어가 업데이트되면 업데이트는 mapStateToProps가 신경쓰는 어떤 값 이외의 변경에 대해서는 변경되지 않을거야 그럼 컴포넌트는 재 랜더링을 건너뛰게 되고 그 하위 서브트리도 마찬가지지. 간단히 승리한 거야.

[11:44 AM] jrajav:: 내가 이해한 바로는, 전체 서브트리가 쓰는 props를 조합한 shouldComponentUpdate를 가지고 있으면 모든 파라미터가 동일할 때(아마도 deep 오브젝트를 핸들링하기 위한 Immutable.js같은 것으로), 서브트리가 렌더되지 않을 것이기 때문에 다른 방법론과 어떻게 다른지 이해할 수 없어

[11:48 AM] jrajav:: 그리고 네 질문에 직접 담변하자면, “containers” vs “functional”에 대한 내 걱정은 내 원칙을 따르면 React는 항상 presentation layer여야 해. 하나의 큰 함수지 상태를 적절한 UI로 내놓는 - <App /> 그 자체를 포함하는 거야

[11:50 AM] jrajav:: 그리고 모든 반대의견에 감사해! @acemarke 난 분명히 선입견을 가지고 있지만 React에 대해선 입문자고 조심스러운 뉘앙스를 가져야한다는 것을 고려하지 못했어. 이것은 내게 매우 가치있어.

[11:52 AM] acemarke: @jrajav : 물론, 합리적이고 기술적인 의견과 토론은 항상 가치있지 :)

[11:53 AM] acemarke: 어디보자. 네 코멘트를 하나하나 보면:

[11:54 AM] acemarke: 내 경우에는 일반적으로 내가 읽었던 수많은 토론과 내 앱이 필요로하는 특별한 니즈에 영향을 받았어

[11:55 AM] acemarke: 예를 들어 Redux 초창기에 Dan Abramov의 충고 같은거지 connected 탑 컴포넌트, 혹은 아마도 최상위 몇몇 컴포넌트의 connected에 대한 것

[11:57 AM] acemarke: 하지만 시간이 지나서 그 조언은 확실히 바뀌었어, 이제는 이치에 맞으면 깊은 곳 어디든지 연결하라는 것이지. 사실, 그의 MobX와 Redux 벤치마트를 위한 최근의 최적화 과정에서 그는 “10K 항목이 있는 List” 시나리오에서는 부모 리스트가 10K 아이템에 대한 ID를 가져오고, ID를 자식에게 전달하고, ListItems 자체도 항상 연결하여 자신들의 업데이트에 대해 책임을 지게 하는 것이였어. 그렇게 하면 하나의 목록 항목을 업데이트해도 ID 셋트가 수정되지 않으므로 상위 목록을 다시 렌더링할 필요가 없게되는 것 등이지.

[12:00 PM] acemarke: 또다른 생각은 “React는 그저 view 계층일 뿐이다” 라는 것은 사실이지만, 여기서 문제는 우리가 data 계층과 view 계층을 연결하는 방법을 알아내려 한다는 것이야. 일반적인 connected 컴포넌트는 각자가 mapStateToProps를 정의하고 있고 plain 컴포넌트는 같은 파일의 바로 옆에 위치하지, 그리고 디폴트로connect(mapStateToProps)(MyComponent)를 익스포트하지. 그래서 connected 컴포넌트에 필요한 데이터가 무엇인지 쉽게 파악할 수 있게 되는 거야

[12:01 PM] acemarke: 또한 여러개의 connected 컴포넌트가 있는 경우에도 전체 UI는 여전히 “상태에 대한 함수”라고 생각해

[12:01 PM] acemarke: 어디보자. 트리 성능…

[12:01 PM] jrajav:: 이 경우의 “재 렌더링”은 리스트 요소가 모든 리스트아이템에 대해 반복할 필요가 있다는 것을 의미한다. SCU에 그 대부분을 전달하여 알아내려고, 맞지?

[12:02 PM] acemarke: 맞아

[12:02 PM] acemarke: 부모 렌더 -> 자식들의 SCU 확인 -> 자식들은 재 렌더되거나 안되거나한다

[12:02 PM] acemarke: 하지만 요점은 SCU가 문자 그대로 동일하더라도 불린다는 거다.

[12:03 PM] jrajav:: 맞아

[12:03 PM] jrajav:: 하지만 SCU가 그저 Immutable.js의 동등성 체크라면, 그저 single === 체크일 뿐이지 않나?

[12:03 PM] jrajav:: 함수의 호출 요소가 있지만 그건 그리 비싸지는 않다.

[12:03 PM] acemarke: 어쨌든 props의 필드당 한 번이다.

[12:04 PM] jrajav:: 맞아

[12:04 PM] jrajav:: 하지만 알겠어 렌더링 할 때 서브트리를 건너뛸 수 있는지 말야

[12:05 PM] acemarke: 네가 물어본 서브트리에 대한 내 관점을 정확히 하려는데 뭘 좀 알려줘

[12:06 PM] acemarke: 내가 이해하기로는 “하나의 탑레벨 컴포넌트가 모두를 지배한다” 시나리오에서, 기본적으로 const mapStateToProps = (state) => state;를 하기만 하면 되는데

[12:06 PM] jrajav:: 네가 말하려는 걸 전부 확신할 수 없어

[12:06 PM] jrajav:: 조금 더 말하자면 mapStatetoProps가 아무데도 없는거야

[12:06 PM] acemarke: 음, 스토어에서 뭔가를 가져가서 사용하는 누군가 말이지…

[12:07 PM] acemarke: 혹은 동등한 뭔가…

[12:07 PM] jrajav:: <App dispatch={store.dispatch} state={store.getState()} />(edited)

[12:07 PM] acemarke: 맞아 그게 실제 connect() 호출없이 connect() 하는 방법이지

[12:08 PM] acemarke: 대충 네가 하려는걸 추측할수 있어:

[12:08 PM] jrajav:: 차이점은 이 하나의 루트 엘레멘트만 상태에 접근할 수 있다는 거야

[12:08 PM] acemarke: 그래 connect(state => state)(App) 이랑 같은거지

[12:09 PM] jrajav:: 물론, 하지만 어느쪽이든 간단하다면 명백하고 명시적인 방법이 더 바람직하지

[12:09 PM] jrajav:: 한편으로 넌 state={store.getState()}를 한번만 수행하는 데다가 추상화 함수를 사용하는거야

[12:09 PM] acemarke: 맞아. 아무튼, 요점은, 탑레벨 컴포넌트가 항상 모든 상태를 가져오는 거야

[12:09 PM] jrajav:: 정확해

[12:09 PM] acemarke: 좋아. 내가 좋아하는 어떤 가설적인 앱 구조를 한번 보자

[12:10 PM] jrajav:: Todo? ??

[12:10 PM] acemarke: LeftSidebar랑 RightMainPanel같은 거(edited)

[12:10 PM] jrajav:: 오

[12:10 PM] acemarke: 아니. TODO는 더이상은 네이버….

[12:11 PM] acemarke: 그리고 왼쪽 사이드바가 CurrentUserInfo 컴포넌트랑, ListOfThings 컴포넌트를 보여준다고 하자

[12:11 PM] acemarke: 네가 목록에서 ThingListItem을 선택하면, MainPanel은 그 Thing의 세부정보를 보여줄거야

[12:11 PM] acemarke: 네가 수정할 수 있도록

[12:12 PM] acemarke: 우리 상태는 이렇게 보이겠지 : { currentUser: {}, things : { byId : {}, order : [] }, thingBeingEdited : {} }

[12:13 PM] acemarke: 지금까진 괜찮지?

[12:13 PM] jrajav:: 그래 난 잘 따라가고 있어

[12:14 PM] acemarke: 오 아마도 currentSelectedThingId 필드가 있을 수 있지

[12:14 PM] acemarke: 아마도

[12:14 PM] acemarke: 어떤 순서로 동작하는지 조금만 더 생각해볼게

[12:14 PM] jrajav:: 문제없어

[12:15 PM] acemarke: 사용자는 이미 로그인했다고 하자, 그리고 데이터도 가져왔고, currentUser와 things는 적절히히 채워져있어.

[12:16 PM] acemarke: 그래서 ThingList는 ThingListItems를 모두 표시하지

[12:17 PM] acemarke: 그래서 ThingListItem #1을 선택하여 클릭하고, {type : SELECT_THING, payload : {id : 1} }를 dispatch 하는거야

[12:17 PM] acemarke: 리듀서는 thing.byId[1]의 값을 thingBeingEdited에 복사하고

[12:18 PM] acemarke: 그리고 RightMainPanel에 있는 폼들에게 이 값을 전달하길 원하지

[12:19 PM] acemarke: 그래서, TopLevelComponent는 <RightMainPanel thingBeingEdited={state.thingBeingEdited} />를 렌더링하고, 그건 다시 <ThingEditForm thingBeingEdited={this.props.thingBeingEdited} />를 렌더링하지

[12:19 PM] acemarke: ThingEditForm은 input 여러 개를 렌더하고

[12:20 PM] acemarke: controlled input이라고 가정할게

[12:20 PM] jrajav:: Controlled input이 그들의 값을 상태에 반영하는 걸 의미하는거야?

[12:20 PM] acemarke: 기술적으로 다른 방법이야 - 그들의 값이 상태에서 나오는 거야

[12:21 PM] acemarke: “uncontrolled” input은 사용자가 조작하게 두는거야, 값은 브라우저 자체에 의해서 정상적으로 저장되며, 특정 시점(양식 제출 같은)에 실제 input 엘레멘트에 값을 요청하는거지

[12:22 PM] acemarke: “controlled” input은 항상 모든 입력에 대해 “value” prop을 지정하는 거야. 항상 그 값을 사용하도록 강제하지

[12:22 PM] acemarke: 즉 onChange 핸들러를 지정하고, 이벤트에서 새 값을 보고, 어딘가에 상태를 넣고, 새로 승인된 값으로 다시 렌더링해야해

[12:24 PM] acemarke: 그래. 이제 "Thing #1"의 값을 내 커서가 있는 “Thing Name”에 넣어야 하지. 그리고 타이핑을 시작해

[12:24 PM] acemarke: “asdf”를 "Thing #1"의 끝에다 추가했어

[12:25 PM] acemarke: 각 키 입력은 onChange 핸들러를 트리거하지

[12:25 PM] acemarke: 시나리오 목적상 이건 컴포넌트 상태가 아니라 Redux state에 저장하고 있다고 가정하자

[12:26 PM] acemarke: 그래서 onChange 핸들러는 첫 타이핑에 {type : EDIT_THING_FIELD, payload : {name : “Thing #1a”} } 를 dispatch 하겠지

[12:27 PM] acemarke: thingBeingEdited 리듀서는 thingBeingEdited.name 필드를 새 값으로 업데이트 할테고

[12:27 PM] acemarke: 그래서 thingBeingEdited.name, thingBeingEdited, 그리고 state까지 모두 새로운 레퍼런스지

[12:27 PM] acemarke: 지금까지 시나리오는 명확하지?

[12:28 PM] acemarke: (다시, 내 머리 꼭대기에서 이걸 만들면서, 망치지 않기를 바라고 있어 :))

[12:28 PM] jrajav:: 맞아 지금까진 명확해

[12:28 PM] jrajav:: 그리고 네가 뭘 하려는건지 보기 시작했어 ??

[12:28 PM] acemarke: 그래서. TopLevelComponent는 새 state 레퍼런스를 갖게되지.

[12:28 PM] acemarke: 재 렌더링을 할거야:

[12:30 PM] acemarke:

render() {
    const {state} = this.props;
    return (
        <div>
           <LeftSidebar currentUser={state.currentUser} things={state.things} selectedThingId={state.selectedThingId} />
           <RightMainPanel thingBeingEdited={state.thingBeingEdited} />
        </div>
    )
}

[12:30 PM] acemarke: RightMainPanel, ThingEditForm, 그리고 “name” 인풋이 명백히 재 렌더링 되지

[12:31 PM] acemarke: LeftSidebar도 재 렌더링을 시도하겠지

[12:31 PM] acemarke: 이 시점에서 SCU가 나머지 컴포넌트에 얼마나 구현됬나하는 질문이 있지

[12:32 PM] acemarke: 이론상, LeftSidebar는 순수한 presentational 컴포넌트야

[12:32 PM] acemarke: CurrentUserDetails와 ThingList만을 신경쓰는거지

[12:32 PM] jrajav:: controlled input을 사용하면 양쪽다 순수 presentational 아니야?

[12:32 PM] acemarke: ?

[12:33 PM] jrajav:: RightMainPanel도 말이야

[12:34 PM] acemarke: 응 맞아. 시나리오에서 또하나의 버금가는 요점은, 내 생각엔, 상태 변화가 실제로 하나의 작은 input에만 “영향”을 주는 거지, RightMainPanel의 깊은 곳에 묻혀있는 인풋말야

[12:34 PM] acemarke: 하지만, 순수하게 탑-다운으로만 이 키입력을 처리하기 때문에 LeftSidebar도 재 렌더링을 시도할 거라는 거지

[12:34 PM] jrajav:: 맞아

[12:34 PM] acemarke: 이제, LeftSidebar의 SCU 구현을 했다고 하자

[12:35 PM] acemarke: 아직 작업은 끝나지 않았지

[12:35 PM] jrajav:: 음

[12:35 PM] jrajav:: 만약 모든 것이 실제로 pure dumb 컴포넌트라면

[12:35 PM] jrajav:: 그리고 우리가 Immutable.js를 모든 상태 트리에서 사용하고 있다면

[12:35 PM] jrajav:: 그리고 모든 dumb 컴포넌트를 recompose의 pure()로 감쌌다면

[12:35 PM] jrajav:: 아마도 꽤 잘 동작하겠지, 그렇지?

[12:35 PM] jrajav:: 어떤 추가 코드를 작성하지 않아도(pure() 호출을 제외하고), LeftMainSidebar는 SCU가 잘 동작할테고 재 렌더링되지 않을거야t re-render

[12:36 PM] acemarke: 모든 컴포넌트에 대해 그렇게 했다면, 대부분, 그렇지

[12:36 PM] acemarke: 자 시나리오를 조금 뒤집어보자

[12:36 PM] acemarke: TopLevelComponent, LeftSidebar, RightMainPanel이 100% presentational 컴포넌트고 연결되지 않았다면

[12:36 PM] acemarke: CurrentUserDetails, ThingsList, ThingEditForm는 연결되었고 말이야

[12:37 PM] acemarke: 각각은 간단한 mapStateToProps를 갖고 있겠지 탑레벨의 상태에서 적절한 부분을 가져오기 위해 말이야

[12:37 PM] acemarke: 다시 한번, Name 필드에 "Thing #1"에 커서를 두고 “a”를 타이핑해보자

[12:38 PM] acemarke: edit 액션이 디스패치되고 thingBeingEdited.name이 업데이트되고, store가 구독자들에게 알리겠지

[12:38 PM] acemarke: CurrentUserDetails의 mapStateToProps가 재실행되서 {currentUser : state.currentUser}를 리턴할거야

[12:39 PM] acemarke: connect()가 shallow-compare하겠지 이전 리턴값과 이번 리턴값을, 그리고 변하지 않았으니 재 렌더링을 건너뛸거야

[12:39 PM] acemarke: ThingsList도 마찬가지고

[12:39 PM] acemarke: ThingEditForm은 thingBeingEdited가 변경된걸 알았으니 재 렌더링 할거고

[12:39 PM] acemarke: 하지만 TopLevelComponent, LeftSidebar, RightMainPanel은 결코 아무 일도 하지 않을거야

[12:40 PM] acemarke: 알지도 못하고 신경도 안쓰니까, 상태의 나머지 부분에 대해서는 전혀.

[12:40 PM] jrajav:: 여기서 요점이 명확히 드러났네, 하지만 - 하나의 선형적인 비교 연산을 또다른 것과 트레이드하고 있는거 아닌가?

[12:40 PM] jrajav:: 나는 connect()’ed 집합이 더 작아질 수 있다고 주장하는 걸 알았어

[12:41 PM] jrajav:: 하지만 상태 트리가 잘 조직되어 있더라도, UI는 비슷하지 않을 수도 있어

[12:41 PM] acemarke: 맞아, 전채 의도와 행동은 크게 다르지 않을 수 있지

[12:42 PM] jrajav:: 이 경우에는 거의 동일하지만 - 양쪽 다 “실제” 재 렌더링에 대한 3번의 비교를 수행하는 함수가 있는 것 처럼 보이네

[12:42 PM] acemarke: 하지만 UI의 중첩 방식에 따라, 얼마나 데이터를 가졌냐에 따라, 중간 계층의 숫자를 확실히 줄일 수 있어

[12:43 PM] jrajav:: 하지만 중간 계층의 숫자를 많은 컨테이너 컴포넌트로 절충하고 있잖아

[12:43 PM] acemarke: 네가 말했듯이, SCU로 모든 레이아웃스러운 컴포넌트를 명시적으로 태그할지 여부에 대한 질문이야

[12:43 PM] acemarke: props 관리에 대한 거래지

[12:45 PM] acemarke: 개인적으로는, 타이핑 수를 줄이고 성능을 동일하게 유지하며 추적할 데이터 흐름이 적어지지

12:45 PM] jrajav:: 하지만 그건 명시적이지 않잖아 우리가 recompose의 pure()에 의존한다면 말이야

[12:46 PM] jrajav:: 그걸 기억해야 할 필요가 있어…

[12:47 PM] acemarke: 맞아 그게 내가 의미한 바야. 실제로 각 컴포넌트 정의에 포함시켜야 해

[12:47 PM] jrajav:: 맞아

[12:48 PM] acemarke: 자 이제 내 생각에 영향을 미치는 내 앱에 대한 일반적인 설명이야

[12:48 PM] acemarke: 몇 년 전에 GWT로 작성한 앱을 재작성한 거지

[12:50 PM] acemarke: 기본적으로 3D 지구를 사용한 지도 플래닝 툴이야, 사용자를 지구와 상호작용할 수 있게 하지. 왼쪽 창에는 프로젝트의 모든 항목이 있는 몇몇 버튼과 트리뷰가 있고, 아래쪽 창에는 트리의 각기 다른 데이터 유형에 대한 속성을 보여주는 탭이 있어. 데이터 항목들은 트리에 보이고, 폼을 통해 보여지고 수정될 수 있지, 그리고 이는 지구에 표시돼

[12:50 PM] acemarke: 시각적으로는 구글 어스 데스크탑의 레이아웃이랑 비슷해: http://elmcip.net/sites/default/files/platform_images/launch_google_earth.jpg

12:50 PM] acemarke: 지구를 놓고, 폼들과 탭드 섹션을 오른쪽 창 세 번째의 아래쪽 두는거지

[12:52 PM] acemarke: 나의 이 특정 앱에서는 꽤 무거운 중첩 구조를 갖고 있으면서도, 폼에 항목을 표시/편집 하고, 지구본을 렌더링하고, 그 선택이나 편집에 대한 모든 정보를 업데이트 해야해

[12:52 PM] acemarke: 내 최상위 몇몇 계층은 pure presentational and dumb 컴포넌트야

[12:52 PM] acemarke: TreeView도 최상위에 있고 마찬가지로 presentational이고 dumb지. 하지만 각 데이터 타입에 따라 “ConnectedFolder” 트리 아이템을 표시해

[12:53 PM] acemarke: 개별적으로 중첩된 트리 항목들은 “자신을 위한 데이터만 가져와서 자식들에게 ID만 전달한다” 는 패러다임을 따르지

[12:53 PM] acemarke: 그리고 트리의 하위 노드들은 expand된 경우에만 렌더하지

12:54 PM] acemarke: 꽤 심각한 중첩 구조지, 하지만 트리를 클릭해서 현재 선택한 항목을 변경한 경우 적절한 속성 폼과, 해당되는 개별 지구본 표시 컴포넌트만 실제로 업데이트돼

12:55 PM] acemarke: 컴포넌트 인스턴스 별로 메모이즈드 셀렉터를 사용하여 트리 항목에 대한 최적화를 할 필요는 아직 없지만, 성능이 이슈가 된다면 그렇게 해야겠지. 항상 나중에 할 수 있는 어떤 것이 있어

[12:56 PM] acemarke: 그래 이런 사고 과정과 구조가 내게 영향을 미친 거야

12:58 PM] acemarke: 또 MobX와 Redux 벤치마크에서 Dan Abramove의 최근 최적화도 말이지: https://twitter.com/dan_abramov/status/720219615041859584 , https://github.com/mweststrate/redux-todomvc/pulls?q=is%3Apr+is%3Aclosed

[12:59 PM] acemarke: 마지막으로 다양한 connected() 접근 방법으로 Redux를 최적화하는 훌륭한 프레젠테이션이 있지: http://somebody32.github.io/high-performance-redux/

[1:00 PM] acemarke: 자 이제 다 말했어: 개인적으로는 순수한 탑-다운 방법은 날 짜증나게 해. 하지만, 정신적으로 그게 너에게 더 잘 맞는다면, 그렇게 해

[1:11 PM] jrajav:: 난 이제 상황을 더 잘 이해하고 있는 것처럼 느껴지네. @acemarke 다시 한번 자세한 토론에 대해 감사해

[1:12 PM] acemarke: 확실히. 텍스트로 벽쌓기는 내 특기지! :)

[1:13 PM] jrajav:: 내 생각에 난 아직도 순수한 함수형 스타일로 앱을 작성해보고 싶은 것 같아. 아마도 성능에 대한 우려가 있을 수 있지만 이 애플리케이션은 성능에 그렇게 연관되지는 않아. 하지만 아마도 아주 큰 상태 트리를 갖게되면 트리의 각 레벨에서 하위 속성을 빼낼 수 있도록 효율적으로 조직하는 방법을 알고 싶어. 그리고 실제로 얼만큼 지저분해 질 수 있는지도 말이야

[1:14 PM] acemarke: 내가 절대 질문하지 않았다고 추측되네: 네 앱이 어떤 앱이며 어떤 종류의 데이터가 있는지?

[1:15 PM] jrajav:: 상대적으로 간단한 검색-표시 앱이야

[1:15 PM] jrajav:: 메인 페이지는 고급 기능이 포함된 검색 뷰가 있고, 사이드바에는 필터링, 자동완성, 페이지네이션, 용어 강조 같은 것들이 있지

[1:16 PM] jrajav:: 검색 결과는 각 항목에 대한 세부 보기로 링크되고, 대부분 수십 개의 정적인 세부 속성들이 여러 탭 및 뷰로 확산되지

[1:18 PM] jrajav:: 따라서 상태 트리는 쿼리를 위한 ‘search’ 객체, ‘searchResults’ 객체(‘검색’ 아래쪽에 중첩시킬 것을 고려중이야)가 될것이고, 그리고 선택한 아이템에 대한 ‘details’ 객체로 분리될 거야

[1:19 PM] jrajav:: 관리 및 성능에 관련한 큰 문제와 맞닥뜨리기에는 앱이 너무 간단하지만 적절한 조직화에 대한 이해와 최소한의 props 전달에 대해 이해하는 것은 충분히 복잡하네

[1:22 PM] acemarke: 잡았다. ThingEditor 예제와 완전히 다른 건 아니네

[1:22 PM] acemarke: 그리고 성능을 강조해야만 하는 것도 아닌 것 처럼 들리고

Redux 성능 향상을 위한 컴포넌트 리팩토링

By Shahzad Aziz

프론트엔드 웹 개발은 빠르게 진화하고 있다. 매일 새로운 모범 사례에 도전하는 새로운 툴과 라이브러리가 발표된다. 신이 나기도 하지만, 역시 따라가기 벅차다. 개발자의 삶을 편안하게 해주는 새로운 도구 중 하나는 인기 오픈소스 상태 컨테이너인 Redux다. 지난 해 Yahoo! Search 팀은 Redux를 사용해서 데이터 분석에 사용되는 레거시 도구를 새로 고쳤다. Redux와 함께 사용하는 역시 매우 인기있는 프론트엔드 컴포넌트 라이브러리인 React를 사용하여 결합했다. Yahoo Search의 규모로 인해 매초마다 수천개의 측정 항목을 데이터 그리드에 저장한다. 우리의 데이터 분석 툴은 우리 Search 팀의 내부 유저가 저장된 데이터에 쿼리를 날려 트래픽과 A/B를 비교하는 테스트를 할 수 있도록 한다. 새로운 도구를 만드는 가장 큰 목적은 속도와 편의성이였다. 처음 시작했을 때부터 우리는 애플리케이션이 복잡해질 것이며 많은 상태를 보유하게 될 것이라는 걸 알았다. 개발과정에서 예상치 못한 성능 병목현상이 발생했다. 우리는 기술 스택에서 기대했던 성능을 달성하기 위해 깊이 파고들었고 리팩토링을 했다. 이 경험을 공유하고 Redux를 사용하는 개발자에게 React 컴포넌트와 상태 구조에 대한 추론을 통해 애플리케이션 성능을 높이고 쉽게 확장할 수 있도록 하려 한다.

Redux는 당신이 애플리케이션의 상태 변경을 예측 가능하게 하고 쉽게 디버그할 수 있기를 바란다. 이를 Flux 패턴의 아이디어를 애플리케이션의 상태를 단일 저장소에 저장하게 확장함으로써 달성한다. 리듀서를 사용하여 논리적으로 상태를 분할하고 책임을 괌리할 수 있게 한다. React 컴포넌트는 Redux 저장소의 변경 사항을 구독한다. 프론트엔드 개발자에게는 매우 매력적인 멋진 디버깅 도구를 상상할 수 있게 한다.

React를 사용하면 컨테이너와 프리젠테이셔널의 두 가지 카테고리로 컴포넌트를 추론하고 생성하는 것이 쉬워진다. 여기서 더 읽어볼 수 있다. 요점은 컨테이너 컴포넌트가 상태에 대해 구독하고 프리젠테이셔널이 전달된 프로퍼티를 이용해 마크업 렌더링을 한다는 것이다. Redux 문서 초반부에서 우리는 컴포넌트 트리 최상위에 컨테이너 컴포넌트를 추가해야 한다는 것을 알 수 있다. 우리 애플리케이션에서 가장 중요한 부분은 대화형 ResultsTable 컴포넌트다. 간결하게 하기 위해서 게시물의 나머지 부분에서는 이 컴포넌트에 집중할 것이다.

React Component Tree

API에서 최적의 성능을 얻기 위해서 백엔드에 많은 수의 간단한 호출을 수행하고 리듀서에서 이 데이터를 결합하고 필터링한다. 이것은 많은 비동기 액션을 통해 데이터 조각을 가져오고 redux-thunk를 사용하여 제어 흐름을 관리한다는 것을 의미한다. 그러나 사용자의 선택이 변경되면 상태에서 가져온 대부분의 항목이 무효화되므로 다시 가져와야 한다. 대체적으로 꽤 잘 동작하지만, 응답이 돌아옴에 따라 상태도 여러번 변경된다는 의미다.

문제점

API에서 큰 성능 향상을 거두는 동안 브라우저의 프레임 그래프는 클라이언트의 성능 병목 현상을 나타내기 시작했다. 컴포넌트 트리의 상위에 있는 MainPage 컴포넌트는 모든 디스패치에 대해 다시 렌더링되었다. 컴포넌트는 렌더링에 비용 많이 드는 작업이 아니여야만 했지만 엄청난 양의 데이터 행이 있었다. 재 렌더링을 위한 델타 시간은 수초 이상 걸렸다.

어떻게 렌더링 성능을 향상시킬 수 있을까? 잘 알려진 메서드는 shouldComponentUpdate 메서드고, 여기서 시작해보았다. 이 방법은 컴포넌트 트리 전체에서 신중하게 구현해야 하는 일반적인 성능 향상 방법이다. 컴포넌트가 원하는 props가 변경되지 않은 경우에는 재 렌더링을 피할수 있도록 필터링할 수 있었다.

shouldComponentUpdate 개선에도 여전히 전체 UI가 갑갑했다. 사용자의 액션에 대한 응답은 지연되었고, 로딩 인디케이터는 한참 후에 나타났고, 목록 자동완성은 엄청 시간이 걸렸고, 아주 작은 사용자 상호작용조차 느리고 무거왔다. 요점은 React 렌더링 성능이 아니었다.

병목현상을 확인하기 위해 React Performance ToolsReact Render Visualizer라는 두 가지 도구를 사용했다. 실험은 여러가지 다른 액션들을 수행한 뒤에 주요 컴포넌트에 대해 렌더 횟수 및 인스턴스 수에 대한 테이블을 만드는 것이었다.

아래는 우리가 만든 테이블 중 하나다. 빈번하게 사용되는 두 가지 액션을 분석했다. 앞서 말한 우리의 메인 테이블 컴포넌트에 대해 얼마나 많은 렌더가 트리거되는지 살펴보았다.

날짜 변경: 사용자는 기록된 데이터를 가져오기 위해 날짜를 변경할 수 있다. 데이터를 가져오려면 모든 메트릭에 대해 병렬적으로 API를 호출하고 Reducer에서 이를 병합했다.

차트 열기: 사용자는 측정 항목을 선택하여 일별로 꺾은선 차트에 표시할 수 있다. 이것은 모달 다이얼로그를 연다.

실험을 통해 상태가 트리를 통과하면서 관련 컴포넌트에 영향을 미치기 전에 반복적으로 많은 것들을 다시 계산해야 한다는 사실이 밝혀졌다. 이것은 비용이 많이 들고 CPU를 많이 사용했다. UI로 제품을 변경하면 React는 1080ms나 되는 시간을 테이블 및 행 컴포넌트에 사용했다. 우리 쓰레드를 바쁘게 만든 것이 공유된 상태의 문제라는 것을 깨닫는 것이 중요했다. React의 가상 DOM의 성능은 좋지만 가상 DOM을 생성하는 것을 최소화하기 위해 노력해야 한다. 이것은 렌더링을 최소화한다는 것을 의미한다.

성능 리팩토링

주된 생각은 컨테이너 컴포넌트를 살펴보고 상태를 보다 균등하게 컴포넌트 트리에 분산시키려고 시도하는 것이었다. 상태에 대해 더 생각해보고 덜 공유하고 덜 파생하게 만들고 싶었다. 가장 필수적인 아이템을 상태에 저장하고 컴포넌트에서 필요한 상태를 계산하고 싶었다.

리팩토링은 두 단계로 나누어 실행했다:

1단계. 컴포넌트 전반에 걸쳐 상태를 구독하는 컨테이너 컴포넌트를 더 추가했다. 배타적인 상태를 소비하는 컴포넌트는 상위에 컨테이너 컴포넌트가 필요하지 않다. 그 컴포넌트들은 배타적인 상태를 구독하는 컨테이너 컴포넌트가 될 수 있었다. 이것은 React가 Redux 액션에 대한 작업을 크게 줄여준다.

잠재적인 컨테이너 컴포넌트들이 있는 React 컴포넌트 트리

우리는 상태가 트리에서 어떻게 활용되고 있는지 식별했다. 가장 중요한 질문은 “배타적인 상태의 컨테이너 컴포넌트가 더 많이 있는지 확인하는 방법은 무엇인가?”였다. 이를 위해 몇 가지 주요 컴포넌트를 분할하여 컨테이너로 포장하거나 컨테이너 자체로 만들어야 했다.

리팩토링 후 React 컴포넌트 트리

위의 트리에서 MainPage 컨테이너가 더 이상 임의의 테이블 렌더링을 담당하지 않는 것에 유의하라. ResultsTable에서 배타적인 상태를 가진 ControlsFooter를 추출했다. 우리가 집중한 것은 테이블 관련된 컴포넌트의 재 렌더링을 줄이는 것이었다.

2단계. 파생 상태와 shouldComponentUpdate

상태가 잘 정의되고 플랫한지 확인하는 것이 가장 중요하다. Redux 상태를 데이터베이스로 생각하고 정규화하기 시작하면 더 쉽다. 우리는 제품, 통계, 결과, 차트 데이터, 사용자 선택, UI 상테 등과 같은 엔티티를 많이 가지고 있다. 대부분 키/값의 쌍으로 분류하고 중첩을 방지한다. 각 컨테이너 컴포넌트는 상태를 쿼리하고 데이터를 비정규화한다. 예를 들어, 내비게이션 메뉴 컴포넌트는 제품 ID로 메트릭 목록을 필터링해서 전체 메트릭 상태에서 쉽게 제품 메트릭을 추출할 필요가 있었다.

전체 상태에서 반복적으로 파생되는 모든 프로세스는 최적화할 수 있다. Redux Reselect로 들어가보자. Reselect는 파생 상태를 계산하기 위한 셀렉터를 정의하게 해준다. Reselect를 이용해 정의된 셀렉터는 효율성을 위해 이전 상태를 메모할 수 있고, 계단식으로 연결될 수 있다. 따라서 아래 함수를 이용해 셀렉터를 정의할 수 있다. 그리고 메트릭과 productId에 어떠한 변경이 없는 한, 메모해둔 복사본을 반환할 것이다.

getMetrics(productId, metrics) {
 metrics.find(m => m.productId === productId)
}

우리는 모든 비동기 액션이 해결되었는지 확인하고, 상태를 다시 렌더링하기 전에 필요한 모든 것을 상태에 유지하려고 했다. 그래서 컨테이너 컴포넌트의 shouldComponentUpdate 메서드의 동작을 정의하기 위해 셀렉터를 만들었다(예. 테이블은 측정 항목 및 결과가 모두 로드된 경우에만 렌더링 된다). shouldComponentUpdate에서 모든 것을 직접 할 수도 있지만, 셀렉터를 이용하면 다르게 생각할 수 있음을 느꼈다. 컨테이너는 상태를 예측할 수 있게 되고 렌더링 전에 그 상태에서 대기한다. 렌더링 해야하는 상태는 해당 상태의 서브셋일 수 있었다. 더 언급할 필요 없이 더 성능이 뛰어났다.

결과

리팩토링을 마친 후에 다시 실험을 해보아 모든 변경 사항의 영향을 비교했다. 컴포넌트 트리 전체에 걸친 상태 변화를 개선하고 더 향상되기를 기대했다.

위의 표를 보면 한눈에 리팩토링이 어떤 영향을 끼쳤는지 명확히 알 수 있다. 새로운 결과에서는 날짜 변경이 어떻게 comparisonTable(7)과 controlsHeader(2) 간 렌더 횟수를 분배시키는지 관찰할 수 있다. 더해봤자 9번이다. 이러한 논리적인 리팩토링은 최대 4배까지 성능을 높일 수 있었다. 또한 React가 낭비하는 시간을 쉽게 최적화할 수 있었다. 이는 아주 중요한 개선사항이였으며 올바른 방향으로 나아가는 것이었다.

향후 계획

Redux는 프론트엔트 애플리케이션을 위한 훌륭한 패턴이다. 애플리케이션 상태를 더 잘 판단할 수 있게 해주었고, 하나의 스토어를 사용하는 것이 앱이 복잡해짐에 따라 확장하는 것이나 디버깅하는 것을 쉽게 했다.

앞으로는 Redux State를 위한 immutability를 알아보려고 한다. Redux의 상태 변경에 대해 규칙을 정하고 shouldComponentUpdate에서 shallow comparison를 이용하는 경우를 생각하려고 한다.