원문보기

우리는 최근 몇 달 간의 작업 끝에, 가장 인기있는 React 스타터 킷 중 하나인 React Boilerplate의 버전3를 릴리즈했다. 팀은 수백명의 개발자가 있는 팀은 어떻게 웹 애플리케이션을 만들고 확장하는지에 대해서 이야기했고, 우리가 그 과정에서 배운 것을 공유하고자 한다.

우리는 작업 초기에 “그저 또다른 보일러플레이트”가 되는 것을 원치 않았다. 우리는 제품 개발을 시작하는 개발자에게 확장성 있는 최고의 개발 토대를 제공하려고 했다.

전통적으로, 소프트웨어 규모의 확장은 대부분 서버사이드 시스템과 관련있었다. 그리고 점점 많은 사용자가 당신의 애플리케이션을 사용할 것이므로, 클러스터에 더 많은 서버를 클러스터에 추가할 수 있는지, 데이터베이스를 여러 서버로 분할할 수 있는지 등을 확인해야할 필요가 있었다.

최근에는 리치 웹 에플리케이션 때문에 프론트엔드 쪽에서도 역시 규모의 확장이 더 중요한 주제가 되었다. 복잡한 앱의 프론트엔드는 더 많은 수의 사용자들과 개발자들과 파트들을 처리할 수 있어야 한다. 이러한 3가지 카테고리의 확장(사용자, 개발자, 파트)을 고려해야 한다. 그렇지 않으면 문제가 발생할 것이다.

컨테이너들과 컴포넌트들

큰 애플리케이션의 명확한 첫 번째 개선점은 상태가 있는 컴포넌트(“컨테이너”)와 상태가 없는 컴포넌트(“컴포넌트”) 간의 차이다. 컨테이너는 데이터를 관리하거나 상태와 연결되어 있고 일반적으로 그들과 연관된 스타일링을 가지고 있지 않다. 한편, 컴포넌트는 그들과 연관된 스타일링을 가지고 있으며 어떤 데이터나 상태 관리에 대한 책임을 가지고 있지 않다. 처음에는 이것이 혼동되었다. 기본적으로 일이 어떻게 작동하는지 책임을 지고 구성 요소는 일이 어떻게 보이는지에 대해 책임을 진다.

이와 같이 컴포넌트를 분리하면 재사용 가능한 컴포넌트와 데이터를 관리하는 중간 계층을 명확하게 구분할 수 있다. 따라서, 데이터 구조가 엉망이 될 염려 없이 컴포넌트를 수정할 수 있고, 스타일링이 엉망이 될 염려 없이 컨테이너를 수정할 수 있다. 그리하여 애플리케이션의 동작을 추론하는 것이 훨씬 더 쉬워지고 명확하게 된다!

구조

전통적으로, 개발자들은 React 애플리케이션을 타입에 따라 구조화했다. 이것이 뜻하는 바는 애플리케이션이 actions/, components/, containers/ 등과 같은 폴더를 가지고 있다는 뜻이다.

NavBar라고 불리는 네비게이션 바 컨테이너를 생각해보자. NavBar는 몇몇 연관된 상태를 가지고 있고 네비게이션 바를 열고 닫는 toggleNav과 같은 액션을 가지고 있을 것이다. 이것이 파일을 타입별로 그룹지어 구조화하는 방법이다.

react-app-by-type
		├── css
		├── actions
		│   └── NavBarActions.js
		├── containers
		│   └── NavBar.jsx
		├── constants
		│   └── NavBarConstants.js
		├── components
		│   └── App.jsx
		└── reducers
		    └── NavBarReducer.js

이 예제에서는 잘 동작하겠지만, 수백 혹은 잠재적으로 수천 개의 컴포넌트가 있을 수 있는데, 이런 경우에는 개발이 매우 힘들어진다. 기능을 추가하려면 수천 개의 파일이 있는 수십 개의 서로 다른 폴더에서 올바른 파일을 검색해야 한다. 이렇게 되면 쉽게 지칠 뿐더러 코드 베이스에 대한 확신도 줄어들 것이다.

Github의 이슈 트래커에서 오랜 논의 끝에 다양한 구조를 시도한 결과, 우리는 훨씬 더 나은 해결책을 발견했다고 생각한다:

애플리케이션의 파일을 유형별로 그룹화하는 대신 기능별로 그룹화하라! 즉, 한 기능(예: 네비게이션 바)과 관련된 모든 파일을 같은 폴더에 넣는다.

NavBar 예제에서 폴더 구조가 어떤지 확인해보자:

react-app-by-feature
		├── css
		├── containers
		│    └── NavBar
		│        ├── NavBar.jsx
		│        ├── actions.js
		│        ├── constants.js
		│        └── reducer.js
		└── components
		    └── App.jsx

이 애플리케이션의 개발자는 무언가를 작업하기 위해 하나의 폴더로 이동해야 한다. 그리고 새로운 기능을 추가하기 위해서 이 폴더 아래 단 하나의 폴더만 만들어야 한다. 찾기 및 바꾸기 기능으로 이름 바꾸기는 쉽고, 수백명의 개발자가 충돌없이 한 번에 동일한 애플리케이션에서 작업할 수 있다!

내가 처음으로 이렇게 작성하는 방법에 대해 알았을 때 생각하기를 “왜 그렇게 했었을까? 다른 방법으로도 잘 동작하네!” 나는 내 오픈 마인드에 자부심을 느끼고, 작은 프로젝트에서 이 구조화를 시도해봤다. 15분 후에 감동받았다. 내 코드 베이스에 대한 자신감은 막대했고, 그 위에서 일하는 것은 매우 쉬웠다.

이것이 반드시 redux 액션과 리듀서를 해당 컴포넌트에서만 사용할 수 있다는 것을 의미하는 것은 아니라는 것에 주의하라. 다른 컴포넌트에서도 얼마든지 임포트하여 사용될 수 있다!

이런 식으로 일하는 동안 두 가지 질문이 내 머리 속에서 튀어나왔다. “어떻게 스타일링을 처리할 수 있을까?” 그리고 “어떻게 원격 데이터를 가져올 수 있을까?” 이 질문들을 각각 다뤄보겠다.

스타일링

아키텍쳐를 위한 결정 외에도, 컴포넌트 기반 아키텍쳐에서 CSS를 사용하는 것은 언어 자체의 두 가지 특정 속성 때문에 어렵습니다: 글로벌 네임 충돌과 상속

유니크 클래스 네임

커다란 어플리케이션 내 어딘가의 CSS를 상상해보자:

.header { /* … */ }
.title {
	background-color: yellow;
}

즉시 문제가 있음을 알 수 있다: title은 매우 일반적인 이름이다. 다른 개발자(혹은 시간이 흐른 후의 같은 개발자)가 다음과 같은 코드를 작성할 수 있다.

.footer { /* … */ }
.title {
	border-color: blue;
}

이름 충돌이 발생하여 갑자기 파란색 테두리와 노란색 배경의 title이 도처에 나타나며, 모든 것을 엉망으로 만든 이 선언을 찾기 우해 수천 개의 파일을 파고들어야 할 것이다.

