리엑트 라우터로 SPA개발하기

Posted by yunki kim on July 8, 2021

  기존의 웹 페이지는 여러개의 html파일로 이루어져 있으며 다른 페이지로 이동할 때 마다 매번 새로운 html파일을 받아오고 페이지 로딩마다 서버에서 리소스를 전달받아 해석한 뒤 화면에 보여주었다.

  하지만 이런 방식은 제공해야할 정보가 많은 요즘 웹에는 적합하지 않다. 서버측에서 모든 정보를 준비한다면 트래픽 증가, 사용자 증가로 인한 서버 부하가 발생할 수 있다. 물론 캐싱, 압축을 통해 어느정도 해결이 가능하지만 사용자와 인터렉션이 자주 발생하는 모던 웹 어플리케이션에는 적당하지 않다. 

  따라서 리액트 같은 라이브러리 또는 프레임워크를 통해 뷰 렌더링을 사용자의 브라우저가 담당하도록 하고, 애플리케이션을 브라우저에 불러와서 실행기킨 후에 사용자와의 인터렉션이 발생하면 필요한 부분만 js를 사용해 업데이트를 해준다. 이를 SPA(Single Page Application)이라 하며 SPA는 한개의 페이지로 이루어진 애플리케이션이라는 의미이다.

SPA

SPA는 비록 사용자에게 제공되는 페이지는 하나이지만 페이지에 로딩된 js와 url에 따라 다양한 화면을 보여준다.

  다른 주소에 다른 화면을 보여주는 것을 라우팅이라 하며 리액트 라이브러리에 이 기능이 내장되어 있지는 않지만 react-router, reach-router, Next.js등을 활용해 구현할 수 있다. 여기서는 react-router를 사용한다. 

 

SPA의 단점

  SPA는 페이지 로딩 시 사용자가 실제로 방문하지 않을 수 도 있는 페이지의 스크립트를 불러오기 때문에 js파일이 너무 커진다. 하지만 이는 code splitting을 통해 라우트별로 파일들을 나누어서 트래픽과 로딩 속도를 개선할 수 있다.

  Js를 실행하지 않는 일반 크롤러에선는 페이지의 정보를 제대로 수행하지 못한다. 

  Js가 실행되기 전까지는 페이지가 비어있기 때문에 짧은 시간 동안 휜 페이지가 나타날 수 있다는 단점도 있다.

 

라우터 적용

  우선 react-router-dom을 설치해야 한다. 그 후 src/index.js파일에서 react-router-dom에 내장된 BrowserRouter라는 컴포넌트를 사용해 감싸야 한다. BrowserRouter 컴포넌트는 웹 애플리케이션에 HTML5의 History API를 사용해 페이지를 새로고침하지 않고도 주소를 변경하고, 현재 주소에 관련된 정보를 props로 쉽게 조회하거나 사용할 수 있게 해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import {BrowserRouter} from 'react-router-dom';
 
ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById('root')
);
 
reportWebVitals();
 
cs

이제 다음과 같은 두개의 컴포넌트를 만들고 이 컴포넌트를 주소에 따라 보여주게 하면 된다.

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
//Home.js
import React from 'react';
 
const Home = () => {
    return (
        <div>
            <h1>Home</h1>
            <p>Home page, first page</p>
        </div>
    );
}
 
export default Home;
 
//About.js
import React from 'react';
 
const About = () => {
    return (
        <div>
            <h1>introduction</h1>
            <p>I made this simple project to practice basic react</p>
        </div>
    );
}
 
export default About;
cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//App.js
import logo from './logo.svg';
import './App.css';
import {Route} from 'react-router-dom';
import Home from './Home';
import About from './About'
 
function App() {
  return (
      <div>
        <Route path={"/"} component={Home} exact={true}/>
        <Route path={"/about"} component={About}/>
      </div>
  );
}
 
export default App;
 
cs

  위와 같이 코드를 작성하면 url을 입력 했을 때 해당 컴포넌트를 보여준다. 여기서 App.js의 첫번째 Route를 보면 exact가 있다. 이게 있어야 주어진 경로와 설정한 컴포넌트가 정확히 맞아 떨어져야만 컴포넌트를 보여줄 수 있게 해준다. 만약 exact를 설정하지 않으면 위 예시의 경우 /about이라는 경로에 /도 포함되 있으프로 Home, About이 두개의 컴포넌트 모두를 보여주게 된다.

