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 종류의 라우트가 존재합니다.
- 홈
- 포스트 목록
- 포스트 목록 (태그 설정)
- 포스트 읽기
- 에디터
- 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 컴포넌트를 작성 후, 브라우저에서 주소를 직접 입력하여 각 라우트들이 제대로 작동하는지 확인해보세요