고맙게도, 소수의 똑똑한 개발자들이 있어서 CSS 모듈이라는 이름의 이 문제에 대한 해결책을 제시했다. 그들의 접근 방식의 핵심은 폴더에 있는 컴포넌트에 스타일을 함께 배치하는 것이다.

	react-app-with-css-modules
		├── containers
		└── components
		     └── Button
		         ├── Button.jsx
		         └── styles.css

특정한 명명 규칙을 염려할 필요가 없다는 것을 제외하면 CSS는 완전히 동일해 보인다. 그리고 매우 일반적인 이름을 지정할 수 있다:

.button {
	/* … */
}

그러고나서 CSS 파일을 컴포넌트에 넣거나 가져오고 JSX 태그에 styles.button을 className으로 지정한다:

/* Button.jsx */
var styles = require('./styles.css');

<div className={styles.button}></div>

브라우저에서 DOM을 살펴보면 <div class="MyApp__button__1co1k"></div>와 같이 표시된다! CSS 모듈은 애플리케이션 이름을 추가하고 클래스 내용의 짧은 해시 뒤에다 추가하여 클래스 이름을 “고유하게” 관리한다. 즉, 클래스가 겹칠 가능성은 거의 없다. 혹여 중복되는 경우라도 상관없다.(해시 - 즉 스타일 내용이 동일하므로)

각 컴포넌트를 위한 속성 리셋

CSS에서 특정 속성은 여러 노드에 걸쳐 상속된다. 예를 들어, 부모 노드에 line-height가 설정되어 있고 하위에 지정된 것이 없으면 자동으로 부모 노드와 동일한 line-height가 적용된다.

컴포넌트 기반 아키텍쳐에서는 이런 것을 원하지 않는다. 다음과 같은 스타일을 가진 Header와 Footer 컴포넌트를 상상해보자:

.header {
	line-height: 1.5em;
	/* … */
}

.footer {
	line-height: 1;
	/* … */
}

우리가 이 두 컴포넌트 내부에 Button을 렌더링하자고 가정하자. 갑자기 헤더와 푸터에서 다른 모양의 버튼이 보이게 된다! line-height 뿐만 아니라 CSS 속성들도 상속된다. 애플리케이션에서 이러한 버그를 추적하고 제거하는 것은 굉장히 어려울 것이ㅏ.

프론트엔드 세상에서, 스타일 시트를 리셋해서 브라우저간에 스타일을 표준화 하는 것은 아주 일반적이다. Reset Css, Normalize.css sanitize.css등의 옵션이 인기있다! 만약 이런 개념을 가지고 모든 컴포넌트를 리셋하면 어떨까?

이를 auto-reset이라고 부르고, PostCSS용 플러그인이 존재한다! 만약 PostCss Auto Reset을 PostCSS 플러그인으로 추가하면, 각 컴포넌트를 로컬 리셋으로 감싸고, 모든 상속 가능한 특성을 그들의 기본값으로 설정하여 상속을 오버라이드한다.

Data-Fetching

이 아키텍쳐와 관련된 두 번째 문제점은 원격 데이터를 가져오는 것이다. 컴포넌트와 그 작업을 함께 배치하는 것은 대부분의 작업에 적합하지만 원격 데이터를 가져오는 것은 본질적으로 단일 컴포넌트에 묶여있지 않은 전역 작업이다!

대부분의 개발자는 Redux로 원격 데이터를 가져올 때 Redux Thunk를 사용한다. 일반적인 thunked 액션은 다음과 같다:

/* actions.js */

function fetchData() {
	return function thunk(dispatch) {
		// Load something asynchronously.
		fetch('https://someurl.com/somendpoint', function callback(data) {
			// Add the data to the store.
			dispatch(dataLoaded(data));
		});
	}
}

이것은 원격 데이터를 가져올 수 있는 훌륭한 방법이지만 두 가지의 문제점이 있다: 함수를 테스트하는 것이 매우 어렵고, 개념적으로는 액션에서 원격 데이터를 가져오게 하는 것이 옳은 것처럼 보인다는 것이다.

Redux의 큰 장점은 테스트하기 쉬운 순수한 액션 생성자다. 액션에서 썽크를 반환하게 하면 우리는 액션을 2번 호출하고 dispatch 함수를 목킹하는 작업 등을 해야한다.

최근의 새로운 접근방식이 React 세계에 폭풍을 불러왔다: redux-saga. redux-saga는 Esnext의 generator를 사용하여 비동기 코드를 동기식으로 보이게 만들고 매우 쉽게 테스트할 수 있게 한다. saga 뒤에 숨겨진 모든 멘탈 모델은 애플리케이션의 나머지 부분을 괴롭히지 않고도 모든 비동기 작업을 처리하는 별도의 스레드와 같이 만들어준다!

예제로 설명하겠다:

/* sagas.js */

import { call, take, put } from 'redux-saga/effects';

// The asterisk behind the function keyword tells us that this is a generator.
function* fetchData() {
	// The yield keyword means that we'll wait until the (asynchronous) function
	// after it completes.
	// In this case, we wait until the FETCH_DATA action happens.
	yield take(FETCH_DATA);
	// We then fetch the data from the server, again waiting for it with yield
	// before continuing.
	var data = yield call(fetch, 'https://someurl.com/someendpoint');
	// When the data has finished loading, we dispatch the dataLoaded action.
	put(dataLoaded(data));
}

이상한 모양의 코드에 겁먹지 마라: 비동기식 흐름을 처리하는 훌륭한 방법이다! 콜백 헬을 피할 수 있고, 거의 소설처럼 쉽게 읽히는 코드, 게다가 테스트하기도 쉽다. 자, 스스로에게 물어보라, 왜 테스트하기가 쉬울까? 이유는 완료되지 않은 상태에서도 “effect”들을 노출하는 redux-saga의 능력 때문이다.

파일의 맨 위에서 임포트한 effects는 redux 코드와 상호작용할 수 있도록 도와주는 핸들러다.

put() 우리 saga로부터 액션을 디스패치한다.

take() 앱에서 액션이 일어날때까지 일시 중지한다.

select() redux의 state의 일부분을 가져온다.(mapStateToProps 같은 역할)

call() 첫 번째 인수를 나머지와 함께 전달하여 함수를 호출한다.

왜 이러한 effects들이 효과적인가? 테스트 예제를 한번 보자:

/* sagas.test.js */

var sagaGenerator = fetchData();

describe('fetchData saga', function() {
	// Test that our saga starts when an action is dispatched,
	// without having to simulate that the dispatch actually happened!
	it('should wait for the FETCH_DATA action', function() {
		expect(sagaGenerator.next()).to.equal(take(FETCH_DATA));
	});

	// Test that our saga calls fetch with a specific URL,
	// without having to mock fetch or use the API or be connected to a network!
	it('should fetch the data from the server', function() {
		expect(sagaGenerator.next()).to.equal(call(fetch, 'https://someurl.com/someendpoint'));
	});

	// Test that our saga dispatches an action,
	// without having to have the main application running!
	it('should dispatch the dataLoaded action when the data has loaded', function() {
		expect(sagaGenerator.next()).to.equal(put(dataLoaded()));
	});
});

Exnext의 generator는 generator.next()가 호출될 때까지 yield 키워드를 지나가지 않는다. generator는 다음 yield 키워드를 만나기 전까지 함수를 실행한다! redux-saga effects를 사용함으로 테스트를 위해 네트워크에 의존하지 않아도 되고 무언가를 모킹할 필요도 없다. 아주 쉽게 비동기 작업을 테스트를 해볼 수 있다.

