React.js Router v3

React.js Router v3 사용하기

아직 React-Router v3 는 유지보수가 이뤄지고 있는 상태이며, 현재 2017년 3월 기준 가장 최신 버전은 v4 이다. (한동안 베타였는데 정식 릴리즈되었다. 관련 동영상이 만들어졌으니 여기를 참조)

리액트 프로젝트에서 여러 페이지가 있을 땐, 라우터를 사용한다. 라우터는 사용자가 요청한 URL에 따라서 다른 결과물을 렌더링해준다. 일반 Apache, Nginx 등의 웹 서버에서 각 페이지마다 다른 디렉토리 및 파일을 제공하여 여러 페이지를 구현하는것과 달리, 리액트 라우터(react-router)를 사용하는 프로젝트에서는 어떤 경로로 들어오던 똑같은 html 파일과 자바스크립트 파일을 제공을 한다.

여기서 제공되는 js 파일에서는 웹 어플리케이션에서 사용 할 모든 컴포넌트들이 담겨있고, URL에 따라서 지정된 컴포넌트를 렌더링해준다. 그리고, 페이지가 한번 로드 된 다음에 다른 페이지로 이동 시, 이동 될 때 마다 페이지를 처음부터 로딩하지 않고 기존에 불러왔었던 자바스크립트 파일을 이용하여 페이지에서 기존 컴포넌트를 언마운트 시키고 다른 컴포넌트를 마운트한다.

예를들어서, 웹의 헤더 컴포넌트와 같이 모든 페이지에서 존재하는 컴포넌트의 경우 페이지가 바뀌어도 처음부터 렌더링 할 필요 없이, 그대로 유지 할 수 있다는 뜻이다.

이 포스트에서는, React-router 를 프로젝트에서 사용하는 방법을 알아보도록 하겠다.

현재 React-router 의 최신버전은 v4 이다. React Router v4 Pre Release 버전의 경우 아직까진 베타여서 production-ready 하지는 않은 것 같다. 서브라우트의 경우 뒤로가기가 제대로 작동하지 않는 이슈가 있었고 (/ → /post/1 → /post/2 로 이동을 한다음에 뒤로가기를 하면 중간 서브라우트가 생략되고 / 로 이동 됨) 문서들이 부족해서 서버사이드 렌더링을 하게 될 때 가이드가 좀 부족한 편이고 (특히 리덕스 등의 상태 관리 라이브러리와 함께 사용시..) 써드 파티 라이브러리도 조금 적은 편이다.
그래서, 이 포스트에서는 v3 를 다뤄보겠다. 이 버전은 기존의 v2 버전과 동일하게 작동하는데, 기능개선 및 버그수정이 된 버전이다. v4 의 경우 새로운 방식의 라우팅이라 편하고 멋지긴 하지만, 만약에 문제를 겪을 시 혼자 해결해 나갈 수 있는 자신이 있으신 분들만 사용하시길 권장한다.

1. 프로젝트 만들기

react-router 를 사용해보기 위하여, 프로젝트를 준비해준다. create-react-app 이란 도구를 사용하면 간편하게 프로젝트를 만들 수 있다. 자세한 사용법은 여기를 참조하면 된다.

1
$ create-react-app react-router-tutorial

이 작업은 2~3분 정도 소요됩니다. 그 동안, 이 포스트를 훑어가면서 앞으로 어떤 작업을 하게 될 지 간단하게 예습을 해보세요.

설치가 완료되면, react-router 를 로컬 모듈로 설치하자.

1
$ npm install --save react-router

2. 프로젝트 계획

이번 프로젝트에서는 4가지의 라우트를 만들겠다.

/ 메인 라우트로서, 프로젝트에서 가장 처음 보여줄 페이지이다. Home 컴포넌트를 보여준다.
/about 이 라우트에서는 About 컴포넌트를 보여준다.
/post 이 라우트에서는 Posts 컴포넌트를 보여준다.
/post/:id/ 이 라우트에서는 Post 컴포넌트를 보여준다. id 라는 파라미터를 화면에 렌더링한다.
추가적으로, Header 라는 컴포넌트를 만들어 이 컴포넌트는 모든 페이지에서 보여주도록 설정 할 것이다.

그 다음에는 Node.js 환경의 서버를 사용하여 프로젝트를 올려보겠다.

3. 라우팅을 위한 컴포넌트 만들기

3.1 Header
먼저 src 디렉토리 내부에 components 디렉토리를 만들고, 그 안에 Header.js 라는 파일을 만든다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/components/Header.js

