줄곧 React 계열의 기술 스택을 활용해보고 싶다고 생각했지만 기회가 오지 않아 해보지 못하고 있었습니다. 그러던 와중, 시뮬레이터 프로그램을 만들 일이 생겨서 React를 차용하기로 했죠. 하지만 제 실력이 너무 형편없다고 생각해서 일단 장난감 프로젝트부터 만들어보고 진행하기로 했습니다.

※ 이 글은 사용된 기술들에 대한 설명을 자세하게 하지 않습니다. 대략적인 흐름만 확인하시고 각각의 기술에 대해서는 각각의 공식문서를 참조해주세요.

패키지 관리자 설치

$ brew install yarn

그냥 npm을 써도 되지만 의존성 설치에 시간이 오래 걸리는게 답답해보였습니다. 저는 이 글을 쓰기 한참 전부터 yarn을 썼었는데, create-react-app은 시스템에 yarn이 있으면 자동으로 활용하는 것으로 보입니다. 이 글은 독자가 yarn을 설치했다고 가정하고 진행합니다.

yarn에 대한 자세한 이야기는 나중에 다른 글로 찾아뵙겠습니다.

create-react-app 설치

$ yarn global add create-react-app

이 글을 처음 쓰기 시작할 당시가 2016년 12월이었는데, 당시만 해도 yarn에 global 설치가 없었는데 버전이 상승하면서 추가되었습니다.

프로젝트 생성하기

$ create-react-app reacttest

뭔가 길게 나오는데, 끝날 즈음 Success!라고 나왔는지 보시면 됩니다.

Hello world!

$ yarn start

자동으로 빌드하고, 서버를 띄워서 브라우저까지 열어줍니다.

성공 화면

장난감 프로젝트 목표 설정

매우 쉽게 React 프로젝트 기반이 만들어졌으니, 이제 장난감으로 뭘 할지 정해야 할 것 같습니다. 저는 김태호님이 최근 작성하신 RxJS글에 영감을 얻어, 검색어로 GitHub ID를 넣으면 해당 유저의 저장소 명과 저장소의 프로그래밍 언어를 출력해주는 것을 짜려고 합니다.

일단 가볍게 파일 하나를 추가하고 소스를 고쳤습니다.

// App.js
import React from 'react';
import logo from './logo.svg';
import './App.css';
import Table from './Table';

export default class App extends React.Component {
  render() {
    return (
      <div className="App">
        <div className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h2>Welcome to React</h2>
        </div>
        <Table data={this.props.data} />
      </div>
    );
  }
}
// Table.js
import React from 'react';

export default class Table extends React.Component {
  render() {
    const rows = this.props.data.map(data => {
      return (
        <tr>
          <td><a href={data.html_url}>{data.full_name}</a></td>
          <td>{data.language}</td>
        </tr>
      );
    });
    return (
      <table>
        <thead>
          <tr>
            <th>Repo</th>
            <th>Language</th>
          </tr>
        </thead>
        <tbody>
          {rows}
        </tbody>
      </table>
    );
  }
}
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './index.css';

fetch('https://api.github.com/users/item4/repos')
  .then(res => res.json())
  .then(res => {
    ReactDOM.render(
      <App data={res} />,
      document.getElementById('root')
    );
  });

(아직 pygments가 JSX 등의 Facebook stack을 지원하지 않는 관계로 문법 강조를 껐습니다)

결과를 확인해보면 다음과 같군요.

성공 화면

소스 분석 및 수정 계획

소스에서는 별도의 의존성 없이 쉽게 Ajax 요청이 가능한 Fetch API를 사용했습니다만, cancel 등의 기능이 부족하므로 장기적으로 쓰기엔 좋지 않습니다. 임시 코드가 아니라면 Fetch API 대신 axios로 바꿀 생각입니다.

fetch의 promise chain에서 render를 해주는 점도 이상합니다. fetch가 비동기적인 동작이기 때문에, 자료를 받은 뒤에 렌더링을 해야하므로 저런 꼴이 되어버렸습니다. 하지만 이건 너무나도 이상하기 때문에, redux를 사용해서 걷어낼 생각입니다.

Redux 붙이기

$ yarn add redux react-redux

결론부터 말하면 어려웠습니다. 예전엔 문서에 예제와 설명이 더 많았던 것 같았는데 그것들이 다 사라져버린 느낌이더군요.