한편, 우리는 테스트 파일들도 역시 테스팅중인 파일들과 함께 배치한다. 왜 다른 폴더에 넣어야 하는가? 이렇게 하면 컴포넌트와 관련된 모든 파일이 실제로 테스트할 때도 같은 폴더에 있게된다!

redux-saga의 이점이 여기서 끝난다고 생각한다면, 큰 실수다! 사실, 원격 데이터 가져오기를 쉽게 하고, 아름답고 테스트하기 쉬운 코드는 redux-saga의 가장 작은 이점일 뿐이다!

redux-saga를 모르타르로 사용하기

컴포넌트들은 이제 분리되었다. 다른 스타일이나 로직에 신경쓰지 않는다; 자신의 사업에만 관심을 갖고있다 – 음, 거의 대부분은.

Clock과 Timer 컴포넌트를 상상해보자. 시계의 버튼을 누르면, 타이머가 시작된다; 그리고 타이머의 정지 버튼을 누르면 시계에 시간을 표시하려고 한다.

일반적으로는 다음과 같이 했을 것이다:

/* Clock.jsx */

import { startTimer } from '../Timer/actions';

class Clock extends React.Component {
	render() {
		return (
			/* … */
			<button onClick={this.props.dispatch(startTimer())} />
			/* … */
		);
	}
}

/* Timer.jsx */

import { showTime } from '../Clock/actions';

class Timer extends React.Component {
	render() {
		return (
			/* … */
			<button onClick={this.props.dispatch(showTime(currentTime))} />
			/* … */
		);
	}
}

갑자기 이 컴포넌트들을 분리하여 사용할 수 없게되고, 재사용은 거의 불가능해진다!

이렇게 하는 대신, redux-saga를 분리된 컴포넌트 사이의 “모르타르”로 사용할 수 있다. 다른 액션을 리스닝함으로써, 애플리케이션에 따라 다양한 방식으로 대응할 수 있다. 즉 컴포넌트가 이제 완전히 재사용 가능해진다는 뜻이다.

컴포넌트를 수정해보자:

/* Clock.jsx */

import { startButtonClicked } from '../Clock/actions';

class Clock extends React.Component {
	/* … */
	<button onClick={this.props.dispatch(startButtonClicked())} />
	/* … */
}

/* Timer.jsx */

import { stopButtonClicked } from '../Timer/actions';

class Timer extends React.Component {
	/* … */
	<button onClick={this.props.dispatch(stopButtonClicked(currentTime))} />
	/* … */
}

각 컴포넌트가 자체적인 액션만 import하고 관심을 같는 것을 유의하라! 자, 이제 사가를 사용해서 분리된 컴포넌트를 하나로 뭉쳐보자:

/* sagas.js */

import { call, take, put, select } from 'redux-saga/effects';

import { showTime } from '../Clock/actions';
import { START_BUTTON_CLICKED } from '../Clock/constants';
import { startTimer } from '../Timer/actions';
import { STOP_BUTTON_CLICKED } from '../Timer/constants';

function* clockAndTimer() {
	// Wait for the startButtonClicked action of the Clock
	// to be dispatched.
	yield take(START_BUTTON_CLICKED);
	// When that happens, start the timer.
	put(startTimer());
	// Then, wait for the stopButtonClick action of the Timer
	// to be dispatched.
	yield take(STOP_BUTTON_CLICKED);
	// Get the current time of the timer from the global state.
	var currentTime = select(function (state) { return state.timer.currentTime });
	// And show the time on the clock.
	put(showTime(currentTime));
}

아름답다.

요약

기억해야할 주요 사항은 다음과 같다:

컨테이너와 컴포넌트를 구별하라. 파일을 피처 단위로 구조화하라. CSS 모듈과 PostCSS Auto Reset을 사용하라. redux-saga를 사용하라 - 다음 목적으로: 비동기 흐름을 테스트하고 읽기 쉽게하고, 분리된 컴포넌트를 합쳐라.

원문보기

CSS의 진화: CSS, SASS, BEM, CSS Modules로부터 Styled Components로

초기 인터넷 시절부터 우리는 항상 웹사이트를 스타일링할 필요가 있었다. CSS는 영원히 계속되었고 지난 몇 년간 독자적인 페이스로 발전해왔다. 이 기사를 통해 여러분과 그 지난 몇 년 간을 짚어본다.

CSS는 무엇과 함께 있어야할 필요가 있는지 먼저 생각해보자. 당신들 모두 CSS를 마크업 언어로 작성된 문서의 표현을 기술하는데 사용되야 한다는 데에 동의할 것이다.

CSS가 강력해졌고 진화해졌다는 뉴스는 없지만, CSS를 어떻게든 활용할 수 있도록 추가적인 도구들이 있다는 것은 널리 알려져 있다.

미개척 서부의 CSS

90년대에 우리는 “멋진” 인터페이스를 만드는데 중점을 뒀다. 와우 팩터는 가장 중요한 것이었고, inline style이 그당시에 쓰였던 것이다. 그리고, 우리는 그저 다르게 보이게 하는 것이라면 다른 것은 신경쓰지 않았고, 결국 웹페이지는 몇몇 gifs와 marquees, 끔찍한(당시에는 인상적인) 엘레멘트를 이용해서 방문객의 주의를 끌기를 기대하는 귀여운 장난감처럼 보였었다.

그 후에 동적 사이트를 만들기 시작했을 때 각 개발자들이 css를 다루는 자신만의 방법을 가지는 일관성 없는 상태로 남겨졌다. 우리 중 몇몇은 새 코드를 도입할때 비주얼 리그레션을 일으키는 특수함과 사투를 펼쳤다. 우리는 !important에 의존해서 UI를 정확히 특정한 방식으로 보이게 하려고 했으나, 곧 깨달았다:

프로젝트의 규모, 복잡성 및 팀 구성원이 증가함에 따라 이러한 모든 관행들이 더욱 분명하고 큰 문제가 되었다. 따라서 스타일을 유지하는 데 있어서 개발자의 경험이 많고 적음을 가리지않고를 떠나 모두가 일관된 패턴을 갖추지 못한 CSS를 다루는 올바른 방법을 찾기위해 사투를 펼쳤다. 그러나 결국, 올바른지 아닌지는 상관하지 않고, 그저 제대로 보이기만 하면 되는데 신경을 쓰게 되었다.

SASS 구조대

SASS는 중첩, 변수, 믹스, 확장 및 로직을 스타일시트에 구현한 전처리 엔진 형태로 CSS를 적절한 프로그래밍 언어로 변형하여 CSS 파일을 보다 잘 구성하고 CSS 덩어리를 더 작은 파일로 분류할 수 있었다. 그 당시에는 아주 훌륭해 보였다.

본질적으로 그것은 SCSS 코드를 가지고 전처리하고 컴파일된 버전을 CSS 묶음으로 출력하는 것이다. 훌륭하지 않은가? 그러나 별로 말할 것도 없이, 시간이 흐르자 적절한 전략과 모범사례가 없다면 SASS는 더 많은 문제를 일으킨다는 것이 분명해졌다.

