[React.JS] 강좌: 리액트 프로젝트에서의 라우터, React-router v3 사용하기


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

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

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

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

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

현재 React-router 의 최신버전은 v4 입니다. 제가 이전에 한번 동영상을 찍은적이 있었죠. 하지만 제 프로젝트에서 사용을 해보니, 아직까진 베타여서 production-ready 하지는 않은 것 같습니다. 서브라우트의 경우 뒤로가기가 제대로 작동하지 않는 이슈가 있었고 (  / → /post/1 → /post/2 로 이동을 한다음에 뒤로가기를 하면 중간 서브라우트가 생략되고 / 로 되더군요 ) 문서들이 부족해서 서버사이드 렌더링을 하게 될 때 가이드가 좀 부족한 편이고 (특히 리덕스 등의 상태 관리 라이브러리와 함께 사용시..) 써드 파티 라이브러리도 조금 적은 편입니다.

그래서, 이 포스트에서는 v3 를 다뤄보겠습니다. 이 버전은 기존의 v2 버전과 동일하게 작동하는데, 기능개선 및 버그수정이 된 버전입니다. v4 의 경우 새로운 방식의 라우팅이라 편하고 멋지긴 하지만, 만약에 문제를 겪을 시 혼자 해결해 나갈 수 있는 자신이 있으신 분들만 사용하시길 추천드립니다 😉

1. 프로젝트 만들기

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

$ create-react-app react-router-tutorial

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

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

$ npm install --save react-router

 

2. 프로젝트 계획

저희 프로젝트에서는 4가지의 라우트를 만들겠습니다.

  • / 메인 라우트로서, 프로젝트에서 가장 처음 보여줄 페이지입니다. Home 컴포넌트를 보여줍니다.
  • /about 이 라우트에서는 About 컴포넌트를 보여줍니다.
  • /post 이 라우트에서는 Posts 컴포넌트를 보여줍니다.
  • /post/:id/ 이 라우트에서는 Post 컴포넌트를 보여줍니다. id 라는 파라미터를 화면에 렌더링합니다.

추가적으로, Header 라는 컴포넌트를 만들어 이 컴포넌트는 모든 페이지에서 보여주도록 설정 할 것입니다.

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

 

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

3.1 Header 만들기

이미지 1. 헤더 컴포넌트

그냥 텍스트로만 만들면 밋밋하고 재미없으니까, 간단한 스타일링도 하면서 해보겠습니다. 먼저 src 디렉토리 내부에 components 디렉토리를 만들고, 그 안에 Header.js 라는 파일을 만드세요.

src/components/Header.js

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

 

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

src/components/Header.css

 

이제 App.js 에서 헤더 컴포넌트를 불러와서 렌더링하세요.

src/App.js

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

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

3.2 BigText 컴포넌트 만들기

이미지 2. BigText 컴포넌트

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

src/components/BigText.js

src/components/BigText.css

3.3 Home 컴포넌트 만들기

프로젝트에서 가장 먼저 보여줄 라우트인 / 라우트를 위한 컴포넌트를 만들어봅시다.

이 컴포넌트는 components 디렉토리 말고 containers 디렉토리에 만들어주세요. (꼭 그럴 필요는 없지만 이는 일반 컴포넌트들과 라우트용 컴포넌트를 분리하기 위함입니다.)

src/containers/Home.js

3.4 About 컴포넌트 만들기

src/containers/About.js

3.5 Posts 컴포넌트 만들기

src/containers/Posts.js

 

4. 라우터 설정

컴포넌트들이 준비되었으니, 라우터 설정을 해봅시다. index.js 파일을 다음과 같이 수정해주세요.

src/index.js

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 파일을 저장하고 브라우저에서 열어보세요. 첫 화면에서 홈이 보여지나요?

이미지 3. 라우트 설정 완료

그럼 계속해서 작업을 진행합시다.

5. 헤더 기능 구현

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

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

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

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

src/components/Header.js

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

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

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

코드를 저장하고, 헤더의 버튼을 눌러보세요. 잘 작동하나요?

이미지 4. 메뉴 아이템 클릭시 페이지 이동

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

src/components/Header.js

현재 주소에 따라서 메뉴 아이템을 파란색으로 설정을 하려면, 컴포넌트의 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 라는 클래스를 적용하도록 설정했습니다.

