1편에서 우리는 기본적인 Backend 를 완성하였습니다.
우리가 만든 Backend 를 기반으로 React 어플리케이션을 만들어봅시다!
6. webpack 설정 추가 및 클라이언트사이드에서 필요한 모듈 설치
webpack 에 resolve 설정 추가하기 (webpack.config.js & webpack.dev.config.js)
var path = require('path'); /* .. 코드 생략 .. */ , resolve: { root: path.resolve('./src') }
위 설정을 추가해주면 React 프로젝트의 루트디렉토리를 설정하여, 나중에 ./components 혹은 ../components 이렇게 접근해야 되는 디렉토리를 바로 components 로 접근 할 수 있게 해줍니다. 이렇게 설정하면 프로젝트를 만들면서 여러모로 편하답니다.
모듈 설치
npm install --save axios react-addons-update react-router react-timeago redux react-redux redux-thunk
설치 해야 할 모듈이 꽤 있죠?
여기선 간단한 설명만하고 자세한 설명 및 사용법은 잠시 후 사용하게 될때 자세히 다루도록 하겠습니다.
axios: HTTP 클라이언트
react-addons-update: Immutability Helper (Redux 의 store 값을 변경 할 때 사용됨)
react-router: 클라이언트사이드 라우터
react-timeago: 3 seconds ago, 3 minutes ago 이런식으로 시간을 계산해서 몇분전인지 나타내주는 React 컴포넌트
redux, react-redux; FLUX 구현체, 그리고 뷰 레이어 바인딩
redux-thunk: redux의 action creator에서 함수를 반환 할 수 있게 해주는 redux 미들웨어, 비동기작업을 처리 할 때 사용됩니다
webpack css-loader 와 style-loader 설치
npm install --save-dev style-loader css-loader
이 로더들을 통하여 프로젝트에서 css 파일을 require (import) 해서 사용 할 수 있습니다.
webpack 에 로더 적용하기 (webpack.config.js & webpack.dev.config.js)
module: { loaders: [ { /* ... */ }, { test: /\.css$/, loader: 'style!css-loader' } ] },
*webpack 설정파일이 바뀌고나면 서버를 재시작해야 적용됩니다.
style.css 파일 생성하기 (src/style.css)
src 디렉토리에 style.css 를 생성하세요. 페이지 배경을 회색으로 설정하는 스타일을 추가합시다
body { background-color: #ECEFF1; }
webpack entry 에 style.css 추가 (webpack.confg.js & webpack.dev.config.js)
// webpack.config.js entry: [ './src/index.js', './src/style.css' ], // webpack.dev.config.js entry: [ './src/index.js', 'webpack-dev-server/client?http://0.0.0.0:4000', 'webpack/hot/only-dev-server', './src/style.css' ],
7. React 프로젝트 디렉토리 구조 이해하기
src ├── actions │ ├── ActionTypes.js │ ├── authentication.js │ ├── index.js │ ├── memo.js │ └── search.js ├── components │ ├── Authentication.js │ ├── Header.js │ ├── index.js │ ├── Memo.js │ ├── MemoList.js │ ├── Search.js │ └── Write.js ├── containers │ ├── App.js │ ├── Home.js │ ├── index.js │ ├── Login.js │ ├── Register.js │ └── Wall.js ├── index.js └── reducers ├── authentication.js ├── index.js ├── memo.js └── search.js
참고로 저희 프로젝트에서 사용하는 디렉토리 구조는 https://github.com/erikras/react-redux-universal-hot-example 에서 사용하는 디렉토리 구조 컨벤션을 따와서 사용하고 있습니다.
우선, 각 컴포넌트들은 components 에 위치합니다. 그리고, 라우터에서 보여줄 ‘페이지’ 는 container 에 위치하구요. 각 페이지에서 “틀” 로서 사용되는 App.js 또한 container 디렉토리에 위치하구있구요.
컴포넌트는 계정인증, 헤더, 메모, 메모목록, 쓰기 컴포넌트들이 있습니다.
컨테이너는 홈, 로그인, 가입, 담벼락 (유저의 메모 목록)이 있구요.
redux 에서 필요한 action type 의 경우 모두 ActionTypes.js 에 적혀있으며,
reducer 는 계정인증 / 메모 / 검색 부분으로 나뉘어져있습니다.
8. Materializecss (http://materializecss.com/)
저희 프로젝트에서는 Materializecss 라는 CSS 프레임워크를 사용 할 거에요.
적용하기 쉽고, 예쁘답니다.
index.html 에 materializecss 관련 파일들 불러오기 (public/index.js)
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <!--Import Google Icon Font--> <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> <!--Import materialize.css--> <link type="text/css" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.6/css/materialize.min.css" media="screen,projection"/ /> <!--Let browser know website is optimized for mobile--> <meta name="viewport" content="width=device-width, initial-scale=1.0"/> <title>MEMOPAD</title> </head> <body> <div id="root"></div> <script type="text/javascript" src="https://code.jquery.com/jquery-2.1.1.min.js"></script> <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.6/js/materialize.min.js"></script> <script src="/bundle.js"></script> </body> </html>
9. Header 만들기
아무리 Materializecss 가 편하다 한들, 이 CSS 프레임워크를 처음 사용하시는분이라면, 혹은 CSS 를 아직 능숙하게 다루지 못하는분이라면, 자신이 원하는 뷰를 만들기위해서 삽질하는건 불가피하겠죠 (저 또한 그렇구요 :D) 하나하나 직접 매뉴얼보면서 사용하면 시간이 너무 많이 소비될테니까, HTML 부분은 제가 미리 만든 뷰를 그대로 복사하여 컴포넌트들을 만들도록 하겠습니다. 강좌를 진행하면서 CodePen 링크를 눌러보시면 컴포넌트 뷰의 HTML 코드와, 해당 뷰에서 사용한 CSS 프레임워크의 클래스들의 링크가 참조되어있습니다.
CodePen 링크: http://codepen.io/velopert/pen/XKVmBR
왼쪽에 있는건 검색 버튼이고, 우측에는 버튼이 두개 있는데요,
로그인을 하지 않은 상태일땐 열쇠 아이콘(로그인)을, 로그인을 한 상태일땐 열린 자물쇠 아이콘(로그아웃)을 보여주게 할겁니다.
위 코드를 React 컴포넌트로 옮길 때 주의 하실점은 태그 속성 중 class를 className으로 변경해주셔야합니다.
컴포넌트 파일 생성 (src/components/Header.js)
import React from 'react'; class Header extends React.Component { render() { return ( <div>Header</div> ); } } export default Header;
컴포넌트 인덱스 생성 (src/components/index.js)
import Header from './Header'; export { Header };
containers 디렉토리 생성 (src/containers)
라우트용 컴포넌트, 및 메인컴포넌트인 App.js 가 위치할 container 디렉토리를 생성하세요.
그리고, App.js 를 containers 디렉토리로 이동시키세요.
컨테이너 인덱스 생성 (src/containers/index.js)
import App from './App'; export { App };
컴포넌트 인덱스를 생성했던것 처럼 컨테이너 인덱스를 만들어주세요
이렇게 하는건, 프로젝트의 구조를 깔끔하게 하기 위함이랍니다.
App 컴포넌트 수정 (src/containers/App.js)
import React from 'react'; import { Header } from 'components'; class App extends React.Component { render(){ return ( <Header/> ); } } export default App;
우리가 컴포넌트 인덱스를 만들었기 때문에, 위와 같은식으로 컴포넌트를 불러올때 편해져요.
나중에 컴포넌트 갯수가 많아져도 한 줄로 import 할 수 있게되죠.
클라이언트 사이드 엔트리 index.js 수정 (src/index.js)
import React from 'react'; import ReactDOM from 'react-dom'; import { App } from 'containers'; const rootElement = document.getElementById('root'); ReactDOM.render(<App />, rootElement);
App 컴포넌트의 위치가 바뀌었으니 클라이언트 사이드 엔트리인 index.js 파일을 수정합시다.
개발서버 실행하기
npm run development
이 명령어를 입력하여 개발서버를 실행해보세요.
문제없이 따라와주셨다면 http://localhost:4000/ 에 들어가면 Header 텍스트가 보일 것 입니다.
Header 컴포넌트 뷰, 그대로 갖다 붙이기
import React from 'react'; class Header extends React.Component { render() { return ( <nav> <div className="nav-wrapper blue darken-1"> <a className="brand-logo center">MEMOPAD</a> <ul> <li><a><i className="material-icons">search</i></a></li> </ul> <div className="right"> <ul> <li> <a><i className="material-icons">vpn_key</i></a> </li> <li> <a><i className="material-icons">lock_open</i></a> </li> </ul> </div> </div> </nav> ); } } export default Header;
Header 컴포넌트 PropTypes 및 defaultProps 설정하기
/*.. 코드생략 ..*/ Header.propTypes = { isLoggedIn: React.PropTypes.bool, onLogout: React.PropTypes.func }; Header.defaultProps = { isLoggedIn: false, onLogout: () => { console.error("logout function not defined");} }; export default Header;
props 의 type 과 기본값을 설정하는건 optional입니다. 즉, 귀찮으면 안해도 돼요!
하지만, 이렇게 하는편이 나중에 읽기 편하고 유지보수하기가 쉬워지니까 하도록 하겠습니다.
isLoggedn 은 현재 로그인 상태인지 아닌지 알려주는 값이고
onLogout 은 함수형 props 로서 로그아웃을 담당합니다.
로그인 여부에 따라 다른 버튼 보여주기
render() { const loginButton = ( <li> <a> <i className="material-icons">vpn_key</i> </a> </li> ); const logoutButton = ( <li> <a> <i className="material-icons">lock_open</i> </a> </li> ); return ( <nav> <div className="nav-wrapper blue darken-1"> <a className="brand-logo center">MEMOPAD</a> <ul> <li><a><i className="material-icons">search</i></a></li> </ul> <div className="right"> <ul> { this.props.isLoggedIn ? logoutButton : loginButton } </ul> </div> </div> </nav> ); }
자, 우리의 헤더 컴포넌트가 ‘대충’ 완성 됐어요.
하지만 아직 기능은 구현하지 않았기 떄문에 로그인버튼을 눌러도 아무 동작을 하지 않죠.
자, 이제, 로그인 버튼을 누르면 로그인 페이지로 들어가게 할건데요, 이 때 페이지 라우팅을 위해서 필요한 라이브러리가 바로 react-router 이랍니다.
10. react-router 사용하기
react-router 사용법: https://github.com/reactjs/react-router
라우터를 사용 할떄는, 우선 루트컴포넌트가 필요합니다. 바로 우리의 App.js 이죠.
App,js 에서, 라우터의 각 ‘페이지’들이 렌더링 될 자리를 만들어줘야해요
루트 컴포넌트에 라우트 렌더링 자리 만들어주기 (src/containers/App.js)
render() { return ( <div> <Header/> { this.props.children } </div> ); }
뭐 특별한걸 한 건 없습니다. { this.props.children } 을 삽입해준것 밖에는요.
this.props.children 은 원래 React 컴포너트에서 <App> 여기에 들어 가는 내용이 표시되는 곳 </App> 입니다.
react-router 를 사용하면 이 부분에 우리가 지정한 라우트가 표시되는거구요.
라우트 컴포넌트 – Home, Login, Register 만들기 (src/containers/_____.js)
// src/containers/Home.js import React from 'react'; class Home extends React.Component { render() { return ( <div> Home </div> ); } } export default Home;
// src/containers/Login.js import React from 'react'; class Login extends React.Component { render() { return ( <div> Login </div> ); } } export default Login;
// src/containers/Register.js import React from 'react'; class Register extends React.Component { render() { return ( <div> Register </div> ); } } export default Register;
이렇게 컴포넌트들을 새로 만들어줬으면, containers index 를 수정해줘야겠죠?
containers 인덱스 수정하기 (src/containers/index.js)
import App from './App'; import Home from './Home'; import Login from './Login'; import Register from './Register'; export { App, Home, Login, Register };
클라이언트 사이드 엔트리 index.js 에서 라우터 사용하기 (src/index.js)
import React from 'react'; import ReactDOM from 'react-dom'; import { Router, Route, browserHistory, IndexRoute } from 'react-router'; import { App, Home, Login, Register } from 'containers'; const rootElement = document.getElementById('root'); ReactDOM.render( <Router history={browserHistory}> <Route path="/" component={App}> <IndexRoute component={Home}/> <Route path="home" component={Home}/> <Route path="login" component={Login}/> <Route path="register" component={Register}/> </Route> </Router>, rootElement );
express 서버에서 클라이언트사이드 라우팅을 호환하도록 수정하기 (server/main.js)
/* ... 코드 생략 ... */ app.use('/api', api); /* ... 주의: API 하단부에 작성하세요 ... */ /* support client-side routing */ app.get('*', (req, res) => { res.sendFile(path.resolve(__dirname, './../public/index.html')); });
위 작업을 하지 않으면, URL 을 직접 입력하여 들어갔을때 클라이언트사이드 라우팅이 제대로 작동하지 않습니다.
클라이언트에서 링크를 클릭해서 들어갔을때는 작동 하지만요,
한번 URL 을 입력하여 클라이언트 사이드 라우팅을 테스트 해보세요.
- http://localhost/login
- http://localhost/register
잘 되나요?
헤더에서 로고 클릭하면 메인 페이지로 이동 (src/components/Header.js)
import { Link } from 'react-router'; /*..codes...*/ render() { /* CODES */ return ( <div> <nav> <div className="nav-wrapper blue darken-1"> <Link to="/" className="brand-logo center">MEMOPAD</Link> /* CODES */
a 태그 대신에 react-router 의 Link 컴포넌트를 사용했는데요,
이 컴포넌트는 페이지를 새로 로딩하는것을 막고, 라우트에 보여지는 내용만 변하게 해줍니다
(만약에 a 태그로 이동을하게된다면 페이지를 처음부터 새로 로딩하게됩니다)
로그인 / 회원가입 페이지에서는 헤더 보이지 않게 하기 (src/containers/App.js)
render() { /* Check whether current route is login or register using regex */ let re = /(login|register)/; let isAuth = re.test(this.props.location.pathname); return ( <div> {isAuth ? undefined : <Header/>} { this.props.children } </div> ); }
여기서 의문을 가질 수 있어요. “이러면 결국 Home 에서만 Header 보여주면 되는거 아닌가요?”
저희가 App.js 에서 Header 를 불러오게 한 이유는, 아직 만들지 않은 라우트인 Wall.js 에서도 Header 를 사용하기 때문인데요.
뭐.. 물론 그냥 App.js 에 Header 를 넣지 않고 각 라우팅 컴포넌트 Home 과 Wall 에 Header를 넣어도 큰 문제는 없습니다!