갑자기 우리는 이 전처리기가 후드 아래서 무엇을 하고 있는지 신경쓰지 못했고, lazily nesting에 의존하여 특수성과의 싸움을 정복했으나 컴파일된 스타일시트의 크기는 엄청나졌다.

BEM이 나타나기 전까지는….

BEM과 컴포넌트 기반의 생각

BEM이 나왔을 때는 공기 중에 재사용성과 컴포넌트에 대해 더 생각하게 만드는 신선한 공기가 맴돌았다. 그것은 의미론을 새로운 수준으로 끌어올렸고, 단순한 Block Element Modifier 컨벤션을 통해 className을 고유하게 만들었다. 다음 예제를 보자:

마크업을 약간 분석하면 즉시 Block Element Modifier가 여기서 놀고 있음을 알아차릴 수 있다.

.scenery와 .sky라는 블록을 가지고 있음을 확연히 알 수 있다. 각 블록들은 또 자체적인 블록을 가지고 있다. Sky는 foggy, daytime, dusk처럼 같은 엘레멘트에 적용될 수 있는 다른 특성들을 가진 유일한 엘레멘트다.

더 나은 분석을 할 수 있도록 pseudo 코드를 사용한 CSS를 살펴보자:

BEM이 어떻게 동작하는지 깊이 알고 싶다면, 내 동료이자 친구인 Andrei Popa가 작성한 이 기사를 추천한다.

BEM은 컴포넌트가 고유한 #reusabilityFtw임을 보장한다는 의미에서 유용하다. 이런 종류의 생각은 패턴으로 나타났고 예전 스타일 시트를 이 새로운 컨벤션으로 이전하기 시작하면서 더 명백해졌다.

그러나, 또 다른 문제들이 발생했다:

  • 클래스 이름으로 셀렉트하는 일이 끔찍한 일이 되었다.
  • 마크업이 길다란 클래스 이름으로 비대해졌다.
  • 재사용할 때마다 모든 ui 컴포넌트를 명시적으로 확장해야만 했다.
  • 마크업이 불필요하게 의미론적이 되었다.

CSS 모듈과 로컬 스코프

SASS나 BEM이 수정하지 않은 문제 중 일부는 언어 논리에 진정한 캡슐화 개념이 없으므로 개발자가 선택한 고유 클래스 이름에 의존해야 한다는 것이였다. 컨벤션보다는 도구로 처리할 수 있을 것 같았다.

이것이 바로 정확히 CSS 모듈이 한 일이다. 로컬 정의된 각 스타일에 의존해 동적으로 클래스 이름을 만들었다. 모든 스타일을 적절하게 캡슐화해서 새로운 CSS 속성을 주입함에 따라 생기는 시각적 퇴행현상을 없애버린 것이다.

React 생태계에서 CSS-모듈은 빠르게 인기를 얻었으며, 이제 기본적으로 많은 React 프로젝트들이 이것을 일반적으로 사용하고 있다. 장점과 단점이 있지만 사용하기 좋은 패러다임이 입증된 것이다.

그러나… CSS 모듈 자체 만으로는 CSS의 핵심 문제를 해결하지는 못한다. 단지 스타일 정의를 지역화하는 방법만을 보여주었을 뿐이다: 한번 선택했던 클래스이름을 다시 선택하지 않는 자동화된 BEM이 있어서 당신이 클래스 네임을 고민할 필요가 없는 것이다(적어도 더 조금만 고민하면 되는)

그러나 이것이 최소한의 노력만으로 예측 가능하고 확장 재사용이 가능한 아키텍쳐에 대한 필요성을 줄이지는 못한다.

이것이 로컬 CSS의 모양이다:

단지 그것이 CSS라는 것을 알 수 있다. 가장 큰 차이는 모든 클래스네임 앞에 유니크한 클래스 이름을 생성하도록하는 :local이 붙는 것이다. 그 유니크한 클래스 이름은 다음과 같다:

.app-components-button-__root — 3vvFf {}

localIdentName 쿼리 파라미터를 통해서 생성되는 글자를 설정할 수 있다. 디버깅이 쉽도록 하는 예를 들면:

css-loader?localIdentName=[path][name]---[local]---[hash:base64:5]

처럼 할수 있다.

이것이 Local CSS Modules의 기본 원리다. 로컬 모듈이 동일한 이름을 사용하더라도 다른 모듈과 동일하지 않을 것이라는 것을 보장하는 자동화된 BEM 표기방법을 제공하는 방법이다. 매우 편리하다.

Styled Components to blend css in JS (fully)

Styled-components는 순수한 시각적 primitives다; 실제 html 태그에 매핑될 수 있으며 자식 컴포넌트들을 styled-component로 감싸는 일을 한다.

다음 코드로 더 설명하는 것이 나을 것이다:

보기만해도 styled component는 굉장히 이해하기 쉬울 것이다. 템플릿 리터럴 표기법을 사용하여 css 프로퍼티를 정의하고 있다는 것은 core styled-compoents 팀이 es6와 css의 모든 파워를 혼합한 것 처럼 보인다.

Styled-components는 기능적, 상태적인 컴포넌트에서 UI를 완전히 분리하고 재사용할 수 있는 매우 단순한 패턴을 제공한다. React Native 혹은 브라우저의 HTML 양쪽 태그 모두를 억세스하는 api를 만드는 것이다.

이것이 Styled Component에 props(혹은 modifiers)를 전달하는 방법이다:

props가 갑자기 각 컴포넌트가 받는 modifier가 되고, css 몇 라인의 출력으로 처리될 수 있음을 알 수 있다. 산뜻하지 않은가?

따라서 스타일을 처리하는데 JS의 모든 기능을 사용하면서 일관성있고 재사용 가능한 상태를 유지한 채로 빠르게 움직일 수 있게 한다.

모든 사람이 재사용하는 Core UI

CSS 모듈이나 Styled Components나 그 자체로는 완벽한 솔루션이 아니라는 것은 꽤 빠르게 명백해졌고, 작동과 확장을 목적으로 하는 몇 가지 패턴이 필요하다. 패턴은 컴포넌트가 무엇인지 정의하는 것과, 로직으로부터 완전히 분리하는 것, 스타일을 지정하는 것 뿐이다.

CSS 모듈을 사용한 컴포넌트의 구현 예제다:

보다시피 팬시한 뭔가가 있지는 않고, 그저 props를 받아서 하위 컴포넌트에 매핑하는 컴포넌트일 뿐이다. 다시 말하자면, props를 children에 전달하는 포장용 컴포넌트일 뿐이다.

다음과 같은 방법으로 컴포넌트를 사용할 수 있다.

styled-components를 사용하여 구현한 비슷한 예제를 살펴보자:

이 패턴이 흥미로운 점은 컴포넌트가 dumb이고 상위 컴포넌트에 매핑된 css 정의의 wrapper일 뿐이라는 것이다. 이 일을 하는 것에는 한 가지 이점이 있다:

기본 UI api를 정의하게 해주므로 이를 스왑함으로 모든 UI가 애플리케이션 전체에 걸쳐 일관성있게 유지되도록 해준다.

이 방법은 디자인 처리와 구현 처리를 완전히 분리시켜서 원한다면 각 처리를 병렬적으로 시작할 수 있다. 피쳐 구현에 포커싱한 개발자와 UI를 갈고닦는 개발자에 대해서 서로 완전히 관심사를 분리할 수 있게 한다.