Link component를 이용한 다른 주소로 이동하기

  위예제는 반드시 직접 url을 타이핑 해야 다른 컴포넌트를 볼 수 있다. 만약 특정한 컴포넌트를 클릭함으로써 다른 주소로 이동하고 싶다면 리엑트에서는 Link 컴포넌트를 사용해야 한다. 기존의 웹 애플리케이션의 경우 a태그를 사용했지만 a태그의 경우 페이지를 전환하는 과정에서 페이지를 새로 불러오기 때문에 기존에 가지고 있는 상태를 모두 날리게 된다. 그에 반해 Link 컴포넌트의 경우 비록 a태그로 이루어진 컴포넌트이지만 페이지 전환을 방지하는 기능이 있다. Link는 페이지를 새로 불러오지 않고 HTML5 History API를 사용해 페이지의 주소만을 변경한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//App.js
import logo from './logo.svg';
import './App.css';
import {Route, Link} from 'react-router-dom';
import Home from './Home';
import About from './About'
 
function App() {
  return (
      <div>
          <ul>
              <li><Link to={"/"}>home</Link></li>
              <li><Link to={"/about"}>about</Link></li>
          </ul>
          //...
      </div>
  );
}
 
export default App;
 
cs

Route하나에 여러개의 path지정하기

  리엑트 라우터 V5이전까지는 첫번째 예제 처럼, 이후부터는 두번째 예제처럼 하면 된다.

1
2
3
4
5
6
7
//ex1
<Route path={"/about"} component={About}/>
<Route path={"/info"} component={About}/>
 
//ex2
<Route path={["/about""/info"]} component={About}/>
 
cs

URL 파라미터

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
//App.js
//...
import Profile from './Profile';
 
function App() {
  return (
      <div>
          <ul>
              //...
              <li><Link to={"/profile/velopert"}>velopert profile</Link></li>
              <li><Link to={"/profile/iskull"}>iskull profile</Link></li>
              <li><Link to={"/profile/1"}> profile</Link></li>
          </ul>
          <div>
              //...
              <Route path={"/profile/:username"} component={Profile}/>
          </div>
      </div>
  );
}
 
export default App;
 
//Profile.js
import React from 'react';
 
const data = {
    velopert: {
        name'김민준',
        description: '리액트를 좋아하는 개발자',
    },
    iskull: {
        name'김윤기',
        description: '코딩 좀 잘하고 싶다',
    }
};
 
//URL파라미터를 사용할 때는 라우트로 사용되는 컴포넌트에서 받아오는
//match라는 객체 안의 params값을 참조한다.
const Profile = ({match}) => {
    //match.params에 있는 username값을 조회
    //username은 해당 Route컴포넌트에 존재.
    const {username} = match.params;
    const profile = data[username];
    if(!profile) {
        return <div>user does not exist</div>
    }
    return (
        <div>
            <h3>{username}({profile.name})</h3>
            <p>{profile.description}</p>
        </div>
    );
};
 
export default Profile;
cs

  match 객체

    match객체는 <Route path>가 어떻게 URL에 매치 됬는지에 대한 정보를 담고 있다. match 객체는 다음과 같은 프로퍼티들을 가지고 있다.

      1. params - url로 부터 파싱된 경로의 유동적인 값에 대한 key-value쌍을 가지고 있다(object)

      2. isExact - URL전체가 매칭 되면 true(boolean)

      3. path - match에 사용된 경로 패턴. nested <Route>를 사용하는데 용이하다.(string)

      4. url - URL중 매칭된 부분, nested <Link>를 사용하는데 용이하다.

 

URL 쿼리

 쿼리는 location 객체에 들어 있는 search에서 조회할 수 있다. location객체는 현재 페이지에 대한 정보를 가지고 있다. location은 다음과 같은 값을 가지고 있다.

    1. pathname - 현재 페이지의 경로명(string)

    2. search - 현재 페이지의 query string(string)

    3. hash - 현재 페이지의 hash(string)

  위에서 볼 수 있듯이 모든 값이 string으로 되어 있다. 또 한 쿼리는 문자열에 여러 값을 설정해 줄 수 있기 때문에 search에서 특정 값을 읽어 오기 위해서는 객체로 변환해야 한다. 이 동작을 하기 위해서는 qs라는 쿼리 문자열을 객체로 변환해주는 라이브러리를 설치해야 한다. 쿼리 문자열을 객체로 파싱하는 과정에서의 결과 값은 항상 문자열이다. 따라서 숫자로 파싱을 해야 한다면 parseInt()함수를 사용해야 한다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//About.js