import React from 'react';
import './Header.css';

const MenuItem = ({active, children, to}) => (
<div className="menu-item">{children}</div>
)

const Header = () => {
return (
<div>
<div className="logo">velopert</div>
<div className="menu">
<MenuItem></MenuItem>
<MenuItem>소개</MenuItem>
<MenuItem>포스트</MenuItem>
</div>
</div>
);
};

export default Header;

추후, 활성화된 라우트의 메뉴 아이템일 경우 다른 스타일이 적용되게 설정 할 것이다.

다음, 그리고 간단하게 스타일링을 해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/* src/components/Header.css */

.logo {
height: 3.5rem;
background-color: #212529;
width: 100vw;
line-height: 3.5rem;
color: white;
font-size: 1.5rem;
font-weight: bold;
text-align: center;
}

.menu {
background-color: #343a40;
height: 3.5rem;
}

.menu-item {
display: inline-block;
color: white;
font-size: 1rem;
line-height: 3.5rem;
width: 33.3333%;
text-align: center;
cursor: pointer;
transition: background-color 0.3s;
text-decoration: none;
}

.menu-item:hover {
background-color: #495057;
}

.menu-item:active,
.menu-item.active {
background-color: #1862ab;
}

이제 App.js 에서 헤더 컴포넌트를 불러와서 렌더링한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/App.js

import React, {Component} from 'react';
import Header from './components/Header';

class App extends Component {
render() {
return (
<div>
<Header/>
{this.props.children}
</div>
);
}
}

export default App;

헤더 아래에는 10번째 줄에서 children 이 렌더링 되게 했다. 복습을 해보자면, 이 부분엔 컴포넌트 태그 사이의 내용이 입력된다.
예: <컴포넌트>여기 있는 내용</컴포넌트>.

나중에, 라우트용 컴포넌트가 저 부분에서 렌더링 되는 것 이다.

3.2 BigText 컴포넌트 만들기
이 컴포넌트는 아무 의미없이 대문짝만하게 큰 글씨를 띄워주는 컴포넌트이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/components/BigText.js

import React from 'react';
import './BigText.css';

const BigText = ({children}) => {
return (
<div className="big-text">
{children}
</div>
);
};

export default BigText;
1
2
3
4
5
6
7
/* src/components/BigText.css */

.big-text{
margin-top: 3rem;
font-size: 7rem;
text-align: center;
}