지금까지는 아주 훌륭한 해결책인것 같다. 내부적으로 우리는 이 문제를 두고 토론을 시작했으며 좋은 아이디어라고 생각했다. 그리고 이 패턴과 함께 또다른 유용한 패턴을 식별하기 시작했다:

Prop receivers

이 패턴은 어떤 컴포넌트로 전달된 props를 리스닝하는 기능을 하므로, 어떤 컴포넌트에든 이 기능을 사용하기 쉽고, 어떤 주어진 컴포넌트라도 재사용성과 가용성을 확장할 수 있는 성배로 만들어준다. modifier를 상속하는 방법으로 생각할수도 있다. 다음 예제가 바로 내가 말하는 바다:

이 방법을 통해 특정 컴포넌트를 위한 각 border를 하드코드할 필요가 없음을 확신할 수 있다. 엄청나게 시간을 절약할 수 있다.

Placeholder / Mixin like functionality

styled component에서는 JS의 풀파워를 사용할 수 있다. 즉 prop receivers 뿐만 아니라 서로 다른 컴포넌트 간의 코드 공유같은 방법으로. 여기 예제가 있다:

Layout Components

우리는 애플리케이션 작업을 할때 처음으로 필요한 것이 UI elements를 레이아웃 하는 것임을 알아냈다. 이를 처리하는 과정에서 레이아웃 목적으로 사용하는 몇몇 컴포넌트를 식별해냈다.

이 컴포넌트들은 종종 구조를 세팅하는 힘든 시간을 보내는 몇몇 개발자들에게 매우 유용함이 증명되었다(css 포지셔닝 테크닉보다 충분히 익숙하지는 않다). 여기 그런 컴포넌트들의 예제가 있다:

width와 height를 props로 받고 horizontal prop도 받아서 스크롤바를 하단에 출력하는 <ScrollView /> 컴포넌트를 볼 수 있다.

Helper components

헬퍼 컴포넌트는 엄청난 재사용을 허용하여 우리의 삶을 간단하게 만든다. 우리의 모든 공통 패턴을 저장한 장소다.

지금까지 꽤 유용했던 헬퍼 몇몇은 다음과 같다:

Theme

테마를 지원하는 것은 애플리케이션 전체에 걸친 1개의 source of truth of values를 가지게 할 것이다. 이는 컬러 팔렛트나 공통 룩앤필과 같은 애플리케이션에서 전체적으로 재사용되는 값들을 저장하는데 유용함이 증명되었다.

장점

  • 풀파워의 JS가 우리 손에 들어왔다. 이는 컴포넌트 UI와 풀 커뮤니케이션을 의미한다.
  • className을 통한 컴포넌트와 스타일의 매핑을 제거한다.(매핑은 속에서 완료된다)
  • 지금까지 중 최고의 개발 경험을 제공한다. 클래스네임과 컴포넌트를 매핑을 생각하는데 들이는 엄청난 양의 시간을 줄인다.

단점

  • 야생에서 테스트된 적이 없다.
  • React를 위해 개발되었다.
  • 아직은 정말 미성숙하다.
  • classNames를 사용하거나 arai-labels를 위한 테스팅이 필요하다.

결론

SASS, BEM, CSS 모듈, Styled Components 어떤 기술을 사용하더라도 다른 개발자들이 시스템의 새로운 파트를 가져오는 것이나 시스템을 깨뜨리는 것에 대해 너무 많이 생각하지 않아도 당신의 코드 베이스에 기여할 수 있도록 잘 정의된 스타일링 아키텍쳐를 대체할 수는 없다.

이 접근 방법은 애플리케이션을 적절하게 확장시키는데 주요하다. plain CSS 혹은 BEM을 사용하더라도 중요한 차이점은 각 구현에 필요한 작업량과 LOC다. 전반적으로 보면 styled-components는 거의 모든 React 프로젝트에 잘 맞고, 아직 야생에서 태스트되지는 않았지만 충분히 유망한 스타일링 방법이다.

의견이나 의견, 조언 등이 있으시면 아래에 의견을 남기고 트위터 @perezpriego7을 통해 연락 바란다.

Refact, Ember, Rails, Elixir, GraphQL을 사용하여 멋진 프로젝트에서 작업하고 싶다면 AlphaSights에서 채용 중이다. 채용 직군을 확인하려면 https://engineering.alphasights.com/#positions 를 방문하라.

원문보기

어쩌면 당신에게 Redux가 필요하지 않을 수도 있다

사람들은 종종 필요하기도 전에 Redux를 선택한다. “Redux가 없어도 앱을 확장할 수 있을까?”. 나중에 개발자들은 Redux를 간접적으로 힐난한다. “이 간단한 기능을 동작시키는 일에 파일을 3개나 손대야하지?”. 정말 왜 그럴까!

사람들은 Redux, React, 함수형 프로그래밍, immutability 등등을 핑계로 자신의 불행을 탓한다. 그리고 나는 그 사람들을 이해한다. 상태를 업데이트하기 위해 보일러플레이트 코드를 요구하지 않는 접근방법들과 Redux를 비교하는 것은 당연하고, Redux가 그저 복잡하기만 하다는 결론을 내게 되는 것도 당연하다. 어떤 면에서 보면, 그걸 의도한 설계이기도 하다.

Redux는 절충안을 제안하고 있다. 당신에게 다음과 같이 요구하는 것이다:

  • 애플리케이션 상태를 플레인 오브젝트와 배열로써 기술하라
  • 시스템의 변경 사항을 플레인 오브젝트로써 기술하라
  • 변경사항을 처리하는 로직은 순수(pure)함수로 기술하라

React를 쓰든지 안쓰든지 간에 이러한 제한점이 Redux 애플리케이션을 만드는데 요구된다. 사실 이들은 꽤 강력한 제약이고, 앱의 어떤 부분에 적용하더라도 주의 깊게 생각해야만 한다.

이렇게 하는데 어떤 좋은 이유라도 있는가?

이러한 제한이 다음과 같이 앱을 만드는 것을 도와주기 때문에 매력적이다:

만약 당신이 확장 가능한 터미널이나 자바스크립트 디버거 혹은 어떤 종류의 웹앱들에서 작업하는 경우라면, 적어도 이 아이디어들을 고려해보거나 실제로 시도해보는 것은 가치가 있을 것이다(별로 새로운 것아니다).

그러나 React를 배우는 중이라면, Redux를 첫 선택으로 만들지 말아달라.

대신 Thinking in React를 배우라. 정말로 Redux가 필요할 때, 혹은 뭔가 새로운 것이 필요할 때만 Redux로 돌아오라. 하지만 고집불통 툴을 사용하는 것처럼 조심해서 접근하라.

만약 “Redux 방식”으로 하는 것에 압박을 느낀다면, 당신이나 당신의 팀 동료들이 이를 심각하게 받아들이는 신호일 수 있다. Redux는 도구 중 하나일 뿐이며, 거친 실험이다.

마지막으로, Redux를 사용하지 않고도 Redux의 아이디어를 적용할 수 있다는 것을 잊지 말아라. 예를 들자면, 로컬 상태를 가지는 React 컴포넌트를 생각해보자:

