본문 바로가기

FrontEnd/React,Redux

PART2:Simply build App with Redux - Router, Redux Structure

How to Make it ?

앞서 무엇을 만들지 설명을 하지 않았다.

대부분에 boilerplate는 찾아보면 카운터를 예로 들고 있다.

뭘만들지 고민해보다가 About 페이지에 게시판을 만들기로 결정 했다.

DB가 없기 때문에 JSON 방식으로 더미 데이터를 저장해 놓고

Promise 를 활용해서 Api 처럼 활용 가능 하도록 MockApi 를 구현할 계획 이다.

Router

components 폴더에 컴포넌트 별로 폴더를 만들고

About, Header, Main 파일을 생성한다.

앞서 만들었던 Hello 컴포넌트를 삭제한다.

Header 컴포넌트에 라우트 MainAbout 2개를 정의할것 이다.

앞서 지난 포스팅에 react-router 설치를 빼먹었다.

npm i -S react-router

Header.js

import React, { Component } from 'react'
import { Link } from 'react-router';

export default class Header extends Component {
    render() {
        return (
            <nav>
                <Link to="/main" activeClassName="active">Main</Link>
                {" / "}
                <Link to="/about" activeClassName="active">About</Link>
                {this.props.children}
            </nav>
            );
    }
}

react-router로 부터 Link 를 사용해서 구현한다. activeClassName 속성은

클릭된 Link 태그의 class를 active 로 설정해주는 속성이다.

그리고 하단부에 this.props.children 이 보인다.

이는 Router 에서 매우 중요한 개념인데 children 을 내려준이유는 지금 작성한

페이지 컴포넌트가 Header 이기 때문이다.

<Route path='/' component={Header} >
    <Route path='main' component={main} />
    <Route path='about' component={about} />
</Route>

우리가 구현해야할 라우터는 이렇게 생겼다.

실제 페이지에서 보면 각 라우터로 이동하는 Header 에는

about 그리고 main 앵커 태그가 상단부에 고정되어 있고

Header 밑으로 앵커 태그 클릭에 따라 main 또는 about 페이지가 출력 된다.

즉 Header라는 부모 컴포넌트 밑에 자식으로 main 과 about 컴포넌트가 있기 때문에

Header 컴포넌트에서 this.props.children 은 자식이 있다고 명시해주는 역할을 하는것이다.

Header 컴포넌트에 this.props.children 을 명시하지 않으면 component가 Route에 정의되어 있어도

자식을 찾을수 없기 때문에 main 이나 about 은 랜더 되지 않는다.

Header.js 에서 링크를 클릭할시에 어디로 이동할지 구현해줬으니

이동할곳에 대한 컴포넌트를 정의해줘야 할 차례이다.

src 폴더 상위에 있는 index.js 를 수정해야 한다.

첫번째 포스팅 당시 Hello.js 를 render하는 부분을 Router로 수정할 것이다.

생성한 컴포넌트들을 호출해서 라우터를 구성한다.

index.js

import React from 'react';
import { render } from 'react-dom';
import { Router, Route, IndexRoute, browserHistory } from 'react-router';
import Header from './components/common/Header';
import Main   from './components/main/Main';
import About  from './components/about/About';

 render(<Router history={browserHistory} >
            <Route path="/" component={Header}>
                <IndexRoute component={Main} />
                <Route path="main" component={Main} />
                <Route path="about" component={About} />
            </Route>
        </Router>,document.getElementById("root"));

[그림]

라우터가 완성 되었다.

라우터까지 React에 대한 내용이였다.

Redux 로 프로젝트를 구조화 시키면서 라우터는 분리될것이다.

Redux

보통사람들은 Redux와 Flux를 많이 비교한다.

Flux 가 먼저 나왔지만 GitHub 상에 Repository 인기를 보면 후에 나온

Redux 의 인기가 더 높다.

몇가지 차이점이 있지만 내가 생각하기에 둘에 가장큰 차이점은 Redux 는

한개의 Store 를 사용하고 있다는 점이다.

동영상 을 보고 필자가 이해한 흐름을

iWorks로 직접 그려봤다.

 

여러분도 직접 그려보면 이해에 도움이 될것이라 확신한다.

이 흐름도를 기반으로 프로젝트 구조화를 진행해보자.

Actions

앞서 생성한 actions 폴더에는 액션을 정의하는 actionTypes.js 파일

그리고 상황별 actions을 dispatch 하는 **Actions.js 파일을 생성할것이다.

actionsTypes.js에 위치는 필자가 생각하는 기준이고 다른 boilerplate 에선

constants.js로 정의 되어 있거나 src 최상단에 자리잡고 있는 경우도 있다.

이는 구현하기 나름이다.

우리는 LOAD_BOARD_CLEAR 라는 상수를 정의 할것이다.

우리가 시도하려는 액션은 게시판에 더미데이터를 불러오는데서 시작하기 때문이다.

actionTypes.js

export const LOAD_BOARD_CLEAR = 'LOAD_BOARD_CLEAR';