이미지 5. 활성화된 메뉴 아이템

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

6. 서브 라우트 설정하기

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

6.1 Post 컴포넌트 만들기

먼저 Post 컴포넌트를 만들어봅시다.

src/containers/Post.js

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

6.2 Posts 컴포넌트 수정하기

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

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

src/containers/Posts.js

6.3 라우터 설정 수정하기

src/index.js

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

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

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

이미지 6. 서브 라우트

서브 라우팅이 잘 됐나요?

6.4 PostLinks 컴포넌트 만들기

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

먼저 컴포넌트파일을 만드세요.

src/components/PostLinks.js

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

src/components/PostLinks.css

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

src/containers/Posts.js

이제 브라우저를 확인해볼까요?

 

이미지 7. 포스트 링크

아주 잘됩니다!

7. 서버에 올리기

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

현재 프로젝트 경로에서 express 를 설치하세요

$ npm install --save express

이 포스트에서는 Node 6.9.2 버전이 사용됩니다. 6 미만 버전을 사용하는 경우 업데이트 해주셔야 정상적으로 작동합니다.

방금 완성한 리액트 프로젝트를 빌드해주세요. 이 과정은 파일 최적화 과정을 거치기 때문에 1~2분 소요 됩니다.

$ npm run build

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

server/index.js

$ node server

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

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

이미지 8. URL 로 직접 들어가면 오류 발생

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

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

server/index.js

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

코드는 GitHub 에서 확인 하실 수 있습니다.