완벽하게 좋다. 진지하게 반복하건대,

로컬 상태는 아주 좋다.

Redux가 제공하는 절충안은 “어떻게 그것이 바뀌었는가”에서 “무엇이 일어났는가”로 분리하는 간접 참조를 추가하는 것이다.

이 절충안이 항상 좋은 것인가? 아니 절충안은 절충안일 뿐이다.

예를 들면, 컴포넌트에서 리듀서를 추출할 수 있다:

와우! 어떻게 npm install을 실행하지 않고 Redux를 사용했는지 보라.

당신의 상태적인(stateful) 컴포넌트에서도 이렇게 해야할까? 아마도 아닐 것이다. 즉, 이 추가적인 간접 지시 방식로부터 어떤 이득을 취할 계획이 있는게 아니라면 이렇게 할 필요가 없다는 것이다. 계획을 갖는 것. 그게 바로 이 글의 요점이다.

Redux 라이브러리는 하나의 글로벌 스토어 객체에다가 리듀서들을 “탑재”하는 것을 도와주는 도우미들일 뿐이다. 딱 당신이 원하는 조금의 양 만큼만 Redux를 사용해도 된다.

하지만 뭔가를 절충하기로 했다면 무언가를 확실히 얻어내야 한다.

원문보기

목차

성능

Redux를 어떻게 성능과 아키텍쳐 측면에서 잘 “확장”“할 수 있나요?

이 문제에 대해서 하나의 확실한 대답을 할 수 없지만, 대부분의 경우 이 문제는 걱정할 필요가 없어야 한다.

Redux에 의한 작업 완료는 몇 구역으로 나뉘어진다: 미들웨어와 리듀서들에서 액션 처리(immutable한 업데이트를 위한 오브젝트 복제를 포함), 액션이 디스패치되고 나서 구독자들에게 알림 보내기, 상태 변화에 따라 UI 컴포넌트를 업데이트하기. 충분히 복잡한 상황에서는 이들 각각이 성능 우려를 만들만한 가능성 이 있지만, Redux가 구현되는 방식에 본질적으로는 느리거나 비효율적인 것은 없다. 사실, React-Redux는 불필요한 재 렌더링을 줄이기 위해 많이 최적화되어 있으며 React-Redux v5는 이전 버전보다 눈에 띄는 개선 사항이 많다.

Redux는 다른 라이브러리와 비교할 때 효율적이지는 않다. React 애플리케이션의 렌더링 성능을 극대화하려면 상태를 정규화 된 모양으로 저장해야 하며, 많은 개별 컴포넌트들을 스토어에 연결해야 하고, 연결된 목록 컴포넌트는 item ID들을 하위 리스트 항목들에게 전달해야 한다.(자신만을 위한 데이터들을 ID를 통해서 찾을 수 있도록) 이렇게 하면 전체 렌더링 양이 최소화된다. 메모이즈드 셀렉터 함수를 사용하는 것도 중요한 성능 고려사항이다.

아키텍쳐에 관해서 일화적인 증거는 Redux가 다양한 프로젝트 및 팀 크기에 대해 잘 작동하고 있다는 것이다. Redux는 현재 NPM에서 수십만 개의 월간 설치로 수천 명의 기업과 수천 명의 개발자가 사용하고 있고, 한 개발자가 다음과 같이 보고했다.

규모면에서, 우리는 ~500개의 액션 타입과, 400개의 리듀서 케이스, ~150개의 컴포넌트, 5개의 미들웨어, ~200개의 액션, 2300개의 테스트를 가지고 있다.

더 읽어보기

Documentation

Articles

Discussions

각각의 액션마다 “내 모든 리듀서”를 호출하는 것은 느리지 않나요?

Redux 스토어는 실제로 하나의 리듀서 함수만을 가진다는 것에 유의하는 것이 중요하다. 스토어는 하나의 리듀서 함수에 현재 상태를 전달하고 액션을 디스패치하하고 리듀서는 이를 적절히 다루도록 하자.

명백히, 모든 가능한 액션을 하나의 함수에서 다루려고 시도하면 함수 크기와 가독성 측면에서 단순하게 확장되지 않으므로, 실제 작업을 최상위 레벨의 리듀서에서 호출할 수 있는 별도의 함수로 분할하는 것이 좋다. 특히, 일반적으로 특정 키를 가진 특정 상태 조각에 대한 업데이트를 관리하는 하위 리듀서 함수를 갖는 패턴을 제안하고 있다. Redux와 함께 제공되는 combineReducers()는 이를 달성할 수 있는 많은 방법 중 하나다. 또한 스토어 상태를 가능한 평평하게 정규화 하는 것이 좋다. 궁극적으로, 어떤 방식의 로직으로든 리듀서를 조직할 책임은 당신에게 있다.

그러나, 다른 리듀서 함수가 함께 조합되고 심하게 중첩된 상태를 가지고서도 리듀서의 속도는 문제가 되지 않는다. 자바스크립트 엔진은 초당 매우 많은 수의 함수 호출을 실행할 수 있으며 대부분의 리듀서는 switch 문을 사용하고 default에서 기존 state를 반환한다.

만약 정말로 reducer의 성능이 우려된다면 redux-ignorereduxr-scoped-reducer를 이용하여 특정 액션에 하나의 리듀서만 반응하도록 할 수 있다. 또한 redux-log-slow-reducers를 이용해 성능 벤치마킹을 할 수 있다.

더 읽어보기

Discussions

리듀서에서 내 상태를 deep-clone해야만 하나요? 상태를 복사하는 것으로 인해 느려지지는 않나요?

Immutabl하게 상태를 갱신하는 것은 일반적으로 deep 카피가 아니라 shallow 카피를 뜻한다. shallow 카피는 딥 카피보다 훨씬 빠르다, 왜나하면 적은 오브젝트와 필드만 카피하고, 포인터를 효과적으로 움직이게 된다.

그러나 영향을 줘야하는 레벨의 중첩까지는 각각 오브젝트를 생성하고 카피해야할 필요가 있다. 특히 비싸지 않아도 되지만, 이는 state를 가능한한 얕게 정규화된 채 유지해야할 또다른 이유다.

일반적인 Redux에 대한 오해: state를 deep copy해야 한다. 실제: 내부가 바뀌지 않으면 참조를 동일하게 유지해야 한다.

더 읽어보기

Documentation

Discussions

어떻게 해야 스토어를 업데이트하는 이벤트의 수를 줄일 수 있나요?

리덕스는 각 액션을 성공적으로 디스패치한 다음 구독자들에게 알린다(즉, 스토어에 도달한 액션이 리듀서에서 처리된 경우). 어떤 경우에는, 구독자가 호출된 횟수를 줄이는 것이 유용할 수 있다. 특히, 액션 생성자가 여러 개의 서로 다른 액션을 연속적으로 발송하는 경우에 유용하다.

React를 사용한다면 ReactDOM.unstable_batchedUpdates()에 여러개의 동기적인 디스패치를 감싸서 성능을 향상시킬 수 있지만 이 API는 실험적이고 React 릴리즈에서 제거될 수 있으니 많이 의존하지는 말아라. redux-batched-actions (여러 개의 액션을 하나의 액션으로 감싸고 리듀서에서 그것들을 “unpack”하는 고차 리듀서), redux-batched-subscribe (여러번 디스패치되는 호출을 디바운스하게 하는 스토어 인핸서), 혹은 redux-batch (여러 개의 액션들을 하나의 구독자 알림으로 다루는 스토어 인핸서)을 참고하라.

