React transmit with flux store, dynamic data with state


React를 사용하면서 flux 의 action > store > component 단방향 데이터 흐름에 매료되었는데 문제가 발생. 프론트엔드, SEO를 크게 신경 안쓴다면 전혀 문제가 아닐 수도..;;

일단 페이스북에서 나온 Relay는 graphQL을 사용하는 데이터 처리 라이브러리입니다. flux와 다른 데이터 처리를 하죠. relay에서 영감을 받아 나온 react-transmit은 graphQL을 사용하지 않고 promise 패턴으로 처리합니다. 구현 형태도 거의 유사하죠. 이 녀석들은 대략적으로 얘기하자면 component 에서 relay 를 통해 Async 데이터를 가져와 데이터를 처리하는 녀석인데 문제는 상위 컨테이너를 생성하고 props를 해당 component에서 사용하게 만듭니다.

이게 뭐가 문제냐구요?

Situation & Problem

일단 전제가 되는 상황은 server-side rendering 을 함께 처리하는 isomorphic/universal 을 하는 상황입니다.

  1. 그냥 데이터를 fetch 해서 프론트에서 출력한다면 component의 componentDidMount정도에서 비동기 API를 호출하면 동적으로 DOM을 생성해서 마크업에 들어가겠죠. server-side rendering 이 없기 때문에 실제 데이터들은 마크업의 소스코드에 노출되지 않습니다.
    현재까지 찾아본 isomorphic/universal react 들은 모두가 init 시의 동적생성 코드에 대한 해결책을 주지 않고 있더라구요.

  2. flux에 매료된 저에게는 action > store > component 와 props 가 아닌 state 를 observer 하면서 다이나믹한 데이터 처리를 하는 게 너무 좋다는 것이죠. relay나 transmit을 사용할때는 state 를 observer하지 않고 props를 보고 있기 때문에 데이터의 추가/제거 할때 다시 graphQL이나 promise fetch 처리를 해야 합니다. ( 일반적인 async 데이터 처리 )

react-transmit의 이슈 트래커에 이런 이슈들이 올라왔지만 아직 안되고 있죠. 아마도 안 될 가능성이 높을 거 같습니다. relay가 store를 사용하는 방향으로 가지 않는한..

그래서 해결점을 만들어 보았습니다. 의외로 간단히 처리가 되어버린;;;

Solution

기존 Transmit 을 사용할때의 코드를 잠시 보자면

class Thread extends React.Component {

constructor(props) {
  super(props);
  ...
}
....
componentDidMount() {
  ThreadStore.listen(this.onPostChange);
  // Ajax API 호출을 하지 않음. Transmit을 이용해 init 데이터를 server-side에서 렌더링
}
...
render() {
  let { post, comment } = this.props; // Transmit으로 post 데이터는 보장됩니다.
  // ES5 라면 post = this.props.post, comment = this.props.comment
  // this.state 를 사용할 수 없습니다 ;ㅅ;
  ...
  }
}
  
export default Transmit.createContainer(Thread, {
  ....
  Promise.all ...
})

이런 형태로 사용하게 되면 위의 postcomment 는 Transmit 에서 promise 로 처리할 데이터를 받게 되고 server-side에서 렌더링 하게 됩니다. 여기까지는 아주 좋습니다 :D

다만 react render에 있는 this.props.comment 는 this.state.comment 가 아니기 때문에 상태를 리스닝하고 있지 않기에 store의 dispatch가 불가능 합니다.
그냥 다시 transmit의 fetch 를 통해도 되지만… flux store 를 꼭 쓰고 싶습니다!

이제 조금 변경해 보죠.

아이디어는 간단합니다. store의 dispatch로 state가 변경되면 comment에 state 의 값을 concat 해서 붙이는 것이죠 ! 이것은 react의 render가 state change 마다 읽기 때문에 가능합니다

react render 부분을 약간 변경합니다. comments를 그냥 props를 사용하지 않고 데이터처리 후 return을 받아 처리하는 거죠.

