Project/[SAFU] 1st Project

7. [Client] React Routing (Oct 24, 2020, Nov 1, 2020회고)

HJChung 2020. 11. 5. 18:08

Oct 24, 2020에 Main 페이지(이 때도 아직까지 page와 컴포넌트의 경계를 헷갈리고 있었다.)에서 Login컴포넌트, Mypage컴포넌트, Signup컴포넌트 가 아래에 누적되어 나타난다는 피드백을 받았다. 

 

문제는 각각으로 라우팅을 해주지 않고, 그냥 바로 Main 컴포넌트 내에 Login컴포넌트, Mypage컴포넌트, Signup컴포넌트를 직접 작성해주었기 때문이었다.  

 

그래서 Oct 24, 2020에 대대적인(ㅋㅋ) 라우팅 작업을 진행해주었다. github.com/codestates/SAFU-client/pull/51 
그런데 큰 그림을 보지 않고 한 탓인지 계속해서 라우팅 시마다 고려해줘야 할 것들이 생겨났고, 라우팅이 정리가 되지 않고 복잡하다보니 리다이렉트 시에도 꼬이는 문제가 발생했다. 

 

그래서 Nov 1, 2020에 라우팅 정리를 해주었다. github.com/codestates/SAFU-client/pull/81

Oct 24, 2020에 한 코드(라우팅 정리 전 코드)는 <더보기> 에 넣어두고, Nov 1, 2020 라우팅 정리 후 코드를 보며 React의 라우팅과 리다이렉트에 대해 배우고 적용해본 것을 정리하고자 한다. 

1. 라우팅 설정하기

우선 우리 서비스는 SPA(Single Page Applicaiton) 이다. 

물론 SPA라고 해서 한 종류의 화면만 보여주는 서비스라는 것은 아니다. 리엑트에는 다른 주소에 따라 다른 뷰를 보여주는 라우팅이라는 기능이 내장되어 있고, 우리는 이를 사용해서 상태(state)를 변경하면서 그에 따라 다른 뷰를 랜더링한다. 

우리 서비스처럼 여러 화면으로 구성된 웹 어플리케이션을 만든다면 클라이언트 사이드에서 이루어지는 라우팅을 간단하게 해주는 react-router 라이브러리를 사용하는게 좋다. 

 

 

1) 최 상단 컴포넌트 src/App.js

위의 컴포넌트 사진을 보면 항상 <Nav/> 컴포넌트는 존재한다. 다만 Nav 컴포넌트 버튼만 상태(로그인 유무)에 따라 달라진다.

그래서 src/App.js에서 Nav는 import해와서 작성해주었고, 그 외에 어떤 주소로 왔을 때 무엇을 보여줄 지는 Nav에서 정의해주었다. 

import React from 'react';
import './App.css';
import Nav from './components/Nav';
import axios from 'axios';
import { BrowserRouter } from 'react-router-dom';

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      isLogin: false,
    };
    axios({
      method: 'get',
      url: 'http://localhost:4000/reviews',
    })
      .then((res) => {
        if (res.data[1] !== undefined && res.data[1].isLogin === true) {
          this.setState({ isLogin: true });
        }
      })
      .catch((err) => {
        console.error(err);
      });
  }
  render() {
    return (
      <div>
        <div>
          <BrowserRouter>
            <Nav isLogin={this.state.isLogin} />
          </BrowserRouter>
        </div>
      </div>
    );
  }
}
export default App;

2) 기본 Route  src/components/Main.js

주소에 아무 path도 주어지지 않았을 때('/') 기본적으로 보여주는 라우트이다. 

import React from 'react';
import '../App.css';
import { Menu, CardList } from '../pages';
import axios from 'axios';

class Main extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      isLogin: false,
      userInfo: [],
      modeChange: false,
    };
    this.handleModeChange = this.handleModeChange.bind(this);
    axios({
      method: 'get',
      url: 'http://localhost:4000/reviews',
    })
      .then((res) => {
        if (res.data[1] !== undefined && res.data[1].isLogin === true) {
          this.setState({ userInfo: res.data[0], isLogin: true });
        } else {
          this.setState({ userInfo: res.data });
        }
      })
      .catch((err) => {
        console.error(err);
      });
  }
  handleIsLoginChange() {
    this.setState({ isLogin: true });
  }
 
  render() {
    return (
      <div>
        <div className="main-body">
          <Menu />
          <CardList isLogin={this.state.isLogin} userInfo={this.state.userInfo} />
        </div>
      </div>
    );
  }
}
export default App;