3.3 Home 컴포넌트 만들기
프로젝트에서 가장 먼저 보여줄 라우트인 / 라우트를 위한 컴포넌트를 만들어보자.
이 컴포넌트는 components 디렉토리 말고 containers 디렉토리에 만들어준다. (꼭 그럴 필요는 없지만 이는 일반 컴포넌트들과 라우트용 컴포넌트를 분리하기 위함이다.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/containers/Home.js

import React from 'react';
import BigText from '../components/BigText';

const Home = () => {
return (
<div>
<BigText></BigText>
</div>
);
};

export default Home;

3.4 About 컴포넌트 만들기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/containers/About.js

import React from 'react';
import BigText from '../components/BigText';

const About = () => {
return (
<div>
<BigText>소개</BigText>
</div>
);
};

export default About;

3.5 Posts 컴포넌트 만들기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/containers/Posts.js

import React from 'react';
import BigText from '../components/BigText';

const Posts = () => {
return (
<div>
<BigText>포스트</BigText>
</div>
);
};

export default Posts;

4. 라우터 설정

컴포넌트들이 준비되었으니, 라우터 설정을 해보자. index.js 파일을 다음과 같이 수정한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { Router, Route, IndexRoute, browserHistory } from 'react-router';

import App from './App';
import Home from './containers/Home';
import About from './containers/About';
import Posts from './containers/Posts';

import './index.css';

ReactDOM.render(
<Router history={browserHistory}>
<Route path="/" component={App}>
<IndexRoute component={Home}/>
<Route path="about" component={About}/>
<Route path="post" component={Posts}/>
</Route>
</Router>,
document.getElementById('root')
);

3번 줄에서는, react-router 에서 4가지의 객체를 불러왔다.

  • Router: 이 컴포넌트는 react-router 의 주요 컴포넌트로서, 라우터의 속성을 정의하고 이 내부에서 라우트 설정을 한다.
  • Route: 이 컴포넌트는 우리가 설정한 경로에서 어떤 컴포넌트를 렌더링 할 지 정하는 컴포넌트 이다. 이 라우트 컴포넌트의 자식에 또 다른 Route 컴포넌트를 넣으면 해당 자식 컴포넌트는 부모 라우트의 서브 라우트가 된다.
  • IndexRoute: 라우트에서 서브라우트가 주어지지 않았을 때, 즉 특정 라우트의 / 경로로 들어 왔을 때, 이 라우트에서 지정한 컴포넌트를 보여준다.
  • browserHistory: HTML5 의 History API 를 사용하여 브라우저의 URL 변화를 주시하고, 조작한다.

13번 줄에서는 Router 컴포넌트를 정의하고 history 값을 browserHistory 로 설정 했다. history 는 브라우저의 주소창이 어떻게 바뀌는지 주시하고 주소를 라우터에서 인식할 수 있도록 location 객체로 파싱을 해준다. history 는 총 3가지가 있는데, 이에 대해선 여기서 더 자세히 알아 볼 수 있다.

14번 줄에서는 Route 컴포넌트의 path 를 “/“ 로 설정했다. 즉, / 경로로 들어왔을땐 App 컴포넌트를 보여주라고 설정하는 것 이죠.

그 내부에는 여러개의 Route 들이 자식으로 있는데, 이 자식들은 URL 이 매칭 하는 경우, App 컴포넌트의 자식으로 들어갑니다. 예를 들어서, / 경로의 경우엔 IndexRoute 를 사용하여 Home 컴포넌트를 렌더링한다. /about 경로의 경우엔 About 컴포넌트를 렌더링하죠.

자, 이제 index.js 파일을 저장하고 브라우저에서 열어보면. 첫 화면에서 홈이 보일 것이다.

5. 헤더 기능 구현

아직 헤더는 폼만 잡고 있고, 클릭해도 아무 기능을 하지 않는다. 이제 기능을 구현해보자. 여기서 버튼을 눌렀을 때, 단순히 태그를 사용하여 링크를 걸 면 안된다. 작동을 하긴 하겠지만, 페이지를 새로 불러오게 된다.

하지만 리액트 프로젝트의 경우 모든 프로젝트의 클라이언트 정보를 코드를 번들링 할 때 한 파일에 담기 때문에, 주소가 변한다고해서 페이지를 새로 로딩 할 필요가 없다. 따라서, 우리는 Link 라는 컴포넌트를 사용해야한다. 이 컴포넌트는 브라우저의 주소만 바꿔주고 페이지를 새로 로딩하진 않는다.

그렇게 브라우저의 주소가 바뀌고 나면, Router 컴포넌트가 이를 인식하여 우리가 정한 컴포넌트를 보여주게 될 것이다.

5.1 메뉴 아이템 클릭시 페이지 이동

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// src/components/Header.js

import React from 'react';
import { Link } from 'react-router';
import './Header.css';

const MenuItem = ({active, children, to}) => (
<Link to={to} className="menu-item">
{children}
</Link>
)

const Header = () => {
return (
<div>
<div className="logo">
velopert
</div>
<div className="menu">
<MenuItem to={'/'}></MenuItem>
<MenuItem to={'/about'}>소개</MenuItem>
<MenuItem to={'/post'}>포스트</MenuItem>
</div>
</div>
);
};

export default Header;

MenuItem 컴포넌트에서 기존에 div 태그를 사용 하던 것을 Link 로 변환 하였다. 이 컴포넌트에 className 을 설정하면 그대로 전달이 돼서 해당 클래스를 가진 a 태그로 이뤄진 컴포넌트로 변환해준다.

이 컴포넌트는 링크가 클릭 되었을 때, 페이지가 전환 되는 것을 막고, Router 에서 정한 history 를 사용하여 브라우저의 주소를 변경한다.

Link 컴포넌트가 눌렸을 때, 설정 될 라우트 경로는 to 값을 통해 설정한다. 위 코드에서는 MenuItem 에서 to 값을 설정하고 이 props 가 Link 컴포넌트의 값으로 설정되게끔 전달되었다.

코드를 저장하고, 잘 작동하는지 헤더의 버튼을 눌러보자.

5.2 현재 주소에 따라 메뉴 아이템에 다른 효과 주기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// src/components/Header.js

import React from 'react';
import { Link } from 'react-router';
import './Header.css';

const MenuItem = ({active, children, to}) => (
<Link to={to} className={`menu-item ${active ? 'active': ''}`}>
{children}
</Link>
);

const Header = (props, context) => {
const { router } = context;
return (
<div>
<div className="logo">
velopert
</div>
<div className="menu">
<MenuItem to={'/'} active={router.isActive('/', true)}></MenuItem>
<MenuItem to={'/about'} active={router.isActive('/about')}>소개</MenuItem>
<MenuItem to={'/post'} active={router.isActive('/post')}>포스트</MenuItem>
</div>
</div>
);
};

Header.contextTypes = {
router: React.PropTypes.object
}

export default Header;

현재 주소에 따라서 메뉴 아이템을 파란색으로 설정을 하려면, 컴포넌트의 context 객체의 router 에 접근을 해야 하는데, 이를 사용하려면, 27 ~ 29번 줄처럼 contextType 을 지정해주어야 한다.

context 는 React 프로젝트에서 전역적으로 사용 될 수 있는 객체이다. 컴포넌트마다 props 로 전달하기 힘든 경우에 이 기능이 사용된다.
class 형태의 컴포넌트의 경우엔 this.context.router 라고 사용을 하면 되고, 위 처럼 함수형 컴포넌트의 경우엔, 두번째 파라미터로 context 를 전달받아서 사용하면 된다.

router 객체 내부의 isActive 함수는 현재 브라우저의 경로가 주어진 경로와 매칭이 되는지 확인을 한다. 첫번째 파라미터로는 경로가 들어가고 두번째 파라미터는 주어진 경로가 IndexRoute 인지 설정을 한다. 예를들어서. 만약에 현재 경로가 /about 일 때, isActive('/') 가 실행 되면 현재 경로가 / 의 자식 경로이기 때문에 true 를 반환한다. 두번째 파라미터를 설정하여 isActive('/', true) 를 실행하면 현재 경로가 정확히 / 일 때만 true 를 반환하고 그 외엔 false 를 반환합니다.

이를 함수를 사용하여 사용하여 MenuItem 의 active 값을 설정해주고, MenuItem 컴포넌트에서는 active 값이 true 라면 active 라는 클래스를 적용하도록 설정했다.

이제 활성화된 메뉴아이템은 파란색으로 설정된다.

6. 서브 라우트 설정하기

이제 우리의 4번째 라우트, /post/:id/ 를 구현해보겠다.

6.1 Post 컴포넌트 만들기
먼저 Post 컴포넌트를 만들어보.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/containers/Post.js

import React from 'react';
import BigText from '../components/BigText';

const Post = ({params}) => {
return (
<div>
<BigText>{params.id}</BigText>
</div>
);
};

export default Post;

라우트에서의 파라미터 값은, 컴포넌트에서 params props 에 접근하여 얻어 낼 수 있다.

6.2 Posts 컴포넌트 수정하기
Post 컴포넌트가 Posts 컴포넌트의 내부에서 보여지게 할 계획이다. 마치, App 컴포넌트 내부에서 헤더를 보여주고 그 하단에 Home, About, Posts 컴포넌트를 보여준 것 처럼 말이죠.

그러려면, Posts 컴포넌트에서 Post 컴포넌트가 보여질 부분에 children 을 렌더링 하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/containers/Posts.js

import React from 'react';
import BigText from '../components/BigText';

const Posts = ({children}) => {
return (
<div>
<BigText>포스트</BigText>
{children}
</div>
);
};

export default Posts;

6.3 라우터 설정 수정하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { Router, Route, IndexRoute, browserHistory } from 'react-router';

import App from './App';
import Home from './containers/Home';
import About from './containers/About';
import Posts from './containers/Posts';
import Post from './containers/Post';

import './index.css';

ReactDOM.render(
<Router history={browserHistory}>
<Route path="/" component={App}>
<IndexRoute component={Home} />
<Route path="about" component={About} />
<Route path="post" component={Posts}>
<Route path=":id" component={Post} />
</Route>
</Route>
</Router>,
document.getElementById('root')
);

라우터를 위와 같이 수정해준다. /post 경로를 위한 Route 컴포넌트 내부에 또 다른 Route 컴포넌트를 작성한다. 여기서 path 을 :id 로 설정을 하면, id 라는 파라미터가 들어가는것이라고 설정을 하는 것 이다.

지금은 Post 컴포넌트가 Posts 내부에 위치하게 하고 싶기 때문에 이렇게 했지만, 만약에 주소가 /post/:id 일 때 Posts 를 보여주지 않고 Post 만 보여주게 하고싶다면, 이렇게 라우트를 다음과 같이 작성하면 된다.

1
2
3
4
5
6
7
8
9
10
11
ReactDOM.render(
<Router history={browserHistory}>
<Route path="/" component={App}>
<IndexRoute component={Home} />
<Route path="about" component={About} />
<Route path="post" component={Posts} />
<Route path="post/:id" component={Post} />
</Route>
</Router>,
document.getElementById('root')
);

여기까지 완성이 되었으면 /post/10 이런식으로 브라우저에서 직접 주소를 입력하여 들어가보자.

6.4 PostLinks 컴포넌트 만들기
마지막으로, 포스트의 링크들을 보여주는 컴포넌트를 만들어서 Posts 컴포넌트에서 렌더링을 해보겠다.

먼저 컴포넌트파일을 만든다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/components/PostLinks.js

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

import './PostLinks.css';

const PostLinks = () => {
return (
<div className="post-links">
<Link to="/post/1">1</Link>
<Link to="/post/2">2</Link>
<Link to="/post/3">3</Link>
<Link to="/post/4">4</Link>
</div>
);
};

export default PostLinks;

간단하게 스타일링도 해줍시다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* src/components/PostLinks.css */

.post-links a {
display: block;
text-align: center;
text-decoration: none;
background-color: #495057;
color: white;
padding: 0.5rem;
transition: background .3s;
}

.post-links a:hover {
background-color: #868e96;
}

.post-links a:active {
background-color: #343a40;
}

이제 Posts 컴포넌트에서 렌더링해줍니다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/containers/Posts.js

import React from 'react';
import BigText from '../components/BigText';
import PostLinks from '../components/PostLinks';

const Posts = ({children}) => {
return (
<div>
<BigText>포스트</BigText>
<PostLinks />
{children}
</div>

);
};

export default Posts;

7. 서버에 올리기

이제 Node.js 의 Express 프레임워크를 사용하여 이 프로젝트를 서버에 올려보겠다. 이 포스트에서는 여러분이 Express 에 대한 기본지식이 있다는 것을 전제로 하고 진행한다. (모르셔도 그대로 따라 할 수는 있으나, 이해를 하는게 조금 어려울 수도 있다)

현재 프로젝트 경로에서 express 를 설치하자.

1
$ npm install --save express

이 포스트에서는 Node 6.9.2 버전이 사용된다. 6 미만 버전을 사용하는 경우 업데이트 해야 정상적으로 작동한다.
방금 완성한 리액트 프로젝트를 빌드한다. 이 과정은 파일 최적화 과정을 거치기 때문에 1~2분 정도 소요 된다.

1
$ npm run build

빌드가 완료되었다면 서버파일을 작성해보자. 서버는 server 디렉토리를 만들어서 그 안에 작성해보자.

1
2
3
4
5
6
7
8
9
10
11
// server/index.js

const express = require('express');
const app = express();
const path = require('path');

app.use('/', express.static(path.resolve(__dirname, '../build')));

app.listen(4000, function () {
console.log('Example app listening on port 4000!');
});
1
$ node server

이제 브라우저로 http://localhost:4000/ 에 들어가서 테스팅을해보자.

작동은 잘 하는데, F5 를 눌르면 오류가 난다.

이렇게 라우팅이 잘 되다가 URL 로 직접 들어가면 오류가 발생하는 이유는, 처음 / 경로로 들어갔을때, 서버에서 리액트 프로젝트와 html 파일을 제공해주고 그 내부에서 라우팅을 할 때는 페이지를 새로 불러오지 않고 클라이언트 내부에서 자체적으로 라우팅을 하기 때문에 정상적으로 작동하지만 새로고침을 하거나 URL 로 직접 들어가면 서버 내에서 해당 라우트를 찾는데, 그것을 위해 우리가 express 에서 따로 준비한게 없어서 이렇게 오류가 나는 것 이다.

이를 해결하기 위해선 코드를 다음과 같이 모든 경로로 들어왔을때 리액트 index.html 를 보여주게 하면 된다. 주의하실 점은, 여기서 리액트 빌드 파일이 /static 경로에 위치해 있기 때문에 static 경로의 경우는 예외로 처리해야한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// server/index.js

const express = require('express');
const app = express();
const path = require('path');

app.use('/', express.static(path.resolve(__dirname, '../build')));
app.get('*', (req, res, next) => {
if(req.path.split('/')[1] === 'static') return next();
res.sendFile(path.resolve(__dirname, '../build/index.html'));
});

app.listen(4000, function () {
console.log('Example app listening on port 4000!');
});

이렇게하고 서버를 재시작하면 URL로 직접 들어가도 정상적으로 작동한다.

참조

공유하기