Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

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

7,289 views

Published on

리액트를 하다보면 누구나 비슷한 경험을 하게 됩니다.
오래 할수록 더 많은 경험을 쌓게 되고 더 좋은 해결책들을 찾게 되죠.
하지만 혼자서 이 모든 것을을 이해하기엔 시간이 참 오래 걸립니다.
모쪼록 이 슬라이드가 시간의 간극을 메꿔줬으면 좋겠네요.
열공하세요.

Published in: Technology
  • Be the first to comment

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

  1. 1. React 애플리케이션 아키텍처 아무도 알려주지 않아서 혼자 삽질했다 손병대 ( miconblog@gmail.com )
  2. 2. 왜 React 하나요?
  3. 3. 왜 React 하나요? 저는 문제 해결에 집중할 수 있었습니다.
  4. 4. 왜 React 하나요? 다른말로 표현하면 아키텍처에 집중할수있어서 좋았습니다.
  5. 5. 1년동안 여행을 다녀왔어요. 왜죠?
  6. 6. 코딩이 하고 싶었거든요.
  7. 7. 그래서 여행하며 정보북을 만들었어요. http://rlibro.com
  8. 8. 첫 커밋은 2015년 12월 31일!
  9. 9. 그때 그시절 그 코드가 지금은 참 가소롭다.
  10. 10. 샘플은 곧 나의 아키텍처 하지만 현실의 문제는 늘 샘플을 벗어난다.
  11. 11. 쉽게 무너지는 아키텍처 새로운 것들은 계속 나오는데 적용하지 않을수도 없고,… 스펙은 계속 바뀌고… OTL
  12. 12. 문서를 꼼꼼히 읽자. 문서에 모든 답이 다 있다. 그런데,.. 문서가 또 너무 많아 -_-;
  13. 13. 좋은 아키텍처는 
 디자인 되야하고 플랜 되어져야 한다.
  14. 14. 좋은 디자인을 위해 알아야할 것들 1년 6개월전에 알았더라면 좋았을 것들…
  15. 15. React는 빙산의 일각1
  16. 16. React는 빙산의 일각2
  17. 17. 첫번재, 컴포넌트 역할 정의 뭣이 컴포넌트인가?
  18. 18. Lv1. 버튼 버튼을 컴포넌트로 만들어봅시다.
  19. 19. 기본 HTML 버튼은 그냥 좀 구려요. 아이콘도 없구요 그래서 버튼의 색상도 바꾸고 아이콘도 넣고 싶어요 버튼을 컴포넌트로 만들면 props 에는 아이콘과 스타일을 넣어볼수 있습니다. <Button icon=‘power’ style={…}>Click me!</Button>
  20. 20. Lv2. 검색창 이번엔 검색창이에요.
  21. 21. 이녀석의 props에 뭐가 있으면 좋을 까요? <SerachBar onSearch={ keyword => { console.log(keyword) } } /> 일단 엔터를 치면 검색어와 함께 콜백을 받는다 고 합시다.
  22. 22. 초기값도 필요하지 않을까요? <SerachBar initialValue={‘슬로베니아’} onSearch={ keyword => { console.log(keyword) } } /> 컴포넌트 설계는 역할을 어떻게 정의하느냐가 전부에요!
  23. 23. Lv3. 자동완성 좀더 나가 봅시다.
  24. 24. <SerachBar initialValue={‘슬로베니아’} dataSource={[..]} onSearch={ keyword => { console.log(keyword) } } /> props 에 무엇을 넣을까요?
  25. 25. 그런데 키워드로 검색된 결과는 어떻게 가져오죠? SearchBar의 역할을 화면 랜더링 전용으로 한정한다면 데이터를 불러올 녀석이 필요합니다.
  26. 26. Lv4. 컨테이너 컴포넌트 중요한 규칙하나, 컨테이너는 데이터를 다룬다
  27. 27. <SerachBarContainer> <SerachBar initialValue={'여행'} dataSource={[]} onSearch={ keyword => { console.log(keyword) } } /> </SerachBarContainer> 그림으로 그려보면 대략 이런 모습입니다.
  28. 28. 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}) }) } } 코드로 바꾸면 대충 이런 모습이겠죠?
  29. 29. 그림으로 그려보면 대충 이렇습니다. 데이터를 가져오는 역할을 누구에게 줄것인가? 
 그리고 그런 녀석을 뭐라고 부를(정의) 것인가?
  30. 30. 두번째, 컴포넌트 분리하기 역할을 어떤 기준으로 나누고 분배할 것인가?
  31. 31. 사용자 이벤트 처리는 누가 하지? 화면을 기준으로 컴포넌트를 나눌때 나타나는 현상 click! 컨테이너가 이벤트를 담당할경우 해당 컨테이너까지 
 이벤트를 끌어올려야한다. … 귀찮아! ) ( ) ( .(. .(. . .( .(.
  32. 32. 적당히 나누는데도 한계가 있다! ) ( ) ( .(. .(. . .( .(. ) ( .(. .(. ) (
  33. 33. 라우터를 이용한 컴포넌트 분리 중첩 라우터를 이용해 집중할 관심사를 완전히 분리한다. : ( )( ./ ( )( ./ (/ ( )( ./ ( )( ( )( ./ ( )( / / (
  34. 34. 라우터를 이용한 컴포넌트 분리 라우터가 로드할 페이지를 결정할때 중첩 된 부모 페이지가 있으면 같이 로드된다. C ( )( ./ ( )( ./ (/ ( )( ./ ( )( ( )( ./ ( )( : / ( 부모는 자식이 필요로 하는 값이 없으면 아예 랜더링을 하지 않는다. 즉, 선택적으로 Props를 주입할수있다.
  35. 35. 라우터를 이용한 컴포넌트 분리 1. 중첩된 부모 라우터에서 필요한 값을 한번만 로드한다. 2. 자식 라우터에는 로드된 값을 선별해서 값이 있을때만 주입하고 랜더링한다. 3. 이렇게 하면 자식 라우터는 필요한 값을 항상 주입 받은 상태로 넘어오기 때문에 기다릴 필요 가 없게된다. C ( )( ./ ( )( ./ (/ ( )( ./ ( )( ( )( ./ ( )( : / (
  36. 36. 사용자 이벤트 처리는 누가 하지? Redux를 이용한 컴포넌트 분리 click! 컨테이너까지 이벤트를 올려주세요! 스토어를 연결한 컨테이너를 중간중간 만든다. 이벤트 처리는 여기서 하기로 하자! ) ( ). ) ( ). ) ( ). ) ( ). ) ( ). ) ( ).
  37. 37. 사용자 클릭 이벤트 Redux를 이용한 컴포넌트 분리 click! 이벤트 처리는 역시 dispatch 액션!! . . . . . . . . . . . . ) . ( 그럼에도 불구하고 콜백을 올리는건 여전히 귀찮다…
  38. 38. Redux를 이용한 컴포넌트 분리 콜백이 귀찮을땐 dispatch를 내려준다. . . . . . . ) . (
  39. 39. 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 컨텍스트를 사용할수있다. . . . . ) . (
  40. 40. Redux와 컴포넌트를 연결하는 방법 connect(mapStateToProps, mapDispatchToProps)(ContainerComponent) connect(mapStateToProps)(ContainerComponent) dispatch 내리면 코드가 간결해진다. 단점, redux에 의존성이 생긴다. 어짜피 재사용하기 힘들다면 GoGo! ( ( ) )
  41. 41. dispatch와 callback을 혼용하는 경우 Redux 아키텍처 상에서 Ajax 호출을 관리하지 않는 경우 지오코딩은 구글맵 SDK를 이용해 호출한다. A A )(
  42. 42. 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 이제 겨우 요거 했다!
  43. 43. 재사용 가능한 컴포넌트란? 2. 프로젝트 내에서 컨테이너도 재사용하고 싶다면. 1. 프로젝트 내에서 뷰를 재사용하고 싶다면 3. 다른 프로젝트에서도 쓸수있게 만든다. 컴포넌트에서 데이터 로드에 대한 역할을 제거해라! 기능을 효율적으로 묶고, 아키텍처를 최대한 활용해라! 데이터 로드 및 아키텍처에 대한 의존도를 낮추고 필요하다면 props로 주입 받아라
  44. 44. 세번째, Redux 아키텍처 아키텍처를 이해하면 개발도 쉬워진다.
  45. 45. Lv1. 리듀서 리듀서 항상 사이드 이펙트가 없는 순수 함수로 작성 되야 한다.
  46. 46. 순수함수란? (Pure Function) 인풋이 같으면 항상 같은 값을 반환한다. 그러니까 주어진 값으로 계산만 해라! 쉬운말로 딴짓 하지마! 그래서 리듀서의 역할은 매우 단순하다 , O NT R : ! I A C
  47. 47. 단순하지만 굉장히 어려운 이뮤터빌리티! 리듀서의 상태 변경 로직은 단순한 삼중등호를 이용한다. 즉, 상태 반환시 항상 새로운 상태 레퍼랜스를 반환해야한다. 기존 상태에 값만 변경하는 행위는 객체의 참조를 변경시키지 않는다 const initialState = { x:1, y: 2 }; function reducer(state=initialState, action) { state.x = 3; state.y = action.payload; return state; } :& A 3 : : & ,
  48. 48. 단순하기 때문에 개발자의 책임도 많아진다. 변경을 알리는 로직은 어떤 값이 변경 됐는지 구분하지 않고 단순히 상태가 변경 됐을 때 모두 구독자에게 알려준다. 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 코드를 까보면 나오는 주석! 아! -__-;;;; 얘들은 이런 중요한 얘기를 문서가 아닌 코드에 심어 놓는구나…
  49. 49. 브라우저(글로벌) 영역 애플리케이션 영역 리덕스 영역 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 요기!
  50. 50. Lv2. 액션 생성자 액션 생성자는 항상 순수한 액션을 반환해야한다.
  51. 51. redux-thunk 가 불러오는 오해 thunk 액션은 순수 오프젝트가 아니라 함수다!
  52. 52. 액션 생성자는 본래 plain object 를 반환하는 녀석이다. ' : 1 : ) ( ( : 1
  53. 53. 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
  54. 54. 액션 생성자를 오해하게 만드는 예제 export function requestUserLocation(){ return (dispatch, getState) => { return dispatch({ type: 'USER_LOCATION_REQUEST' }); } } redux-thunk 를 이용한 샘플을 따라하면서 잘못 사용하고 있다. 테스트하기 어렵다. dispatch를 직접 실행하면서 본래 목적과 혼용되기 쉽다. 코드만 보면 dispatch가 갑자기 어디선가 주입된다. 왜! 안좋아?
  55. 55. 액션 생성자를 잘못 쓴 예제 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 실행한다. 비동기 호출을 하고 있다. 역시나 테스트하기 어렵다 왜! 안좋아?
  56. 56. 액션 생성자를 사용하는 올바른 예! 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, }, }; } 왜! 좋아? 테스트하기 쉽다. 단순하다.
  57. 57. 브라우저(글로벌) 영역 애플리케이션 영역 리덕스 영역 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 아키텍처의 이해, 액션 생성자 요기!
  58. 58. Lv3. Redux 미들웨어 Redux 아키텍처에서 비동기는 미들웨어가 처리한다.
  59. 59. 미들웨어는 순차적으로 실행이 되므로 상황에 따라서 마지막 미들웨어는 실행되지 않을수도 있다. 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..
  60. 60. 액션을 처음으로 다시 되돌릴수도 있다. 액션은 중간에 사라지기도 한다. 미들웨어를 통과한 마지막 액션은 항상 순수한 액션 객체여야 한다. 다음 미들웨어에 액션을 넘기면서 진행된다. 새로운 액션을 만들수도 있다. 미들웨어 안에서 변형되는 다양한 액션들 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) . ) . . ) (
  61. 61. 미들웨어 안에서 다양하게 응용되는 상황들 N P I . 미들웨어 Thunk 미들웨어 Saga 미들웨어 Custom. . . ( ) A ) R P I . A P I R a R P RS RTOC
  62. 62. 브라우저(글로벌) 영역 애플리케이션 영역 리덕스 영역 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 아키텍처의 이해, 미들웨어 요~오기!
  63. 63. 네번째, 비동기 제어 비동기를 제어하는 자가 프로그램을 지배한다.
  64. 64. redux-saga 를 알아보자! saga는 테스크 단위로 관리한다. 비동기 상황을 제너레이터의 코루틴를 이용해 동기식 작업으로 전환시켜준다.
  65. 65. 이건 앞에서 설명했던 그림입니다. N P I . 미들웨어 Thunk 미들웨어 Saga 미들웨어 Custom. . . ( ) A ) R P I . A P I R a R P RS RTOC
  66. 66. Saga는 앞에서 설명했던 그림과 유사한 일을 합니다. N P I . Saga Task Saga Task Saga Task. . . ( ) A ) R P I . A P I R a R P RS RTOC
  67. 67. Saga 미들웨어 하나로 모든 상황을 제어할수있다. 단, 이 모든 관리는 Generator 코루틴에 대한 이해를 필요로 한다.
  68. 68. 정보북을 생성하는 Task를 봅시다.
  69. 69. 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), ]; } 코드를 읽어보세요.
  70. 70. 생각해 봅시다. Redux와 Router 연동 전략 글쓰기 페이지에서 글을 다 쓰면 목록 페이지로 되돌리고 싶다.
  71. 71. 글이 제대로 저장되면 이전 화면으로 돌아가고 싶다. /guide/Flores,Guatemala/notes/create/guide/Flores,Guatemala
  72. 72. 컴포넌트 중심의 사고라면 콜백이 참 쉽다. 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 } ); } }); } 하지만 리덕스 아키텍처에서는 비동기 호출은 미들웨어에게 맞긴다.
  73. 73. 리덕스 아키텍처에서는 액션을 호출한다. render() { .. 앞에서와 동일 코드라 생략 .. } handleSubmit = ( ) => { const { content } = this.state; this.props.dispatch( { type: ‘SAVE_NOTE’ , payload: { content } } ); } 그럼 성공했을때 URL은 어디서 바꾸지? ( )
  74. 74. 생각해볼수 있는 전략 4가지 1. 라우터와 리덕스를 동기화 시킨다. - SAVE_NOTE_SUCCESS 액션을 라우팅 리듀서가 받아서 상태를 바꾸면 알아서 라우더가 변경된다 - 라우팅 리듀서에 매번 액션을 정의 하기가 좀 거시기해… 2. 라우팅 리듀서가 모든 메시지를 모니터하고 있다가 
 액션에 redirect payload가 있다면 URL을 변경한다. - 하지만 미들웨어가 모든 메시지를 모니터하는게 좀 걸려.. 3. 특정 비동기 액션이 끝나면 명시적으로 라우팅 액션을 실행한다. - Saga 라면 이 부분을 좀더 쉽게 할수있겠지 - 미들웨어에서 비동기를 처리하더라도 콜백을 받을수 있기 때문에 쉽게 라우팅 액션 처리가 가능하다. 4. 2번과 3번을 적절히 조합. - 공통 액션 처리에 대한 공통 라우팅 전략으로 적합하다. - { type: ‘SAVE_NOTE’, payload: { content }, redirect: { pathname: ‘/guide’ } }
  75. 75. 그밖에, 아키텍처에 영향을 주는 결정 스펙 변경 모듈 업데이트 ( webpack, react-router v3 to v4 ) 배포 환경의 변화 immutablejs 도입 Flow type checker / typescript 도입 ServerSide Rendering 클라우드 API / GraphQL 도입 Rx / MobiX 등 근간을 이루는 아키텍처 패러다임의 변화
  76. 76. 이상 끝! 질문은 메일로… 오프에선 맥주로… 여행기는 페북으로… 여행정보는 알리브로에… miconblog@gmail.com facebook.com/wearemooving rlibro.com

×