render() {
  let { post } = this.props;
  let comments = this.getComments();

그리고 getComments를 구현합니다.

getComments() {
  let comments = [];
    
  if( !_.isEmpty( this.state.comments ) ) { // 클라이언트에서는 초기 로드시 this.state.comments 는 {} 일 거에요. state는 store의 dispatch가 일어나면 추가되었겠죠.
    comments = this.props.post.comments.concat( this.state.comments ); // props의 comments 는 Transmit으로 인해 생성되어있죠. 거기에 state 내용을 추가합니다.
  }
  else {
    comments = this.props.post.comments; // 초기 props의 comments
  }
  
  // console.log( 'comments: ', comments.length )
  return comments;
}

자 일단 완성. 그런데 문제가 발생합니다. 같은 key의 react component 오류가 나오게 되죠.
이유는 위의 comments = this.props.post.comments.concat( this.state.comments ); 부분입니다. 이 react component는 서버와 프론트 양쪽에서 모두 실행되기 때문인데요.

  • server-side : this.props.post.comments 있음 / this.state.comments 있음
  • cliend-side : this.props.post.comments 있음 / this.state.comments 없음

이렇게 되어있게 되는 거죠. 그렇기 때문에 server-side 에서 this.props.post.comments 에 this.state.comments 를 그냥 conacat 해 버리면 똑같은 배열이 두개 연결되는 결과를 보여줍니다.

  • server-side : concat 한 후에 comments.length = 22
  • client-side : concat 한 후에 comments.length = 11

그래서 중복된 배열값은 concat 하지 말아 보죠. 또한 서버와 클라이언트 렌더링을 분기해서 처리하면 불필요한 처리를 피할 수 있겠네요.

getComments() {
  let comments = [];
  
  if (__SERVER__) { // global 변수로 __SERVER__ 를 만들어서 처리하고 있는데 그냥 if (typeof window === 'undefined') { // For server 정도를 사용할 수도 있습니다.
    // console.log("Hello server");
    comments = this.props.post.comments;
  }
  
  else if (__CLIENT__) { // if (typeof window !== 'undefined') { // For client
    // console.log("Hello client");
    if( !_.isEmpty( this.state.comments ) ) {
      // comments = this.props.post.comments.concat( this.state.comments ); // 이놈을 사용하지 않고
      comments = _.union( this.props.post.comments, this.state.comments); // underscore를 이용해 union으로 유니크한 배열만 합칩니다.
    }
    else {
      comments = this.props.post.comments;
    }
  }
  
  // console.log( 'comments: ', comments.length )
  return comments;
}

자 이제 comments는 props의 기본 데이터에 state 가 변할때마다 state.comments 를 추가하게 되었네요.

Yeahhhhhhh~~

Related Posts

Web Front-end developer가 하는일은 무엇인가 그리고 웹개발자에게 바라는 JS

quora에 올라온 글을 재밌게 번역해 주신 내용 중에 프론트엔드 개발자가 하는 일

Apply, call, bind - javascript

apply, call, bind 정리하기

Isomorphic과 Universal javascript에 대한 단상

Isomorphic, Universal에 대해 들어보셨나요?

Hintholder - 모든 브라우저를 위한 placeholder

크롬, 파폭, 사파리 등 모던 브라우저부터 구버전 IE 까지 사용할 수 있는 placeholder

React, react-router, history npm 설치시 버전 충돌

react-router가 react와 버전이 맞지 않을 때 해결

기존에 익숙하게 사용하던 ES5 Javascript 와 ES6 비교

Front-end 에서 많이 사용하는 Javascript의 ES6 버전을 아직 많이 사용하고 있지 않다. 비교를 해보자.

Ubuntu 14.04 서버 셋팅 - python 3.4 & virtualenv

python 3.4 & virtualenv at ubuntu 14.04

github page로 blog 시작

Blog - github page with jekyll.

Pixyll in Action

See what the different elements looks like. Your markdown has never looked better. I promise.