1-1. 프로젝트 구조 잡기

프로젝트 생성

이번에 만들게 될 프로젝트는, 이전에 만든 서버를 연동하여 사용하게 됩니다. 이전에 우리가 만든 blog 디렉토리에서 create-react-app 을 통하여 blog-frontend 라는 리액트 프로젝트를 생성하세요.

$ create-react-app blog-frontend

이 프로젝트에서는 컴포넌트를 스타일링을 할 때는 Sass 와 CSS 모듈을 결합하여 사용하며, 리덕스를 사용하여 상태를 관리하고, 리액트 라우터를 통하여 여러 페이지들을 관리합니다. 추가적으로, 나중에 프로젝트를 완성하고 나서 코드 스플리팅과 서버사이드 렌더링을 구현합니다. 이 주요 요소들을 염두해 두고, 프로젝트의 디렉토리 구조를 형성해봅시다.

주요 디렉토리 생성

src 내부에 주요 디렉토리들을 만드세요. 주요 디렉토리는 총 5개가 있습니다.

  • components: 리덕스 상태에 연결되어있지 않은 프리젠테이셔널 컴포넌트들이 들어있습니다. 각 컴포넌트의 스타일 또한 이 디렉토리에 넣습니다.
  • containers: 리덕스 상태와 연결되어있는 컨테이너 컴포넌트들이 들어있습니다
  • lib: 백엔드 API 함수들과, 코드 스플리팅 할 때 사용되는 asyncRoute 가 들어있습니다
  • pages: 라우터에서 사용하게 될 각 페이지에 대한 컴포넌트들이 들어있습니다
  • store: Ducks 구조를 적용시킨 리덕스 모듈들과, 스토어 생성 함수가 들어있습니다
  • styles: 폰트, 색상, 반응형 디자인 도구, 그림자 생성 함수 등 프로젝트에서 전역적으로 필요한 스타일 관련 코드들이 들어있습니다.

불필요한 파일 제거

src 내부에 기본적으로 생성된 파일 중 불필요해진 파일들을 제거하세요: App.css, App.js, App.test.js, index.css, logo.svg

Sass 및 CSS 모듈 적용하기

이번 프로젝트에서는 컴포넌트 스타일링을 Sass 와 CSS 모듈을 결합하여 사용합니다. 이를 적용하기 위해선 웹팩 설정파일들을 밖으로 꺼내주어야 겠지요? yarn eject 를 사용하여 프로젝트 설정 파일을 밖으로 꺼내주세요.

$ yarn eject

그리고, Sass 적용을 위하여 node-sass 와 sass-loader 를 설치하세요. 추가적으로, CSS 모듈을 좀 더 편하게 사용 할 수 있게 해주는 classnames 를 설치하겠습니다.

$ yarn add node-sass sass-loader classnames

설치를 완료 한 다음에, 전역적으로 사용되는 스타일들을 편하게 불러올 수 있게 하기 위하여 config/path.js 의 하단에 globalStyles 를 추가하세요

config/paths.js - 하단부

module.exports = {
  dotenv: resolveApp('.env'),
  appBuild: resolveApp('build'),
  appPublic: resolveApp('public'),
  appHtml: resolveApp('public/index.html'),
  appIndexJs: resolveApp('src/index.js'),
  appPackageJson: resolveApp('package.json'),
  appSrc: resolveApp('src'),
  yarnLockFile: resolveApp('yarn.lock'),
  testsSetup: resolveApp('src/setupTests.js'),
  appNodeModules: resolveApp('node_modules'),
  publicUrl: getPublicUrl(resolveApp('package.json')),
  servedPath: getServedPath(resolveApp('package.json')),
  globalStyles: resolveApp('src/styles')
};

이제 webpack.config.dev.js 파일을 열어서 css 로더의 하단에 sass 로더를 설정하겠습니다.

config/webpack.config.dev.js - sass-loader 설정부분

          {
            test: /\.css$/,
            (...)
          },
          {
            test: /\.scss$/,
            use: [
              require.resolve('style-loader'),
              {
                loader: require.resolve('css-loader'),
                options: {
                  importLoaders: 1,
                  localIdentName: '[name]__[local]___[hash:base64:5]',
                  modules: 1,
                },
              },
              {
                loader: require.resolve('postcss-loader'),
                options: {
                  (...)
                },
              },
              {
                loader: require.resolve('sass-loader'),
                options: {
                  includePaths: [paths.globalStyles]
                }
              }
            ],
          },

기존의 css-loader 부분을 복사하고 그 하단에 그대로 붙여넣은 뒤에, 파일 확장자 부분과, css-loader 의 옵션을 변경하고, 배열이 끝나기 전에 sass-loader 를 설정하세요. 그 다음엔 아까전에 지정한 globalStyles 를 includePaths 로 설정하시면 스타일 설정이 끝납니다.

유사한 작업을 프로덕션용 설정파일은 webpack.config.prod.js 에도 반영하겠습니다.

