ݺߣ

ݺߣShare a Scribd company logo
React 애플리케이션 아키텍처
아무도 알려주지 않아서 혼자 삽질했다
손병대 ( miconblog@gmail.com )
왜 React 하나요?
왜 React 하나요?
저는 문제 해결에 집중할 수 있었습니다.
왜 React 하나요?
다른말로 표현하면 아키텍처에 집중할수있어서 좋았습니다.
1년동안 여행을 다녀왔어요.
왜죠?
코딩이 하고 싶었거든요.
그래서 여행하며 정보북을 만들었어요.
http://rlibro.com
첫 커밋은 2015년 12월 31일!
그때 그시절 그 코드가 지금은 참 가소롭다.
샘플은 곧 나의 아키텍처
하지만 현실의 문제는 늘 샘플을 벗어난다.
쉽게 무너지는 아키텍처
새로운 것들은 계속 나오는데 적용하지 않을수도 없고,…
스펙은 계속 바뀌고… OTL
문서를 꼼꼼히 읽자.
문서에 모든 답이 다 있다.
그런데,.. 문서가 또 너무 많아 -_-;
좋은 아키텍처는 

디자인 되야하고
플랜 되어져야 한다.
좋은 디자인을 위해 알아야할 것들
1년 6개월전에 알았더라면 좋았을 것들…
React는 빙산의 일각1
React는 빙산의 일각2
첫번재, 컴포넌트 역할 정의
뭣이 컴포넌트인가?
Lv1. 버튼
버튼을 컴포넌트로 만들봅시다.
기본 HTML 버튼은 그냥 좀 구려요.
아이콘도 없구요
그래서 버튼의 색상도 바꾸고
아이콘도 넣고 싶어요
버튼을 컴포넌트로 만들면 props 에는 아이콘과 스타일을 넣어볼수 있습니다.
<Button	icon=‘power’	style={…}>Click	me!</Button>
Lv2. 검색창
이번엔 검색창이에요.
이녀석의 props에 뭐가 있으면 좋을
까요?
<SerachBar	onSearch={	keyword	=>	{	console.log(keyword)	}	}	/>	
일단 엔터를 치면 검색어와 함께 콜백을 받는다
고 합시다.
초기값도 필요하지 않을까요?
				<SerachBar		
			initialValue={‘슬로베니아’}	
							onSearch={	keyword	=>	{	console.log(keyword)	}	}		
				/>	
컴포넌트 설계는 역할을 어떻게 정의하느냐가 전부에요!
Lv3. 자동완성
좀더 나가 봅시다.
<SerachBar		
			initialValue={‘슬로베니아’}	
			dataSource={[..]}	
							onSearch={	keyword	=>	{	console.log(keyword)	}	}		
				/>	
props 에 무엇을 넣을까요?
그런데 키워드로 검색된 결과는 어떻게 가져오죠?
SearchBar의 역할을 화면 랜더링 전용으로 한정한다면
데이터를 불러올 녀석이 필요합니다.
Lv4. 컨테이너 컴포넌트
중요한 규칙하나, 컨테이너는 데이터를 다룬다
<SerachBarContainer>	
			<SerachBar		
						initialValue={'여행'}	
						dataSource={[]}	
						onSearch={	keyword	=>	{	console.log(keyword)	}	}		
			/>	
</SerachBarContainer>	
그림으로 그려보면 대략 이런 모습입니다.
import	SerachBar	from	'./SerachBar';	
class	SerachBarContainer	extends	React.PureComponent		{	
	 state	=	{	
					 loading:	false,	
								results:	[]	
			}	
	 	
			render	()	{	
					 return	(	
										<SerachBar	dataSource={this.state.results}	onSearch={this.handleSearch}/>	
				);	
			}	
					
			handleSearch	=	(query)	=>	{	
					
								this.setState({loading:true})	
								fetch('/search')	
								.then((결과)=>{	
										this.setState({results:결과,	loading:false})	
								})	
					 	
			}	
}	
코드로 바꾸면 대충 이런 모습이겠죠?
그림으로 그려보면 대충 이렇습니다.
데이터를 가져오는 역할을 누구에게 줄것인가? 

