React.js Codelab 2016 – Express 와 React.js 를 사용한 웹 어플리케이션 만들기 (2)


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 컴포넌트

reduxreact-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 프레임워크의 클래스들의 링크가 참조되어있습니다.

이미지 7

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를 넣어도 큰 문제는 없습니다!

 

  • Seongkuk Park

    materialize 처음 알았네요~
    부스트랩만 주구장창 쓰다보니 .ㅅ.
    강의 잘 보고 있습니다 ~.~

  • SangHakLee

    8. Materializecss
    이 부분에서

    index.html 에 materializecss 관련 파일들 불러오기 (public/index.js)
    index.html 에 materializecss 관련 파일들 불러오기 (public/index.html)

    이걸로 변경해야 할것같습니다.

  • ggoban

    분리하느라 express 는 express-es6-rest-api 로 초기설정하고 뜯어고치고
    프론트앤드는 create-react-app 으로 만들어서 깨서 사용하니 구조가 약간 달라지네요. 아직까진 잘 되긴 하는데
    끝까지 잘 따라갈수 있기를 ㄷㄷㄷ

    • 화이팅!! 나중에 시간이 나면 서버클라이언트를 분리해서 새로 자료를 만들고싶네요..ㅋㅋ

  • ahribori

    안녕하세요:)

    ‘express 서버에서 클라이언트사이드 라우팅을 호환하도록 수정하기’에서

    /* support client-side routing */
    app.get(‘*’, (req, res) => {
    res.sendFile(path.resolve(__dirname, ‘./../public/index.html’));
    });

    이부분이 모든 요청에 대해서 public/index.html을 response 해주는 것 같은데요.
    npm run build로 빌드한다음에 npm run win_start로 express 서버를 띄워서(production mode)
    localhost:3000으로 접속해보면 bundle.js도 위의 조건에 걸려서 index.html로 response 되는 것 같습니다.

    일단 임시방편으로

    app.get(‘*’, (req, res, next) => {
    const regExp = /bundle.js$/;
    if(!regExp.test(req.url)) {
    res.sendFile(path.resolve(__dirname, ‘./../public/index.html’));
    } else {
    next();
    }
    });

    이렇게 고쳐서 해결하긴 헀는데요, 일반적인 해결 방법을 알고 계시다면
    알려주시면 감사하겠습니다.

    항상 좋은 강좌 감사드립니다!

  • 황인규

    webpack 2 부터는 resolve.root가 없는것 같습니다.

    resolve: {
    modules: [path.resolve(__dirname, “src”), “node_modules”]
    }

    이렇게 작성하면 설명하신 것처럼 똑같이 쓸 수 있는 것 같습니다!!

  • 김성민

    react 라우터에서 변동이 있어서인지는 모르겠는데 저대로 입력하면 빈 화면만 나오네요.
    임시로 react-router-dom 으로 사용하고있는데 수정 부탁드립니다
    항상 좋은 강좌 감사합니ㅏㄷ.

    • react-router v4부터 많이 바뀐 것 같다 그러더라고요. 저도 react-router-dom으로 대체 사용중입니다. (기존 react-router의 모든 기능은 그대로 담고 있습니다.)