config/webpack.config.prod.js - sass-loader 설정 부분

          {
            test: /\.css$/,
            (...)
          },
          {
            test: /\.scss$/,
            loader: ExtractTextPlugin.extract(
              Object.assign(
                {
                  fallback: require.resolve('style-loader'),
                  use: [
                    {
                      loader: require.resolve('css-loader'),
                      options: {
                        importLoaders: 1,
                        minimize: true,
                        sourceMap: shouldUseSourceMap,
                        localIdentName: '[name]__[local]___[hash:base64:5]',
                        modules: 1,
                      },
                    },
                    {
                      loader: require.resolve('postcss-loader'),
                      options: {
                        (...)
                    },
                    {
                      loader: require.resolve('sass-loader'),
                      options: {
                        includePaths: [paths.globalStyles]
                      }
                    },
                  ],
                },
                extractTextPluginOptions
              )
            ),
            // Note: this won't work without `new ExtractTextPlugin()` in `plugins`.
          },

라우터와 리덕스 적용

이번엔 리액트 라우터와 리덕스를 프로젝트에 적용하겠습니다. 다음 명령어로 필요한 라이브러리들을 설치하세요:

$ yarn add react-router-dom redux redux-actions react-redux redux-pender immutable

루트 컴포넌트 설정

설치를 완료 한 후, src 디렉토리에 Root 컴포넌트를 만드세요. 이름이 App 이 아니라, Root 인 이유는, 이 컴포넌트는 클라이언트쪽에서만 사용되기 때문입니다. App 컴포넌트의 경우엔 잠시 후 components 디렉토리 내부에 생성하게 되며, Root 컴포넌트에서는 App 컴포넌트를 브라우저에서 사용하는 라우터인 BrowserRouter 컴포넌트안에 감싸게 됩니다. 그리고, 나중에 서버사이드 렌더링을 구현하게 될 때에는, 서버 렌더링 전용 라우터인 StaticRouter 라는 컴포넌트에 App 을 감싸서 사용합니다.

src/Root.js

import React from 'react';
import { BrowserRouter } from 'react-router-dom';
import App from 'components/App';

const Root = () => {
  return (
    <BrowserRouter>
      <App/>
    </BrowserRouter>
  );
}

export default Root;

이제 App 컴포넌트를 만들어줘야겠죠? 그 전에, 컴포넌트를 불러올 때 경로를 절대경로로 입력 할 수 있게 하기 위하여 NODE_PATH 를 설정하겠습니다. 이전에는, package.json 에서 scripts 부분에서 설정했었지요? 또 다른방법은, .env 를 사용하는 방법이 있습니다. (config/env.js 에서 dotenv 가 적용되어있습니다.) 프로젝트의 루트 디렉토리에 .env 파일을 생성하고 다음과 같이 NODE_PATH 를 설정하세요.

.env

NODE_PATH=src

그 다음엔 App 컴포넌트를 만드세요.

src/components/App.js

import React from 'react';

const App = () => {
  return (
    <div>
      ReactBlog
    </div>
  );
};

export default App;

이제 자바스크립트 엔트리 파일인 index.js 를 수정하겠습니다. 기존에 있던 index.css 와 App.js 를 불러오는 코드를 제거하고, Root 를 불러와서 렌더링 하세요.

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import Root from './Root';
import registerServiceWorker from './registerServiceWorker';

ReactDOM.render(<Root />, document.getElementById('root'));
registerServiceWorker();

서버를 시작하여 페이지가 잘 나타나는지 확인 해 볼까요?

$ yarn start

리덕스 설정

리덕스를 설정하기 위하여, 프로젝트에서 필요한 모듈들을 먼저 만들어봅시다. 우리 프로젝트에서 필요한 리덕스 모듈들은 총 4종류 입니다:

  • base: 로그인 상태와 삭제/로그인시 보여지는 모달에 관한 상태를 다루게 됩니다
  • editor: 마크다운 에디터의 상태를 다루게 됩니다
  • list: 포스트 목록 관련 상태를 다루게 됩니다
  • post: 단일 포스트에 대한 상태를 다루게 됩니다.

위 모듈들의 세부 코드들은 나중에 작성을 하겠습니다. 지금은, store 디렉토리에 modules 디렉토리를 만들고, base.js, editor.js, list.js, post.js 파일들을 내부에 만들어서 동일한 내용으로 코드를 작성하세요:

src/store/modules/base.js, editor.js, list.js, post.js

import { createAction, handleActions } from 'redux-actions';

import { Map } from 'immutable';
import { pender } from 'redux-pender';

// action types

// action creators

// initial state
const initialState = Map({});

// reducer
export default handleActions({

}, initialState)

그 다음엔, 이 모듈들을 전부 불러와서 내보내줄 인덱스 파일을 만들겠습니다. 이 과정에서 비동기 액션을 관리하기 위한 redux-pender 의 penderReducer 도 불러와서 내보내세요.

src/store/modules/index.js

export { default as editor } from './editor';
export { default as list } from './list';
export { default as post } from './post';
export { default as base } from './base';
export { penderReducer as pender } from 'redux-pender';

리덕스 모듈이 준비되었으니, configure.js 라는 파일을 만들어서 스토어를 생성하는 함수인 configure 를 구현하겠습니다. 함수를 따로 만드는 이유는, 스토어 생성이 클라이언트에서도 이뤄지지만, 추후 서버사이드 렌더링을 할 때에도 서버쪽에서 호출해야 되기 때문입니다.

우리가 방금 만든 모듈들을 combineReducers 를 통하여 합쳐주고, penderMiddleware 또한 적용하겠습니다. 그리고, 개발환경에서는 Redux Devtools 를 사용하도록 설정하세요.

src/store/configure.js

import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
import penderMiddleware from 'redux-pender';
import * as modules from './modules';

const reducers = combineReducers(modules);
const middlewares = [penderMiddleware()];

// 개발 모드일때만 Redux Devtools 적용
const isDev = process.env.NODE_ENV === 'development';
const devtools = isDev && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__;
const composeEnhancers = devtools || compose;

// preloadedState 는 추후 서버사이드 렌더링이 되었을 때 전달받는 초기상태입니다.
const configure = (preloadedState) => createStore(reducers, preloadedState, composeEnhancers(
  applyMiddleware(...middlewares)
));

export default configure;

스토어를 생성 할 준비가 끝났습니다. Root 컴포넌트에서 configure 함수를 호출하여 스토어를 만들고, Provider 컴포넌트로 BrowserRouter 를 감싸세요.

src/Root.js

import React from 'react';
import { BrowserRouter } from 'react-router-dom';
import App from 'components/App';
import { Provider } from 'react-redux';
import configure from 'store/configure';

const store = configure();

const Root = () => {
  return (
    <Provider store={store}>
      <BrowserRouter>
        <App/>
      </BrowserRouter>
    </Provider>
  );
}

export default Root;

라우트 지정

프로젝트에서 필요한 라우트에서 사용 할 페이지 컴포넌트들을 사전 생성 해주겠습니다. 이 프로젝트에서는, 총 6 종류의 라우트가 존재합니다.

  1. 포스트 목록
  2. 포스트 목록 (태그 설정)
  3. 포스트 읽기
  4. 에디터
  5. 404 페이지

여기서, 1번, 2번, 3번 컴포넌트는 동일한 컴포넌트를 공유합니다. 모두 포스트 목록을 보여주지만, 서로 다른 설정으로 보여줍니다.

pages 디렉토리에 다음 컴포넌트들을 만드세요:

src/pages/ListPage.js

import React from 'react';

const ListPage = () => {
  return (
    <div>
      List
    </div>
  );
};

export default ListPage;

src/pages/PostPage.js

import React from 'react';

const PostPage = () => {
  return (
    <div>
      Post
    </div>
  );
};

export default PostPage;

src/pages/EditorPage.js

import React from 'react';

const EditorPage = () => {
  return (
    <div>
      Editor
    </div>
  );
};

export default EditorPage;

src/pages/NotFoundPage.js

import React from 'react';

const NotFoundPage = () => {
  return (
    <div>
      NotFound
    </div>
  );
};

export default NotFoundPage;

페이지 컴포넌트들을 다 만든 다음엔 페이지 인덱스 파일을 만드세요.

src/pages/index.js

export { default as ListPage } from './ListPage';
export { default as PostPage } from './PostPage';
export { default as EditorPage } from './EditorPage';
export { default as NotFoundPage } from './NotFoundPage';

이제 이 컴포넌트들을 불러와서 App 컴포넌트에서 라우트를 적용하세요.

src/components/App.js

import React from 'react';
import { Switch, Route } from 'react-router-dom';
import { ListPage, PostPage, EditorPage, NotFoundPage } from 'pages';
const App = () => {
  return (
    <div>
      <Switch>
        <Route exact path="/" component={ListPage}/>
        <Route path="/page/:page" component={ListPage}/>
        <Route path="/tag/:tag/:page?" component={ListPage}/>
        <Route path="/post/:id" component={PostPage}/>
        <Route path="/editor" component={EditorPage}/>
        <Route component={NotFoundPage}/>
      </Switch>
    </div>
  );
};

export default App;

여기서 사용된 리액트 라우터의 Switch 컴포넌트는, 설정된 라우트중에서 일치하는 라우트 하나만 보여주는 속성을 가지고 있습니다. 최하단에 설정된 NotFoundPage 에는, path 를 지정하지 않았기 때문에 어떠한 경우에도 렌더링이 됩니다. 하지만, Switch 로 감쌌으므로, 먼저 매칭된 한가지의 라우트만 보여주기 때문에 ListPage, PostPage, 혹은 EditorPage 가 보여져야 할때는 렌더링되지 않지만, 그 어떤 라우트에도 일치하지 않게 된다면, NotFoundPage 가 보여지게 됩니다.

App 컴포넌트를 작성 후, 브라우저에서 주소를 직접 입력하여 각 라우트들이 제대로 작동하는지 확인해보세요

results matching ""

    No results matching ""