더 읽어보기

Discussions

Libraries

“하나의 상태 트리”가 메모리 문제를 일으키지 않나요? 많은 액션들을 디스패칭하는 것은 많은 메모리가 필요하지 않나요?

우선 메모리 사용 측면에서 Redux는 다른 자바스크립트 라이브러리와 다르지 않다. 유일한 차이점은 Backbone처럼 다양한 독립적인 모델 인스턴스를 저장하는 것이 아니라, 여러 오브젝트 레퍼런스가 중첩되어 하나의 트리를 이룬다는 것이다. 두번째로는 전형적인 Redux 앱은 Backbone 앱에 비해 적은 메모리를 사용할 것이라는 것이다. 왜냐하면 Redux는 모델이나 컬렉션의 인스턴스를 생성하기 보다는 plain 자바스크립트 오브젝트와 배열을 사용하는 것을 독려하기 때문이다. 마지막으로, Redux는 한 번에 하나의 상태 트리 레퍼런스만을 가지고 있다. 트리 내에서 더 이상 레퍼런스되지 않는 오브젝트들은 보통 가비지 콜렉션 대상이 된다.

Redux는 액션 자체에 대한 기록을 저장하지 않는다. Redux DevTools는 리플레이할 수 있도록 액션들을 저장하지만 일반적으로 Redux DevTools는 제품 모드가 아닌 개발 모드에서만 활성화한다.

더 읽어보기

Documentation

Discussions

원문보기

2개의 시리즈물인 React 성능 엔지니어링의 두 번째 파트다. 파트 1에서 우리는 React 성능 툴을 어떻게 쓰는지 살펴보았고, React의 일반적인 렌더링 병목현상과 몇 가지 디버깅 팁을 알아보았다. 아직 안봤다면 한 번 살펴보라!

파트 2에서는 디버깅 워크플로우에 깊게 들어가볼 것이다 - 이 모든 아이디어들을 어떻게 실제로 실행 할 수 있을까? 실생활에서 영향받은 몇 가지 예제와 Chrome의 devtools를 이용해서 성능을 진단하고 수정해볼 것이다. (다 읽은 뒤에 어떠한 제안이나 추가할만한 사항이 있다면 알려달라!)

우리는 다음 샘플 코드를 참조할 것이다 - 간단한 todo list를 React로 렌더링하는 것이다. JS fiddle 스니펫에서 “Result”를 클릭하면 성능 개선을 완료한 인터랙티브 버전을 볼 수 있다. 우리느 업데이트된 JS fiddle을 가지고 포스트할 것이다.

Case Study #1: TodoList

위의 TodoList를 가지고 시작해보자. 최적화되지 않은 코드에서 타이핑을 해보면 얼마나 느린지 알 수 있을 것이다.

Chrome dev tools을 실행시켜 브라우저가 하는 일에 대한 상세한 프로파일링을 해주는 Timeline profiler를 시작하자: 사용자 이벤트를 핸들링하고, JS를 실행하고, 렌더링하고 페인팅하는 것 등. input에 한 글자만 타이핑하고 timeline profiler를 멈추자. 아직까지는 느린 것을 확인할 수 있다. 한 글자만 입력했기 때문이다. 하지만 이게 프로파일링에 필요한 최소 정보량을 생성하는 가장 빠른 방법이다.

textInput의 긴 막대를 보면 121.10ms가 스크립팅(Children)에 들어간 것을 보자. timeline 프로파일러는 느린 이유가 스타일링이나/레이아웃 재계산때문이 아니라 스크립팅 때문임을 나타낸다.

그래서 scripting으로 내려가본다. Profiles 탭으로 가자 - 타임라인은 브라우저 전반에 걸쳐 프로파일링을 하지만 Profiles 탭은 JS측에 특화된 조금 다른 모습의 시각화 툴이다. 우리 앱이 아닌 다른 앱에 대한 프로파일을 기록한 것이다:

Heaby (Bottom Up) 부분을 보면, React의 batchedUpdates가 대부분의 시간을 차지했음을 알 수 있다. 이는 React 측에 문제가 있음을 확실히 보여주는 것이다. 반대로, function 내에서 Self 측정된 시간을 보면 child functions에서 시간은 제외되었다 - sort by Self는 어떤 특정 값비싼 functions들이 있는지 살펴볼 수 있다. 사용자측의 함수에는 특별한 병목이 없는 것 처럼 보인다. 그러므로 이제 React Perf 툴을 시도해보자.

느린 액션에 대한 측정값들을 생성하기 위해 콘솔에서 React.addons.Perf.start()를 호출한 다음 글자를 쳐서 느린 액션을 실행할 것이다. 그리고 나서 React.addons.Perf.stop()을 실행하여 측정을 마친다. 필요없는 시간 낭비가 있었는지 React.addons.Perf.printWasted()를 실행시켜서 볼 수 있다:

첫 번째 아이템은 Todos에 의해 렌더된 TodoItem을 나타낸다; 그러나 Pref.printWasted()는 렌더 트리를 리빌딩하지 않으면 100ms의 시간을 절약할 수 있다는 것을 알려준다. 최적화가 가장 필요한 후보처럼 보인다.

왜 TodoItem이 이렇게 많은 시간을 낭비하는지 진단하려면 WhyDidYouUpdateMixin이라는 커스텀 Minxin을 사용한다. 컴포넌트와 log를 후킹하여 update가 어디에서 왜 일어났는지 확인한다. 다음 코드를 보고 필요에 따라 적용하라:

/* eslint-disable no-console */
import _ from 'underscore';

/*
Drop this mixin into a component that wastes time according to Perf.getWastedTime() to find
out what state/props should be preserved. Once it says "Update avoidable!" for {state, props},
you should be able to drop in React.addons.PureRenderMixin
React.createClass {
  mixins: [WhyDidYouUpdateMixin]
}
*/
function isRequiredUpdateObject(o) {
  return Array.isArray(o) || (o && o.constructor === Object.prototype.constructor);
}

function deepDiff(o1, o2, p) {
  const notify = (status) => {
    console.warn('Update %s', status);
    console.log('%cbefore', 'font-weight: bold', o1);
    console.log('%cafter ', 'font-weight: bold', o2);
  };
  if (!_.isEqual(o1, o2)) {
    console.group(p);
    if ([o1, o2].every(_.isFunction)) {
      notify('avoidable?');
    } else if (![o1, o2].every(isRequiredUpdateObject)) {
      notify('required.');
    } else {
      const keys = _.union(_.keys(o1), _.keys(o2));
      for (const key of keys) {
        deepDiff(o1[key], o2[key], key);
      }
    }
    console.groupEnd();
  } else if (o1 !== o2) {
    console.group(p);
    notify('avoidable!');
    if (_.isObject(o1) && _.isObject(o2)) {
      const keys = _.union(_.keys(o1), _.keys(o2));
      for (const key of keys) {
        deepDiff(o1[key], o2[key], key);
      }
    }
    console.groupEnd();
  }
}