서버사이드 렌더링까지 다뤄보려고했으나, 거기까지 진행하려면 글이 너무 길어져서 다음으로 미뤄보겠습니다..

  • incago

    감사합니다.

    ——–
    3.5 Posts 컴포넌트 만들기
    src/containers/Post.js <—- Posts.js 가 되어야 할 것 같습니다.
    ——–

    • 반영 완료. 감사합니다

  • Hyunseo Kang

    좋은 내용 항상 감사합니다.

    6.3 라우터 설정 수정하기 예제의 9번째 라인 import Post from ‘./components/Post’; 로 바꿔야 할듯 합니다.

    • 강의가 올라오자마자 주말에도 열공을 하신다니! 감사합니다 🙂
      반영되었습니다.

  • ggoban

    실습 완료했습니다~ 다음 서버 렌더링 도 기대하겠습니다~

    • 서버 렌더링은 주말부터 한번 준비해보겠습니다 ㅋㅋ
      지금 코드만 작성해둔 상태인데, 제가 하는 방법이 맞는 방법인지는………

      결론부터 얘기를 하자면 서버에서 웹팩을 사용합니다.ㅋㅋㅋㅋ

  • arthur

    정말로 훌륭한 강의준비해주셔서 항상 감사했습니다.

  • Kyungbae Ro

    정주행중!
    감사합니다.

  • Kyungbae Ro

    복습해도 이해가 안되는 부분이 있습니다.
    1. Header.js파일 `menu-item${active ? ‘active’: ‘’}` 에서
    “으로 감싸고 $를 사용했는데, 어떤 operation인지 궁금합니다.
    그냥 {active? ‘active’:”} 이면 안되는거죠?

    2. const { router } = context; 는
    context객체에서 ‘router’ 와 동일한 객체명이 있으면 그것만 리턴받아 변수로 사용한다는 뜻인가요?
    import { Link } from ‘react-router’; 이것도 비슷하게
    ‘react-router’객체 안해 ‘Link’ 와 동일한 객체명이 있으면 리턴받아 import 한다는 의미인거죠?

    • 1. Template Literal 이라는 기능이에요. 만약에 이를 사용하지 않는다면
      {‘menu-item ‘ + (active ? ‘active’:”)} 이렇게 하시면 됩니다.

      2. 네, ES6의 비구조화 할당 문법입니다 https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment

      • Kyungbae Ro

        답변 감사해요!
        인프런 강좌를 정주행하니, 여기서 궁금했던 것들이 거의다 해소되었어요.
        전달 내용과 구조도 훨씬 깔끔해서 이해하는데 완전 도움 되었습니다.

  • Kyungbae Ro

    서버사이드 랜더링과 최근 쌓이신 내공이 너무 궁금합니다.
    인프런에 3월 강좌를 언급하셨는데, 그때 공개되는 건가요?

  • Kyungbae Ro

    아 그리고 다른포스팅에서 책 발행도 언급하셨던거 같은데,
    기다릴께요. 1빠로 사볼께요 ^^

  • dali high

    감사합니다 ~

  • 양문규

    강의 감사합니다. 저는 c9으로 코딩을 따라하고 있는데요, 계속 오류가 나는데 도저히 원인이 무엇인지 잘 모르겠습니다..
    Warning: React.createElement: type is invalid — expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it’s defined in.

    Warning: Failed prop type: The prop `history` is marked as required in `Router`, but its value is `undefined`.
    in Router (at index.js:14)

    Uncaught TypeError: Cannot read property ‘location’ of undefined
    at new Router (Router.js:43)
    at ReactCompositeComponent.js:295
    at measureLifeCyclePerf (ReactCompositeComponent.js:75)
    at ReactCompositeComponentWrapper._constructComponentWithoutOwner (ReactCompositeComponent.js:294)
    at ReactCompositeComponentWrapper._constructComponent (ReactCompositeComponent.js:280)
    at ReactCompositeComponentWrapper.mountComponent (ReactCompositeComponent.js:188)
    at Object.mountComponent (ReactReconciler.js:46)
    at ReactCompositeComponentWrapper.performInitialMount (ReactCompositeComponent.js:371)
    at ReactCompositeComponentWrapper.mountComponent (ReactCompositeComponent.js:258)
    at Object.mountComponent (ReactReconciler.js:46)

    이렇게 세 줄이 나오는데 어떻게 해야할까요?

    • 최홍석

      npm install –save [email protected]
      react-route v3 기준으로 하고있어요

      • windrider

        [email protected] 으로 재설치를 해도 안되어서, 아래와 같이 하니까 되네요.

        import {Router, Route, IndexRoute} from ‘react-router’;
        import createBrowserHistory from ‘history/lib/createBrowserHistory’

    • 신석균

      현재 react-router v4가 설치 되어 있어서 npm install –save react-router 로 설치하면
      v4.0으로 설치되는데 4.0에서는 해당 코드가 동작하지 않는거 같습니다.
      따라서 3.0으로 재설치 하세요
      npm uninstall react-router
      npm install –save [email protected]

  • 조성근

    안녕하세요. 항상 감사합니다.
    질문이 있어서 남깁니다..
    이렇게 라우터를 내부에서 돌릴경우는 각각의 컴포넌트와 라우트를 연결해주면 되는데요..
    그런데 외부 서버 쪽에서 라우터를 설정해 주면 , [react-router] did not match any routes 라는 오류가 뜨더라구요. 그래서 react-router를 사용하면 꼭… 내부에서만 라우트 설정해줄 스 있는건가요???
    express에서 app.get(‘/test, (req,res) => {}) 예를 들어 이렇게 작성했다면…오류가 납니다.
    주소창에 /test 치면 당연히 오류나구요. react-router에 정의 되어 있지 않다고 앞서 언급했던 오류가 납니다……그래서 서버측에서 라우터로 작동하게는 못하는지 궁금합니다.
    이렇게 된 이유는 form 테그 안에서 데이터를 mysql 서버로 옮기는데 post로 접근 시키고자 express단에서 라우트 조정을 하고자 하였는데,,,절대로ㅡㅡ 오류가 나네요..

  • Time Spot

    헤더에서 발견된 경고사항이 있어 아래와같이 수정하니깐 잘 되네요..

    1. 추가할 사항
    // After (15.5)
    import PropTypes from ‘prop-types’;

    2. 변경할 사항
    Header.contextTypes = {
    router: PropTypes.object
    }

  • Hyunsung Kim

    https://uploads.disquscdn.com/images/e11fed5ab2a3a5308a5b4ab07343bb2d2f1ac28386e3287ca93bae7aa142eb68.png

    npm install –save [email protected] 한 이후에
    npm start 를 하면 이렇게 오류가 나네요;

  • Jisang Hong

    좋은 강의 감사합니다.
    오타가 좀 있네요. Post와 Posts 및 post, posts가 혼용되어 있습니다.
    처음에 Posts로 시작했으니 Posts로 통일되어야 할 것 같습니다.