해당 방식으로 액션들을 정의해줘도 되고 이런 방식도 있다.

actionTypes.js

const ACTION = {
    LOAD_BOARD_CLEAR : 'LOAD_BOARD_CLEAR'
}
Object.freeze(ACTION);
export default ACTION;

참고로 전자의 방식은 책이나 예제에서 많이 소개하고 있는 방식이다.

이에 따라 필자도 전자의 방식으로 구현을 이어가겠다.

다음은 actions 를 dispatch 하는 boardActions.js 파일을 생성한다.

해당 파일은 우선 생성만 해놓는다.

파일 안에 구현해야될 코드는 앞선 작업이 마무리된뒤에 계속해서 진행될 것이다.

Reducers

Reducer 는 state(구) 와 action 을 받아서 새로운 state 를 반환한다.

이때문에 시간여행 디버깅도 가능해 지는데 지금은 우선

새로운 state 를 반환 한다는 정도만 알아둬도 좋을것 같다.

reducers 폴더에 boardReducer.js 를 생성한다.

boardReducer.js

import { LOAD_BOARD_CLEAR } from '../actions/actionTypes';

export default function boardReducer(state = [], action) {
    switch(action.type) {
        case LOAD_BOARD_CLEAR :
            return action.contents;

        default :
            return state;
    }
}

state = [] 코드가 생소할수도 있다. 이는 ES6 문법으로 state 값을 빈배열로

설정해준다.

실제 프로젝트에서는 여러개의 Reducer가 있고 초기값도 제각각 인데

초기 state 를 설정하는 더 좋은 방법이 있다.

initState.js 라는 파일을 생성하자. initState.js

export default {
    contents : []
}

boardReducer.js

import { LOAD_BOARD_CLEAR } from '../actions/actionTypes';
import initState from './initState'

export default function boardReducer(state = initState.contents, action) { ... }

예제 에서는 Reducer 가 1개이기 때문에 combineReducers 를 사용하지 않아도 무관하지만

실제 프로젝트에서는 여러개의 Reducer 가 존재한다.

그렇기 때문에 combineReducers 를 사용한다.

root.js 파일을 생성한다.

import { combineReducers } from 'redux';
import board from './boardReducer';

const rootReducer = combineReducers({
    contents: board
});

export default rootReducer;

boardReducer 를 호출해서 cobineReducers 로 묶어준다.

이때 객체의 key값 contents 를 생략하고 board 만 입력해도 무방한데

board 라는 Reducer 의 key값은 자동으로 board 가 된다.

Store

실제 개발환경에서는 process.env 를 통해 환경이 dev or 'prod' 인지 체크하고

환경에 따라 다른 Store를 호출하는 방식을 활용한다.

Store 가 여러 middleware 를 포함할수 있기 때문이다.

배포 환경에서 불필요한 middleware 까지 호출 한다면 당연히 느려질수 밖에 없을것이다.

예제는 분기처리하지 않고 한개의 파일만 생성한다.

store.js 를 생성한다.

store.js

import { createStore, applyMiddleware } from 'redux';
import rootReducer from '../reducers';
import thunk from 'redux-thunk'

export default function configureStore(initialState) {
    return createStore(
        rootReducer,
        initialState,
        applyMiddleware(thunk)
        );
}

thunk middleware는 actions 폴더에있는 boardActions.js에서 asynchronism dispatch 를 가능하게 해준다.

Provider & Router Separation

Redux 구조화가 거의 마무리 단계에 이르렀다.

Redux의 최상위 파일은 보통 이러한 구조를 가지고 있다.

    <Provider store={store} >
        <Router history={browserHistory} routes={routes} />
    </Provider>

Provider 안에 Router 가 있는데 필자가 작성한 예제에서는 Route가 2개지만

얼마든지 많아질수있다. 그래서 Route를 분리해야한다.

src 하위에 routes.js 파일을 생성한다.

routes.js

import React from 'react';
import { Route, IndexRoute, browserHistory } from 'react-router';
import Header from './components/common/Header';
import Main   from './components/main/Main';
import About  from './components/about/About';

export default (
        <Route path="/" component={Header}>
            <IndexRoute component={Main} />
            <Route path="main" component={Main} />
            <Route path="about" component={About} />
        </Route>
    );

Route 분리가 완료되었다.

이제 index.js 파일을 수정한다.

index.js

import 'babel-polyfill';
import React, { Component } from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { Router, browserHistory } from 'react-router';
import routes from './routes';
import configureStore from './store/store';

const store = configureStore();

render(
    <Provider store={store} >
        <Router history={browserHistory} routes={routes} />
    </Provider>,document.getElementById("root"));

여기까지 따라 왔다면 Redux로 기본적인 구조화가 완료된 상태 이다.

하지만 뭔가 눈에 보이는것이 없다.

아직 더미 데이터를 만들지 않았고 디스패치된 액션이 없기 때문이다.

다음 포스팅에서 는 MockApi 를 만들고 webpack을 활용해서 css 를 페이지에 로드

하는 방법을 소개한다.