3) 그 외에 path가 주어졌을 때 보여져야할 Route 준비하기

우리 서비스에는 <Signup/>, <Login/>, <Mypage/>, <Findid/>, <Fidpw/>, <CardWrite/> 등의 많은 페이지(컴포넌트 하나로만 구성되어 있어서 컴포넌트가 곧 페이지)가 있다. 

컴포넌트를 불러와서 한 파일로 보내줄 수 있도록 인덱스를 만들어준다. 

src/pages/index.js

export { default as Card } from '../components/Card';
export { default as CardEdit } from '../components/CardEdit';
export { default as CardList } from '../components/CardList';
export { default as CardWrite } from '../components/CardWrite';
export { default as Login } from '../components/Login';
export { default as Main } from '../components/Main';
export { default as Menu } from '../components/Menu';
export { default as Mypage } from '../components/Mypage';
export { default as Nav } from '../components/Nav';
export { default as SignUp } from '../components/SignUp';
export { default as Findid } from '../components/findId';
export { default as Findpw } from '../components/findPw';
export { default as Infoedit } from '../components/infoEdit';
export { default as App } from '../App';

매번 라우팅을 위해 컴포넌트를 import할 때마다 하나씩 선언해주는 것이 불편하여 src/pages/index.js에 모아둔 것이다. 
이제 사용시에는

import { SignUp, Login, Main, CardWrite, Mypage, Findid, Findpw, Infoedit } from '../pages';

이런 식으로 한꺼번에 불러올 수 있다.

4) Route 설정하기

이제 '/' path에서는 <Main/> 이, '/login'에서는 <Login/> 컴포넌트가 랜더링 되되어야 하니 라우트에 맞춰서 컴포넌트를 보여주도록 라우트가 시작되는 곳(우리는 Nav.js)에 정의해준다. 
Oct 24, 2020 코드) 

더보기
//Nav.js - (state에 따라 or 라우팅에 따라) 변경되는 부분: signup, login, mypage, logout 버튼
import React from 'react';
import axios from 'axios';
import { BrowserRouter, Link, Switch, Route } from 'react-router-dom';
import Login from './Login';
import SignUp from './SignUp';
import Mypage from './Mypage';
import Main from './Main';
import Findid from './findId';
import Findpw from './findPw';
import Infoedit from './infoEdit';
import CardWrite from './CardWrite';

axios.defaults.withCredentials = true;

class Nav extends React.Component {
  constructor(props) {
    super(props);
  }
  handleLogoutButton = () => {
    axios({
      method: 'post',
      url: 'http://localhost:4000/users/logout',
    })
      .then((res) => {
        console.log('로그아웃 완료');
      })
      .then(() => {
        window.location = '/';
      })
      .catch((err) => {
        //500(err)
        console.error(err);
      });
  };
  render() {
    if (this.props.isLogin) {
      return (
        <div className="navi">
          <h2 className="title">
            <a href="/">S*FU</a>
          </h2>
          <BrowserRouter>
            <ul className="nav-ul-login nav-ul">
              <li>
                <Link to="/Mypage">my page</Link>
              </li>
              <li>
                <button
                  onClick={(e) => {
                    e.preventDefault();
                    {
                      alert('로그아웃 하시겠습니까?');
                      this.handleLogoutButton();
                    }
                  }}
                >
                  log out
                </button>
              </li>
            </ul>
            <Switch>
              <Route path="/" exact component={Main}></Route>
              <Route path="/Mypage" component={Mypage}></Route>
              <Route path="/Infoedit" component={Infoedit}></Route>
              <Route path="/CardWrite" component={CardWrite}></Route>
            </Switch>
          </BrowserRouter>
        </div>
      );
    } else {
      return (
        <div className="navi">
          <h2 className="title">
            <a href="/">S*FU</a>
          </h2>
          <BrowserRouter>
            <ul className="nav-ul">
              <li>
                <Link to="/SignUp">sign up</Link>
              </li>
              <li>
                <Link to="/Login">log in</Link>
              </li>
            </ul>
            <Switch>
              <Route path="/" exact component={Main}></Route>
              <Route path="/SignUp" component={SignUp}></Route>
              <Route path="/Login" component={Login}></Route>
              <Route path="/Findid" component={Findid}></Route>
              <Route path="/Findpw" component={Findpw}></Route>
            </Switch>
          </BrowserRouter>
        </div>
      );
    }
  }
}
export default Nav;

 