const WhyDidYouUpdateMixin = {
  componentDidUpdate(prevProps, prevState) {
    deepDiff({props: prevProps, state: prevState},
             {props: this.props, state: this.state},
             this.constructor.displayName);
  },
};

export default WhyDidYouUpdateMixin;

TodoItem에 이 믹스인을 추가하고, 어떻게 되는지 봤다:

아하! tags가 이전과 이후에 동일한 것을 알았다 - mixin이 말하기를 동일한 객체는 아니지만 내용은 같은 객체라고 한다. 또 한편, 두 함수가 동일하다는 것을 알아내기는 힘들다. Function.bind가 생성한 새로운 펑션이 같은 아규먼트로 바인딩되었기 때문이다. 이것들은 유용한 단서가 된다 - 우리기 앞서 어떻게 tags와 deleteItem을 전달했는지 보니, TodoItem이 생성될 때마다 새로운 값을 전달했던 것 처럼 보인다.

만약 바인드되지 않은 함수를 TodoItem 전달한다면, 그리고 tags를 상수로 저장한다면 이런 문제를 피할 수 있다:

WhyDidYouUpdateMixin은 prevProps와 new props가 shallow equal함을 보인다. PureRenderMixin을 사용하면 이런 상황에서 업데이트를 건너뛸 수 있따.

profiler를 다시 해보니 35ms 정도만 걸렸음을 알 수 있다(이전 속도의 4배): When we run the profiler again, we see that it only takes about 35ms (4x faster than before):

조금 나아졌지만 아직 이상적인 상황은 아니다. input에 타이핑하는 것은 오버헤드가 되어선 안된다. 우리는 여전히 O(목록에 있는 항목 수)로 동작하고 있다. 단순히 상수를 줄이기만 했기 때문에 각 항목에 대해 shallow compare 수행이 필요하다.

1000개의 항목을 추가하는 극단적인 상황을 가정했을때, 30ms가 적당한 속도라고 가정하자. 그러나 수천의 항목이 예상되는 경우에는 60fps(프레임당 16ms - 눈에 띄는 지연시간)가 이상적이다.

컴포넌트를 쪼개서 여러개로 나누는 것은 합리적인 다음 단계라고 볼 수 있다(유효한 첫 번째 단계이다). Todos 컴포넌트가 2개로 나눠질만한 서브컴포넌트로 이루어져 있음을 알 수 있다. AddTaskForm 컴포넌트는 input과 버튼을 포함하고, TodoItems 컴포넌트는 항목들에 대한 목록을 포함한다.

각각의 리팩토링은 지속적인 성능 향상을 기대할 수 있다:

  • 만약 PureRenderMixin을 이용해서 TodoItems를 생성한다면 각 아이템들에 대해 다시 랜더링을 하는 O(n) 작업을 피할 수 있다. prevProps.items === this.props.items와 같은 작업을 하기 때문이다.
  • 만약 AddTaskForm 컴포넌트를 생성하고 입력된 text에 대한 상태를 유지하자. 텍스트가 변경되어도 Todos 컴포넌트는 다시 랜더링되지 않는다.(O(n) 렌더링 작업을 피할 수 있다)

두 개를 합쳐서 키 입력당 10ms의 속도를 달성했다!

Case Study #2:

시나리오: 한 사용자가 너무 많은(3000개 이상) 태스크를 가지고 있다면 경고를 렌더링하기를 원하고, 또한 각 todo 항목이 각각 배경색을 가질 수 있도록 하고싶다.

구현:

  • todo list 예제의 TodoItems 구현과 비슷하게 구현한다 - 이 예제에서는 input의 text를 최상위 컴포넌트의 state로 저장한다.
  • 태스크의 개수에 따라 메시지를 렌더하는 TaskWarning 컴포넌트를 생성한다. 컴포넌트 내에 로직을 캡슐화하기 위해, 렌더링을 하지 않아야 할 때는 null을 리턴하도록 한다.
  • div:nth-child(even)에 대해 회색 배경색을 가지도록 CSS를 추가한다.

관찰: input에 빠르게 타아핑하면 page가 약간 렉이 걸린다(3000개 미만에서). 만약 이 상태에서 3000번째 항목을 추가하면 렉이 사라진다. 놀랍게도 더 많은 작업을 추가하니 문제가 해결된 것이다!

디버깅: timeline 프로파일이 뭔가 매우 흥미로운 것을 보여준다:

어떤 이유에서인지 한 글자를 타이핑하기만 해도 30ms나 잡아먹는 큰 규모의 스타일 재계산이 일어난다(이것이 30ms/글자수 이상의 속도로 타이핑할 때 렉이 나타나는 이유다, jank를 통해 관찰했다)

위 이미지의 아래쪽에서 나타난 First invalidated 섹션을 보라. Danger.dangerouslyReplaceNodeWithMarkup이 레이아웃 invalidation을 유발하는 것을 알 수 있다. 그리고 이것이 스타일 재계산으로 인도한다. react-with-addons.js:2301:를 보라

oldChild.parentNode.replaceChild(newChild, oldChild);

어떤 이유로 React는 DOM 노드를 완전히 새로운 DOM 노드로 갈아치운다! DOM 조작은 매우 값비싸다는 것을 상기시켜보자. Perf.printDOM()을 사용해서 React의 DOM 조작을 알아볼 수 있다:

어트리뷰트의 업데이트는 TaskWarning이 없을 때 input에 abc를 타이핑하는 것을 반영한 것이다. 그러나, 동일한 virtual DOM임 것처럼 보이는데도 React가 DOM을 터치하기로 결정하여 DOM 노드의 치환이 나타난다.

밝혀졌듯이, React(<=v0.13)는 noscript 태그를 사용하여 “no component”를 렌더링하지만 두 개의 noscript 태그를 같지 않은 것으로 잘못 처리한다. noscript는 불필요하게 다른 noscript로 교체된다. 또한 회색 배경의 모든 div 들을 생각해보라. CSS 때문에 3000 개의 항목 노드 중 하나의 렌더링은 그 앞의 형제 노드에 종속적이가 된다. noscript 태그를 바꿀 때마다 후속 DOM 노드의 스타일이 전부 다시 계산된다.

이 이슈를 해결하기 위해 다음과 같이 한다:

  • TaskWarning은 empty div를 리턴한다
  • TaskWarning 컴포넌트를 이동시켜서 div 내에서 해당 노드의 CSS selector에 영향을 미치지 않도록 한다.
  • React를 업그레이드 한다 :-)

하지만 이것은 요점에서 빗나갔다. 중요한 것은 timeline 프로파일러에서 직접 진단할수 있다는 것이다.

결론

React 성능 이슈가 dev tool에서 어떻게 나타나는지 보여주는데 유용했으면 좋겠다 - Timeline과 Profiles, React의 Perf 툴의 조합해서 먼 길을 떠나자.

몇천개의 항목과 임의적 컬러링이 있는 Todo list가 인위적으로 보일수도 있지만, 실제로 개발 도중 커다란 문서나 스프레드시트를 렌더링할 때 매우 비슷한 문제와 맞닥뜨린 적이 있다. 그리고 맞다, 우리 팀은 아직 성장하고 있고 복잡한 React 앱이 당신을 흥분시킨다면 연락바란다.