import React from 'react';
import qs from 'qs';
 
const About = ({location}) => {
    const query = qs.parse(location.search, {
        ignoreQueryPrefix: true,//문자열의 ?생략
    })
    const showDetail = query.detail === 'true' //쿼리의 파싱 결과값은 string이다.
    return (
        <div>
            <h1>introduction</h1>
            <p>I made this simple project to practice basic react</p>
            {showDetail && <p>You set detail as true</p>}
        </div>
    );
}
 
export default About;
cs

서브 라우트

  라우트 내부에 또 다름 라우트를 정의하는 것을 의미한다. 

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//App.js
import logo from './logo.svg';
import './App.css';
import {Route, Link} from 'react-router-dom';
import Home from './Home';
import About from './About'
import Profiles from './Profiles';
 
function App() {
  return (
      <div>
          <ul>
             //...
              <li><Link to={"/profiles"}>profile</Link></li>
          </ul>
          <div>
              //...
              <Route path={"/profiles"} component={Profiles}/>
          </div>
      </div>
  );
}
 
export default App;
 
 
//Profiles.js
import React from 'react';
import {Link, Route} from 'react-router-dom';
import Profile from './Profile';
 
const Profiles = () => {
    return (
        <div>
            <h3>User list:</h3>
            <ul>
                <li><Link to={"/profiles/velopert"}>velopert</Link></li>
                <li><Link to={"/profiles/iskull"}>iskull</Link></li>
            </ul>
            {/*props설정시 값을 생략하면 자동을 true가 할당된다.*/}
            {/*Route에 render props를 넣어 컴포넌트 자체를 전달하는 것이 아닌*/}
            {/*보여주고 싶은 JSX를 넣어줄 수 있다.*/}
            {/*컴포넌트를 따로 만들기 애매하거나 컴포넌트에 props를 별도로 넣고 싶을때 사용한다.*/}
            <Route
                path={"/profiles"}
                exact
                render={() => <div>Select user</div>}
            />
            <Route path={"/profiles/:username"} component={Profile}/>
        </div>
    )
}
 
export default Profiles;
 
cs

History 객체

  history객체는 라우트로 사용된 컴포넌트에 match, location과 함께 전달되는 props중 하나로, 이 객체를 통해 컴포넌트 내에 구현하는 메서드에서 라우터 API를 호출할 수 있다. 특정 버튼을 눌렀을때 뒤로 가거나, 로그인 후 화면 전환, 다른 페이지로 이탈 방지 등에 사용된다.

  history 객체는 다음과 같은 프로퍼티와 메서드를 가지고 있다.

    1. length - history stack에 있는 엔트리 갯수(number)

    2. action - 가장 최근에 진행된 액션(PUSH, REPLACE, POP)(string)

    3. location - 가장 최근의 location 객체(object)

    4. push(path, [state]) - history stack에 새로운 엔트리를 push한다(function)

    5. replace(path, [state]) - history stack에 있는 가장 최근의 엔트리를 대체한다(function)

    6. go(n) - history stack의 포인터를 n개의 엔트리 만큼 이동시킨다(function)

    7. goBack() - -1만큼 이동(function)

    8. goForward() - +1만큼 이동(function)

    9. block(propmt) - 현재 페이지에서 다른 페이지로 이동하는 것을 막는다(function)

  물론  history.location을 통해 location객체에 접근할 수 있지만 history 객체는 mutable이다. 따라서 location객체에 접근해야 한다면 location객체에 직접 접근하는 것이 좋다. 

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//HistorySample.js
import React, {Component} from 'react';
 
class HistorySample extends Component {
    handleGoBack = () => {
        this.props.history.goBack();
    }
 
    handleGoHome = () => {
        this.props.history.push('/');
    }
 
    componentDidMount() {
        this.unblock = this.props.history.block('Do you want to leave');
    }
 
    componentWillUnmount() {
        if(this.unblock) {
            this.unblock();
        }
    }
 