더보기를 눌러서 이전의 코드를 보면 Nav.js에서 로그인 유무에 따라 라우팅이 다 따로 되어있어서, 라우팅이 필요할 때마다 이게 로그인 전에 해당하는 컴포넌트인지 후에 해당한는 컴포넌트인지 신경써 주어야 한다는 불편함이 있다. 그래서 로그인 전/후 바뀌어야 하는 부분만(가령 로그인을 하면 Nav의 버튼이 Mypage, logout으로 랜더링, 로그인 전은 Signup, login으로 랜더링되야 하는 부분만) {button}으로 따로 빼주었고, 라우팅은 한 곳으로 모아주었다.

그래서 src/component/Nav.js

import React from 'react';
import { Link, Route } from 'react-router-dom';
import { SignUp, Login, Main, CardWrite, Mypage, Findid, Findpw, Infoedit } from '../pages';

import axios from 'axios';

axios.defaults.withCredentials = true;

class Nav extends React.Component {
  handleLogoutButton = () => {
    axios({
      method: 'post',
      url: 'http://localhost:4000/users/logout',
    })
      .then((res) => {
        console.log('로그아웃 완료');
      })
      .then(() => {
        window.location = '/';
      })
      .catch((err) => {
        console.error(err);
      });
  };

  render() {
    let button;
    if (this.props.isLogin) {
      button = (
        <ul className="nav-ul-login nav-ul">
          <li>
            <Link to="/Mypage">my page</Link>
          </li>
          <li>
            <button
              onClick={(e) => {
                e.preventDefault();
                {
                  alert('로그아웃 하시겠습니까?');
                  this.handleLogoutButton();
                }
              }}
            >
              log out
            </button>
          </li>
        </ul>
      );
    } else {
      button = (
        <ul className="nav-ul">
          <li>
            <Link to="/SignUp">sign up</Link>
          </li>
          <li>
            <Link to="/Login">log in</Link>
          </li>
        </ul>
      );
    }

    return (
      <div className="navi">
        <h2 className="title">
          <a href="/">S*FU</a>
        </h2>
        {button}
        <Route exact path="/" component={Main}></Route>
        <Route path="/SignUp" component={SignUp}></Route>
        <Route path="/Login" component={Login}></Route>
        <Route path="/Findid" component={Findid}></Route>
        <Route path="/Findpw" component={Findpw}></Route>
        <Route path="/Mypage" component={Mypage}></Route>
        <Route path="/Infoedit" component={Infoedit}></Route>
        <Route path="/CardWrite" component={CardWrite}></Route>
      </div>
    );
  }
}
export default Nav;

※ BrowserRouter의 사용

BrowserRouter는 HTML5의 HistoryAPI를 사용하여 페이지를 새로고침 하지 않고 주소 변경할 수 있게 해준다. (즉, 페이지 변경으로 인한 깜빡임이 없다. ) 또 현재 수고에 관련된 정보를 props로 조회하거나 사용가능하다.

※ exact의 사용

위의 src/component/Nav.js에서 라우팅 설정을 해준 곳을 보면 / 경로를 사용하는 <Route> 컴포넌트에만 exact prop이 사용된 이유는, React Router의 디폴트 매칭 규칙 때문이다. 내가 사용하고 있는 react-router는 path props의 경로를 현재 브라우저의 주소창의 URL 경로 (location.pathname)와 비교를 한다. 그래서 exact를 사용하지 않으면 현재 URL 경로 값이 <Route>의 path props와 앞부분만 일치해도 같은 것으로 생각한다. 따라서 path가 /일 경우, / 뿐만 아니라 /로 시작하는 모든 URL 경로, 사실 상 가능한 모든 경우의 수의 경로와 매치가 된다. 
예를 들어) 

<div>
  <Route path="/" component={Main}/>
  <Route path="/Signup" component={Signup}/>
</div>

이렇게 exact를 쓰지 않으면 , /about 에도 / 가 있기 때문에, 매칭이 되어서 Main컴포넌트와 Signup 컴포넌트가 둘다 랜더링 되게 된다. 

즉, exact prop이 없으면 주소가 '/'인 Main 컴포넌트가 URL 경로와 상관없이 항상 보여지는 것이다. 

그래서 반드시 exact 를 써줘야한다. 그래야 주어진 경로와 정확히 맞아 떨어져야만 설정한 컴포넌트를 랜더링한다. 

※ switch의 사용

exact 뿐만 아니라 switch를 사용해서 디폴트 매칭 규칙을 막을 수 있다. 이 글을 읽고 내가 switch를 무분별하게 사용하고 있었구나.. 라고 깨달았다. 그래서 Nov 1, 2020에 정리할 때는 불필요한 switch도 다 빼주었다. 

 