그리고 그런 녀석을 뭐라고 부를(정의) 것인가?
두번째, 컴포넌트 분리하기
역할을 어떤 기준으로 나누고 분배할 것인가?
사용자 이벤트 처리는 누가 하지?
화면을 기준으로 컴포넌트를 나눌때 나타나는 현상
click!
컨테이너가 이벤트를 담당할경우 해당 컨테이너까지 

이벤트를 끌어올려야한다. … 귀찮아!
) (
) (
.(.
.(.
. .(
.(.
적당히 나누는데도 한계가 있다!
) (
) (
.(.
.(.
. .(
.(.
) (
.(.
.(.
) (
라우터를 이용한 컴포넌트 분리
중첩 라우터를 이용해
집중할 관심사를 완전히 분리한다.
:
( )(
./ ( )(
./
(/
( )(
./ ( )(
( )(
./ ( )(
/
/ (
라우터를 이용한 컴포넌트 분리
라우터가 로드할 페이지를 결정할때 중첩
된 부모 페이지가 있으면 같이 로드된다.
C
( )(
./ ( )(
./
(/
( )(
./ ( )(
( )(
./ ( )(
:
/ (
부모는 자식이 필요로 하는
값이 없으면 아예 랜더링을 하지 않는다.
즉, 선택적으로 Props를 주입할수있다.
라우터를 이용한 컴포넌트 분리
1. 중첩된 부모 라우터에서 필요한 값을 한번만 로드한다.
2. 자식 라우터에는
로드된 값을 선별해서
값이 있을때만 주입하고 랜더링한다.
3. 이렇게 하면 자식 라우터는 필요한 값을 항상
주입 받은 상태로 넘어오기 때문에 기다릴 필요
가 없게된다.
C
( )(
./ ( )(
./
(/
( )(
./ ( )(
( )(
./ ( )(
:
/ (
사용자 이벤트 처리는 누가 하지?
Redux를 이용한 컴포넌트 분리
click! 컨테이너까지 이벤트를 올려주세요!
스토어를 연결한 컨테이너를 중간중간 만든다.
이벤트 처리는 여기서 하기로 하자!
) ( ).
) ( ).
) ( ).
) ( ).
) ( ).
) ( ).
사용자 클릭 이벤트
Redux를 이용한 컴포넌트 분리
click!
이벤트 처리는 역시 dispatch 액션!!
. .
. .
. .
. .
. .
. .
)
. (
그럼에도 불구하고 콜백을 올리는건
여전히 귀찮다…
Redux를 이용한 컴포넌트 분리
콜백이 귀찮을땐 dispatch를 내려준다.
. .
. .
.
.
)
. (
Redux를 이용한 컴포넌트 분리
너무 깊어 dispatch도 내려주기 귀찮을땐
context 매직을 사용한다.
export	default	class	BookNoteList	extends	React.PureComponent	{	
		static	contextTypes	=	{	
				store:	React.PropTypes.object	
		};	
			
		render()	{	
		return(	
		<div>	
		<Button		
					onClick={	
						()=>	this.context.store.dispatch(액션)		
					}	
		>	컨텍스트 쓰지말래도 난 쓸꺼야! </Button>	
		</div>	
		)	
		}	
}	
Provider를 이용해 주입한 store 컨텍스트를 사용할수있다.
. .
. .
)
. (
Redux와 컴포넌트를 연결하는 방법
connect(mapStateToProps,	mapDispatchToProps)(ContainerComponent)	
connect(mapStateToProps)(ContainerComponent)	
dispatch 내리면 코드가 간결해진다.
단점, redux에 의존성이 생긴다.
어짜피 재사용하기 힘들다면 GoGo!
(
(
)
)
dispatch와 callback을 혼용하는 경우
Redux 아키텍처 상에서 Ajax 호출을 관리하지 않는 경우
지오코딩은 구글맵 SDK를 이용해 호출한다.
A
A
)(
React는 빙산의 일각 리마인드
브라우저(글로벌) 영역
애플리케이션 영역
리덕스 영역
Middleware Control Flow
. E A A E A I - E /
l pSh
/
E E
EC
. )/ )/
pSh MS
( A A
( C A
EC AA
E
E E
/) .
iMS
E
sRf T rk
Pao f
E
Lj
. E A
( A A
C CE
gc
E C CE
/
e
사용자
A
(
Pao
/
e / OP V
EC A
A A
,
Lj
pSh
E EE A
mn
d
bU
A
Lj
EC A
이제 겨우 요거 했다!
재사용 가능한 컴포넌트란?
2. 프로젝트 내에서 컨테이너도 재사용하고 싶다면.
1. 프로젝트 내에서 뷰를 재사용하고 싶다면
3. 다른 프로젝트에서도 쓸수있게 만든다.
컴포넌트에서 데이터 로드에 대한 역할을 제거해라!
기능을 효율적으로 묶고, 아키텍처를 최대한 활용해라!
데이터 로드 및 아키텍처에 대한 의존도를 낮추고
필요하다면 props로 주입 받아라
세번째, Redux 아키텍처
아키텍처를 이해하면 개발도 쉬워진다.
Lv1. 리듀서
리듀서 항상 사이드 이펙트가 없는 순수 함수로 작성 되야 한다.
순수함수란? (Pure Function)
인풋이 같으면 항상 같은 값을 반환한다.
그러니까 주어진 값으로 계산만 해라!
쉬운말로 딴짓 하지마!
그래서 리듀서의 역할은 매우 단순하다
,
O NT
R : !
I A C
단순하지만 굉장히 어려운 이뮤터빌리티!
리듀서의 상태 변경 로직은
단순한 삼중등호를 이용한다.
즉, 상태 반환시
항상 새로운 상태 레퍼랜스를 반환해야한다.
기존 상태에 값만 변경하는 행위는
객체의 참조를 변경시키지 않는다
const	initialState	=	{	x:1,	y:	2	};	
function	reducer(state=initialState,	action)	{	
state.x	=	3;	
state.y	=	action.payload;	
return	state;	
}	
:& A
3 : : &
,
단순하기 때문에 개발자의 책임도 많아진다.
변경을 알리는 로직은 어떤 값이 변경 됐는지
구분하지 않고 단순히 상태가 변경 됐을 때
모두 구독자에게 알려준다.
Note that selectorFactory is responsible for all caching/memoization of
inbound and outbound props. Do not use connectAdvanced directly
without memoizing results between calls to your selector, otherwise the
Connect component will re-render on every state or props change.
function	mapStateToProps(	state:	Object	)	{	
	 // 이곳에서 캐싱/메모이제이션을 처리해서 넘겨야한다.	
		const	testState	=	state.get(‘test');		
		const	testValue	=	reselectValue(testState);	
		// 결국 이곳에서 반환되는 값들은 React의 shouldComponentUpdate 사이클을 타게 된다.	
		return	{	
				test:	testValue,		
		};	
}	
export	default	connect(mapStateToProps)(ReactComponent);	
&
Redux 코드를 까보면 나오는 주석!
아! -__-;;;; 얘들은 이런 중요한 얘기를 문서가
아닌 코드에 심어 놓는구나…
브라우저(글로벌) 영역
애플리케이션 영역
리덕스 영역
Middleware Control Flow
. E A A E A I - E /
l pSh
/
E E
EC
. )/ )/
pSh MS
( A A
( C A
EC AA
E
E E
/) .
iMS
E
sRf T rk
Pao f
E
Lj
. E A
( A A
C CE
gc
E C CE
/
e
사용자
A
(
Pao
/
e / OP V
EC A
A A
,
Lj
pSh
E EE A
mn
d
bU
A
Lj
EC A
Redux 아키텍처의 이해, Reducer 요기!
Lv2. 액션 생성자
액션 생성자는 항상 순수한 액션을 반환해야한다.
redux-thunk 가 불러오는 오해
thunk 액션은 순수 오프젝트가 아니라 함수다!
액션 생성자는 본래 plain object 를 반환하는 녀석이다.
'
: 1
: ) ( ( : 1
function	createThunkMiddleware(extraArgument)	{	
		return	({	dispatch,	getState	})	=>	next	=>	action	=>	{	
				if	(typeof	action	===	'function')	{	
						return	action(dispatch,	getState,	extraArgument);	
				}	
				return	next(action);	
		};	
}	
const	thunk	=	createThunkMiddleware();	
thunk.withExtraArgument	=	createThunkMiddleware;	
export	default	thunk;	
thunk 미들웨어는 몇줄 안돼…

진짜 단순하다.
그러나 thunk 액션 생성자는 액션도 반환할수있다.
즉, 누구나 알고 있는 기본 아키텍처의 변경을 가져온다.
'
: 1
: ) ( ( : 1
액션 생성자를 오해하게 만드는 예제
export	function	requestUserLocation(){	
		return	(dispatch,	getState)	=>	{	
				return	dispatch({	
						type:	'USER_LOCATION_REQUEST'	
				});	
		}	
}	
redux-thunk 를 이용한 샘플을 따라하면서 잘못 사용하고 있다.
테스트하기 어렵다.
dispatch를 직접 실행하면서 본래 목적과 혼용되기 쉽다.
코드만 보면 dispatch가 갑자기 어디선가 주입된다.
왜!
안좋아?
액션 생성자를 잘못 쓴 예제
export	function	addComment	(noteId,	noteAuthorId,	content)	{	
		const	currentUser	=	Parse.User.current();	
		return	(dispatch,	getState)	=>	{	
				dispatch({	type:	types.ADD_COMMENT_REQUEST	});	
				Parse.Cloud	
						.run('addComment',	{	
								noteId:	noteId,	
								fromUserId:	currentUser.id,	
								toUserId:	noteAuthorId,	
								content:	content	
						})	
						.then(	note	=>	{	
								dispatch({	
										type:	types.ADD_COMMENT_SUCCESS,	
										response:	Object.assign({},	normalize(note.toJSON(),	noteSchema)	),	
										meta:	{	
												request:	{	
														objectName:'DiaryNote'	
												}	
										}	
								})	
						});	
		}	
}	
dispatch 실행한다.
비동기 호출을 하고 있다.
역시나 테스트하기 어렵다
왜!
안좋아?
액션 생성자를 사용하는 올바른 예!
export	function	addComment(	noteId,	values	)	{	
		if(	!values	)	{	return	{	type:	types.NO_VALUE	}	}	
		values.author	=	Parse.User.current();	
		values.referrerId	=	noteId;	
		return	{	
				type			:	types.PARSE_SERVER,	
				payload:	{	
						objectName:	'Comment',	
						method				:	'POST',	
						params				:	values,	
				},	
				meta			:	{	
						prefix:	'COMMENT_ADD',	
						noteId,	
						schema:	Schemas.COMMENT,	
				},	
		};	
}	
왜!
좋아?
테스트하기 쉽다.
단순하다.
브라우저(글로벌) 영역
애플리케이션 영역
리덕스 영역
Middleware Control Flow
. E A A E A I - E /
l pSh
/
E E
EC
. )/ )/
pSh MS
( A A
( C A
EC AA
E
E E
/) .
iMS
E
sRf T rk
Pao f
E
Lj
. E A
( A A
C CE
gc
E C CE
/
e
사용자
A
(
Pao
/
e / OP V
EC A
A A
,
Lj
pSh
E EE A
mn
d
bU
A
Lj
EC A
Redux 아키텍처의 이해, 액션 생성자
요기!
Lv3. Redux 미들웨어
Redux 아키텍처에서 비동기는 미들웨어가 처리한다.
미들웨어는 순차적으로 실행이 되므로 상황에 따라서 마지막 미들웨어는 실행되지 않을수도 있다.
		compose(	
				//applyMiddleware(require('redux-immutable-state-invariant')()),	
				applyMiddleware(thunk,	parse),	
				applyMiddleware(sagaMiddleware),	
				//applyMiddleware(createLogger()),	
				applyMiddleware(redirectMiddleware),	
				window.devToolsExtension	?	window.devToolsExtension()	:	f	=>	f,	
		),	
여기서 정의된 순서로 실행된다.
Middleware
A / ) / / / / (A
/
/ /
/
(/ A ) /
미들웨어
Thunk
미들웨어
Saga
미들웨어
Logger
미들웨어
Custom..
액션을 처음으로 다시 되돌릴수도 있다.
액션은 중간에 사라지기도 한다.
미들웨어를 통과한 마지막 액션은
항상 순수한 액션 객체여야 한다.
다음 미들웨어에 액션을 넘기면서 진행된다.
새로운 액션을 만들수도 있다.
미들웨어 안에서 변형되는 다양한 액션들
import	{	browserHistory	}	from	'react-router';	
export	default	store	=>	next	=>	action	=>	{	
		if	(	action.redirect	)	{	
				setTimeout(()	=>	{	
						browserHistory.replace(action.redirect.pathname);	
				},	0);	
		}	
		next(action);	
}	
action.redirect 값이 있으면 

URL을 변경하는 간단한 미들웨어
미들웨어
Thunk
미들웨어
Saga
미들웨어
Custom) . ) .
. ) (
미들웨어 안에서 다양하게 응용되는 상황들
N P I
.
미들웨어
Thunk
미들웨어
Saga
미들웨어
Custom. .
. (
)
A ) R
P I
.
A P I R a R
P RS
RTOC
브라우저(글로벌) 영역
애플리케이션 영역
리덕스 영역
Middleware Control Flow
. E A A E A I - E /
l pSh
/
E E
EC
. )/ )/
pSh MS
( A A
( C A
EC AA
E
E E
/) .
iMS
E
sRf T rk
Pao f
E
Lj
. E A
( A A
C CE
gc
E C CE
/
e
사용자
A
(
Pao
/
e / OP V
EC A
A A
,
Lj
pSh
E EE A
mn
d
bU
A
Lj
EC A
Redux 아키텍처의 이해, 미들웨어
요~오기!
네번째, 비동기 제어
비동기를 제어하는 자가 프로그램을 지배한다.
redux-saga 를 알아보자!
saga는 테스크 단위로 관리한다.
비동기 상황을 제너레이터의 코루틴를 이용해 동기식 작업으로 전환시켜준다.
이건 앞에서 설명했던 그림입니다.
N P I
.
미들웨어
Thunk
미들웨어
Saga
미들웨어
Custom. .
. (
)
A ) R
P I
.
A P I R a R
P RS
RTOC
Saga는 앞에서 설명했던 그림과 유사한 일을 합니다.
N P I
.
Saga
Task
Saga
Task
Saga
Task. .
. (
)
A ) R
P I
.
A P I R a R
P RS
RTOC
Saga 미들웨어 하나로 모든 상황을 제어할수있다.
단, 이 모든 관리는 Generator 코루틴에 대한 이해를 필요로 한다.
정보북을 생성하는 Task를 봅시다.
import	{	put,	call,	select,	take,	fork	}	from	'redux-saga/effects';	
			import	*	as	types	from	‘../actionType/redBookType';	
			... 중략 ...			
function*	postRedBook(	uname,	cityName,	countryName,	coverImage,	geoPoint,	authorId	)	{}	
function*	createRedBookTask()	{	
		while	(	true	)	{	
				const	action	=	yield	take(types.CREATE_REDBOOK);	
				const	{	uname,	cityName,	countryName,	coverImage,	geoPoint,	authorId	}	=	action.payload;	
				const	response	=	yield	postRedBook(uname,	cityName,	countryName,	coverImage,	geoPoint,	authorId);	
				if	(	response.success	)	{	
						yield	put({	type:	types.CREATE_REDBOOK_SUCCESS,	payload:	{	uname,	book:	response.success	}	});	
						yield	put({	type:	types.CLOSE_BOOK	});	
						yield	put({	type:	'SYS_CREATE_DIARYNOTE_BY_REDBOOK',	payload:	{	book:	response.success,	authorId	}	});	
				}	
		}	
}	
export	default	function*()	{	
		return	[	
				yield	fork(loadRedBooksTask),		
				... 중략 ...			
				yield	fork(createRedBookTask),	
		];	
}	
코드를 읽어보세요.
생각해 봅시다.
Redux와 Router 연동 전략
글쓰기 페이지에서 글을 다 쓰면 목록 페이지로 되돌리고 싶다.
글이 제대로 저장되면 이전 화면으로 돌아가고 싶다.
/guide/Flores,Guatemala/notes/create/guide/Flores,Guatemala
컴포넌트 중심의 사고라면 콜백이 참 쉽다.
render()	{	
		.. 코드 중략 ..	
return	(	
<div>	
			.. 코드 중략 ..	
			<Button	onClick={this.handleSubmit}>작성</Button>	
</div>	
)	
}	
handleSubmit	=	(	)	=>	{	
			this.setState(	{loading:	true}	);	
			savePost(	this.state.content,	{		
					success:	()	=>	{	
browserHistory.goBack();	
					},	
					error:	()	=>	{	
this.setState(	{	loading:	false	}	);	
					}	
			});		
}	
하지만 리덕스 아키텍처에서는 비동기 호출은 미들웨어에게 맞긴다.
리덕스 아키텍처에서는 액션을 호출한다.
render()	{	
		.. 앞에서와 동일 코드라 생략 ..	
}	
handleSubmit	=	(	)	=>	{	
			const	{	content	}	=	this.state;	
			this.props.dispatch(	{	type:	‘SAVE_NOTE’	,	payload:	{	content	}	}	);		
}	
그럼 성공했을때 URL은 어디서 바꾸지?
(
)
생각해볼수 있는 전략 4가지
1. 라우터와 리덕스를 동기화 시킨다.
- SAVE_NOTE_SUCCESS 액션을 라우팅 리듀서가 받아서 상태를 바꾸면 알아서 라우더가 변경된다
- 라우팅 리듀서에 매번 액션을 정의 하기가 좀 거시기해…
2. 라우팅 리듀서가 모든 메시지를 모니터하고 있다가 

액션에 redirect payload가 있다면 URL을 변경한다.
- 하지만 미들웨어가 모든 메시지를 모니터하는게 좀 걸려..
3. 특정 비동기 액션이 끝나면 명시적으로 라우팅 액션을 실행한다.
- Saga 라면 이 부분을 좀더 쉽게 할수있겠지
- 미들웨어에서 비동기를 처리하더라도 콜백을 받을수 있기 때문에 쉽게 라우팅 액션 처리가 가능하다.
4. 2번과 3번을 적절히 조합.
- 공통 액션 처리에 대한 공통 라우팅 전략으로 적합하다.
- { type: ‘SAVE_NOTE’, payload: { content }, redirect: { pathname: ‘/guide’ } }
그밖에, 아키텍처에 영향을 주는 결정
스펙 변경
모듈 업데이트 ( webpack, react-router v3 to v4 )
배포 환경의 변화
immutablejs 도입
Flow type checker / typescript 도입
ServerSide Rendering
클라우드 API / GraphQL 도입
Rx / MobiX 등 근간을 이루는 아키텍처 패러다임의 변화
이상 끝!
질문은 메일로… 오프에선 맥주로…
여행기는 페북으로… 여행정보는 알리브로에…
miconblog@gmail.com		
facebook.com/wearemooving	
rlibro.com

More Related Content

React 애플리케이션 아키텍처 - 아무도 알려주지 않아서 혼자서 삽질했다.