    render() {
        return(
            <div>
                <button onClick={this.handleGoBack}>back</button>
                <button onClick={this.handleGoHome}>home</button>
            </div>
        );
    }
}
 
export default HistorySample;
 
//App.js
//...
import HistorySample from "./HistorySample";
 
function App() {
  return (
      <div>
          <ul>
              //...
              <li><Link to={"/history"}>History example</Link></li>
          </ul>
          <div>
              //...
              <Route path={"/history"} component={HistorySample}/>
          </div>
      </div>
  );
}
 
export default App;
 
cs

/history에서 버튼을 누르면 다음과 같이 작동한다

withRouter

 withRouter는 HoC(Higher-order Component)이다. 라우트로 사용된 컴포넌트가 아니어도 math, location, historoy객체를 접근할 수 있게 해준다.

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
39
40
//WithRouter.js
import React from 'react';
import {withRouter} from 'react-router-dom';
const WithRouterSample = ({location, match, history}) => {
    return (
        <div>
            <h4>location</h4>
            {/*stringfy에서 두번째 인자로 null, 세번째 인자로 2를 주면 들여쓰기가 적용된 상태로 문자열이 만들어 진다*/}
            <textarea
                value={JSON.stringify(locationnull2)}
                rows={7}
                readOnly={true}
            />
            <h4>match</h4>
            <textarea
                value={JSON.stringify(match, null2)}
                rows={7}
                readOnly={true}
            />
            <button onClick={() => history.push('/')}>home</button>
        </div>
    );
};
//withRouter를 사용할 때에는 컴포넌트를 내보낼때 함수로 감싸야 한다.
export default withRouter(WithRouterSample);
 
//profiles.js
//...
import WithRouterSample from "./WithRouter";
 
const Profiles = () => {
    return (
        <div>
            //...
            <WithRouterSample/>
        </div>
    )
}
 
export default Profiles;
cs

  여기서 march의 params가 비어 있다. 이는 withRouter를 사용할때는 match가 현재 자신을 보여주고 있는 라우트 컴포넌트를 기준으로 전달되기 때문이다. 따라서 path="/profiles"이므로 username 파라미터를 읽어오지 못한다. 이를 해결하기 위해서는 WithRouter컴포넌트를 profiles가 아닌 profile에 놓으면 된다.

Switch

 Switch컴포넌트를 사용하면 수 많은 Route컴포넌트 중 일치하는 단 하나의 라우트만을 렌더링 한다. Switch를 사용하면 모든 규칙과 일치하지 않을 때 Not found페이지를 보여줄 수 있다.

 

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
//App.js
import
 logo from './logo.svg';
import './App.css';
import {Route, Link, Switch} from 'react-router-dom';
import Home from './Home';
import About from './About'
import Profiles from './Profiles';
import HistorySample from "./HistorySample"
 
function App() {
  return (
      <div>
          <ul>
              //...
          <Switch>
              <Route path={"/"} component={Home} exact={true}/>
              <Route path={["/about""/info"]} component={About}/>
              <Route path={"/profiles"} component={Profiles}/>
              <Route path={"/history"} component={HistorySample}/>
              {/*path를 따로 정의하지 않으면 모든 상황에 렌더링 된다*/}
              <Route
                  render={({location}) => (
                      <div>
                          <h2>Page not found</h2>
                          <p>{location.pathname}</p>
                      </div>
                  )}
              />
          </Switch>
      </div>
  );
}
 
export default App;
 
cs

 

NavLink

  NavLink는 Link와 비슷하지만 Link에서 사용하는 경로가 일치할 때 특정 스타일 또는 css클래스를 적용할 수 있다.

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
import React from 'react';
import {NavLink, Route} from 'react-router-dom';
import Profile from './Profile';
 
const Profiles = () => {
    const activeStyle = {
        background: 'black',
        color: 'white',
    }
    return (
        <div>
            <h3>User list:</h3>
            <ul>
                <li><NavLink activeStyle={activeStyle} to={"/profiles/velopert"}>
                    velopert
                </NavLink></li>
                <li><NavLink activeStyle={activeStyle} to={"/profiles/iskull"}>
                    iskull
                </NavLink></li>
            </ul>
            //...
        </div>
    )
}
 
export default Profiles;
cs