출처: https://velopert.com/3417

 

2. Route 파라미터 읽기

라우트로 설정한 컴포넌트는 다음 3가지 props를 전달받는다. 

    • history: 이 객체를 통해 push하거나 replace하여 다른 경로로 이동하거나 앞 뒤 페이지로 전환 할 수 있다.
    • location: 현재 경로에 대한 정보를 지니고 있고 URL 쿼리 (/about?foo=bar 형식) 정보도 가지고있다.
    • match: 어떤 라우트에 매칭이 되었는지에 대한 정보가 있고 params 정보를 가지고있다.

 

 

3. Route 이동하기 

 1) Link 컴포넌트 

react-router의 Link 컴포넌트를 사용하면 페이지를 새로 불러오지 않고 원하는 라우트로 화면전환을 해준다. 

다른 라우트로  이동되길 원한다면, 일반 <a href...>를 사용하면 안된다. 왜냐하면, 이렇게하면 새로고침을 해버리기 때문이다. (즉, 페이지 변경으로 인한 깜빡임 x)
예를 들어 Nav 컴포넌트의 Signup 버튼을 누르면 '/Signup' 페이지로 이동하고, Login버튼을 누르면 '/Login' 페이지로 이동하고 을 한다고 할때, 아래와 같이 코드를 만들 수 있다. 

<ul className="nav-ul">
   <li>
      <Link to="/SignUp">sign up</Link>
   </li>
    <li>
       <Link to="/Login">log in</Link>
    </li>
</ul>

그 외에도 NavLink 컴포넌트를 사용하면  Link 컴포넌트와 유사하지만 중첩될수도 있는 라우트들은 exact 로 설정을 해야하고, 해당 페이지로 가면 특정 스타일 또는 클래스를 지정할 수 있다.

2) window.location과  this.props.history.push

  • window.location을 사용해서 이동하게 하면 '/' 주소의 페이지 전체를 리로드 한다. 즉  '주소 url/'에 들어가면 보이는 모든 컴포넌트를 리로드 한다.
    그래서 로그인 완료 후 window.location = '/' 를 써주게 되면 '/' 주소의 페이지에 있는 Nav 컴포넌트가 리로딩되어 버튼도 Mypage와 logout으로 바뀌면서 Main컴포에 들어있는 CardList에도 + 버튼이 보인다.
  • this.props.history.push는 '/' 주소에 연결되어 있는 컴포넌트만 리로드합니다.
    나는 Nav.js에서 <Route exact path="/" component={Main}></Route>로 '/'에 Main 컴포넌트를 라우팅 등록해주었다.
    그래서 로그인 후 this.props.history.push는 '/'를 써주게 되면 Nav 컴포의 버튼은 리로드가 되지 않아서 여전히 Signup, login이지만 Main컴포만 리로드 외어 그 안애 에 들어있는 CardList에는 + 버튼이 보이게 된다.

그래서 페이지 전체가 리로드 되어야 하는 경우인, 로그인 후, 회원탈퇴 후, 개인정보 수정 후 에 대해서만
window.location= '/' 을 사용하고. 나머지는 모두 this.props.history.push='/' 로 바꾸어 무분별해 보이는 리로딩을 제거했다.

 

window.location을 쓴 경우) 

src/component/Login.js의 로그인 이벤트 

handleLoginButton = () => {
    axios({
      method: 'post',
      url: 'http://localhost:4000/users/login',
      data: {
        useremail: this.state.useremail,
        password: this.state.password,
      },
    })
      .then((res) => {
        this.setState({ isLoginMessage: true });
      })
      .then(() => {
        window.location = '/';
      })
      .catch((err) => {
        if (err.message === 'Request failed with status code 401') {
          alert('회원 정보를 찾을 수 없습니다. email과 password를 확인해주세요.');
          this.setState({ isLoginMessage: false });
        }
      });
  };

this.props.history.push을 쓴 경우)

src/component/CardWrite.js의 카드작성 이벤트 

axios
   .post('http://localhost:4000/reviews/create', this.state)
   .then((res) => {
       this.props.history.push('/');
    })
    .catch((err) => {
         alert('failed to create');
         console.log(err);
     });

 

 

아직 부족한 점이 많지만 이번 기회에 React 라우터에 대해서 정리해 볼 수 있어서 좋았다. 

키워드는

BrowserRouter, Route, Swtich, Link, Route parameter!