Ajax 요청은 대표적인 비동기 작업이기때문에 Redux에 대해 잘 알아야 할 필요가 있습니다. 예전에 간단한 동기 작업은 만져본 적이 있었지만, 아무래도 한 번에 점프하려면 고통이 따르는 것 같습니다. 결국은 Reddit API 예제를 거의 복붙하다시피 해서 구현했습니다.

구현을 위해서 밟은 스탭을 그대로 알려드리면 너무 너저분하므로 최종적으로 무엇무엇을 하면 되는지 설명해보자면

action을 정의해야 합니다

제 경우 GitHub API에서 검색을 한다는 것이 주제였는데, 검색어 변경(CHANGE_KEYWORD) action이 필요했고, 검색의 시작을 알리는 REQUEST_REPO, 검색 결과를 수신했음을 알리는 RECEIVE_REPO가 필요했습니다. 저는 예제에서 시키는 대로 redux-trunk를 사용했는데, 이것으로 얻은 dispatch를 이용해서 실제 action을 이용하는 녀석들(정확한 이름은 bounded action creator)도 만들었습니다.

export const CHANGE_KEYWORD = 'CHANGE_KEYWORD';
export const REQUEST_REPO = 'REQUEST_REPO';
export const RECEIVE_REPO = 'RECEIVE_REPO';

export const changeKeyword = () => {
  return {
    type: CHANGE_KEYWORD,
  };
};

export const requestRepo = () => {
  return {
    type: REQUEST_REPO,
  };
};

export const receiveRepo = (items) => {
  return {
    type: RECEIVE_REPO,
    items,
  }
};

reducer를 정의해야합니다

action의 결과로 넘어온 값이 reducer로 분배됩니다. 제 경우엔 검색어를 저장하는 keyword와 저장소 정보를 저장하는 repo의 두 개로 구성했습니다. 중요한 점은 자료를 return할 때 기존 값을 변형해서 넘기면 안 된다는 점이겠네요. 이러한 이유로 spread operater나 Object.assign이 매우 유용합니다.

function repo(state = {isFetching: false, didInvalidate: false, items: []}, action) {
  switch (action.type) {
    case CHANGE_KEYWORD:
      return Object.assign({}, state, {
        didInvalidate: true,
      });
    case REQUEST_REPO:
      return Object.assign({}, state, {
        isFetching: true,
        didInvalidate: false,
      });
    case RECEIVE_REPO:
      return Object.assign({}, state, {
        isFetching: false,
        didInvalidate: false,
        items: action.items,
      });
    default:
      return state;
  }
}

store를 만들고 React와 연계해야합니다

튜토리얼을 대충 봐서는 알 수 없고, 예제 소스를 봐야 알 수 있었습니다. 해야 하는 작업은 크게 3개입니다.

  1. store를 만듭니다.
  2. redux.Provier로 전체 앱을 한번 감싸줍니다.
  3. state를 props로 받기 위해 의 정의를 새로 합니다. 변환 함수를 작성 한 뒤 redux.connect를 이용합니다. export default connect(f)(App)꼴로 사용합니다.
export default class Root extends React.Component {
  render() {
    const { store } = this.props;
    return (
      <Provider store={store}>
        <Router history={browserHistory}>
          <Route path="/" component={App}>
            <IndexRoute component={IndexPage} />
            <Route path="/info(:keyword)" component={InfoApp} />
          </Route>
        </Router>
      </Provider>
    );
  }
}
Root.propTypes = {
  store: React.PropTypes.object.isRequired,
};
const mapStateToProps = (state) => {
  const { repo, info, limit } = state;
  return {
    repo,
    info,
    limit,
  };
};

export default connect(mapStateToProps)(InfoApp);

느낀 점

처음 하는 거긴 하지만, 어려운데 재밌습니다. 아직 초장인데도 불구하고 jQuery로 callback과 씨름하던 나날이 다른 모양으로 바뀔 것을 기대할 수 있습니다.

이 글이 1편인 이유는 아직 지금 만들던 프로그램도 다 못 만들었다고 판단중이기 때문입니다. 일단 1글자 바뀔때마다 요청을 보내다보니 API Limit을 너무 빨리 갉아먹습니다. API Limit을 검사해서 한계에 접근하면 동작하지 않도록 제한을 하고, 검색 자체도 너무 빈번하게 발생하지 않도록 조율하고 싶습니다. 그리고 계정이 존재하지 않는 경우와 저장소가 없는 계정이 분간이 안 되는 문제도 수정하고 싶습니다.