본문 바로가기

CodeSoom- React 13기

[코드숨] 리액트 13기 -6강/라우팅(Routing)

라우팅

historyApiFallback

대다수의 웹 서비스를 보면 존재하지 않는 페이지에 접근할 때 index.html로 redirect 해주는 걸 볼 수 있습니다. 개발 환경에서 이러한 기능은 webpack devServer에서 설정할 수 있습니다.

module.exports = {
  //...
  devServer: {
    historyApiFallback: {
      index: 'index.html',
    },
  },
};

Window.location

window.location을 통해서 Location 객체를 얻어올 수 있습니다. Location에서 얻어올 수 있는 정보는 위의 링크를 참고하세요.

Routing

라우팅이란 출발지에서 목적지까지의 경로를 결정하는 기능입니다. 애플리케이션의 라우팅은 사용자가 태스크를 수행하기 위해 어떤 화면(view)에서 다른 화면으로 화면을 전환하는 내비게이션을 관리하기 위한 기능을 의미합니다. 일반적으로 사용자가 요청한 URL 또는 이벤트를 해석하고 새로운 페이지로 전환하기 위한 데이터를 취득하기 위해 서버에 필요 데이터를 요청하고 화면을 전환하는 위한 일련의 행위를 말합니다. 자세한 사항은 위의 문서에서 학습하실 수 있습니다.

React Router

설치

npm i react-router-dom

 


정리)

 

▼ webpack.config.js

devServer 에서 port 등 설정 가능

historyApiFallback이 index.html을 서빙해주는 효과 

 "http://localhost:8080" 가 붙은 주소에 대해서는 마치 주소가 있는 것처럼 행동할 수 있다. 해당 주소가 세션 기록에 저장된 듯이 말이다. historyApiFallback 이라는 옵션명은 historyAPI으로 주소에 대한 Fallback(대비책)을 세웠기 때문에 이 옵션명을 사용하지 않았을까 생각한다 (.https://basemenks.tistory.com/270)

devServer: {
    historyApiFallback: {
      index: 'index.html',
    },
  },

 

▼ 현재 로컬 주소 위치 확인

console.log(window.location);

화면 출력

 

console.log(window.location.pathname);

http://localhost:8080/about 일 경우 화면 출력

 

* pathname 이용해서 코드 작성시

아래와 같이 출력된다

pathname === '/' 는 locallhost:8080

 

http://localhost:8080/restaurants  와 같이 뒤에 다른 주소가 붙으면 아래와 같이 정상 출력

 

* pathname 을 객체 이용, 보여지는 컴포넌트 조절

기본 locallhost:8080 일 경우(HomePage), /restaurants 가 붙었을 경우(RestaurantsPage) 화면 출력

 

위 코드일시, 없으면 오류가 나서 화면 출력이 안되기에 아래와 같이 설정

localhost:8080/~~ 일 경우 출력

 


App.jsx (Router 연결시 간단 작성 코드)

import HomePage from './HomePage';
import AboutPage from './AboutPage';
import RestaurantsPage from './RestaurantsPage';
import NotFoundPage from './NotFoundPage';

export default function App() {
  const { location: { pathname } } = window;

  const MyComponent = {
    '/': HomePage,
    '/about': AboutPage,
    '/restaurants': RestaurantsPage,
  }[pathname] || NotFoundPage;
  // Router:입력한 주소에 따라 다른 컴포넌트가 보이도록 연결해주는 것

  return (
    <MyComponent />
  );
}

 

App.test.jsx

import { useSelector } from 'react-redux';

import { render } from '@testing-library/react';

import App from './App';

describe('App', () => {
  it('App이 랜더링된다', () => {
    useSelector.mockImplementation((selector) => selector({
      regions: [],
      categories: [],
      restaurants: [],
    }));

    render(<App />);
  });
});

 

HomePage.jsx

export default function HomePage() {
  return (
    <div>
      <h1>Home</h1>
      <ul>
        <li><a href="/about">About</a></li>
        <li><a href="/restaurants">Rrestaurants</a></li>
      </ul>
    </div>
  );
}

 

HomePage.test.jsx

import { render } from '@testing-library/react';

import HomePage from './HomePage';

describe('HomePage', () => {
  it('HomePage가 랜더링된다', () => {
    render(<HomePage />);
  });
});

나머지 컴포넌트들(AboutPage, RestaurantsPage,NotFoundPage )도 HomePage와 비슷한 형식으로 내용만 다름

 


🔸 위 코드를 더 간단하게 React Router로 이용할 수 있음

 

설치

npm i react-router-dom

 

코드숨 강의에서는 Switch로 작성하였는데 현재는 Routes 로 바뀜

▼ 이전 버전 (작성시 오류남)

import {
  BrowserRouter,
  Switch,
  Route,
} from 'react-router-dom';

import HomePage from './HomePage';
import AboutPage from './AboutPage';
// import RestaurantsPage from './RestaurantsPage';
// import NotFoundPage from './NotFoundPage';

export default function App() {
  return (
    <BrowserRouter>
      <Switch>
        <Route path="/about" component={AboutPage} />
        <Route path="/" component={HomePage} />
      </Switch>
    </BrowserRouter>
  );
}

 

▼ 현재 수정된 버전 

import {
  BrowserRouter,
  Routes,
  Route,
} from 'react-router-dom';

import HomePage from './HomePage';
import AboutPage from './AboutPage';
import RestaurantsPage from './RestaurantsPage';
import NotFoundPage from './NotFoundPage';

export default function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route exact path="/" element={<HomePage />} />
        <Route path="/about" element={<AboutPage />} />
        <Route path="/restaurants" element={<RestaurantsPage />} />
        <Route path="*" element={<NotFoundPage />} />
      </Routes>
    </BrowserRouter>
  );
}

 

exact : 정확하게 해당되는 path 만 이동되도록 설정 (path="/" 일경우 /뒤에 아무거나 와도 다 이동되는것이 아니라, /만 있을 경우 이동되도록 설정)

 

참고)

https://lefthanddeveloper.tistory.com/16

 

react-router-dom에서 Switch를 import할 수 없다고 에러가 뜰때

React로 개인 포트폴리오 사이트를 만드려고 합니다. 그런데 개발하는 도중, 계속 에러 메세지가 떠서 무슨 일이지 하고 보니, 메세지 내용이 Switch를 react-router-dom 패키지에서 import 할 수 없다 이

lefthanddeveloper.tistory.com

 


 a 태그(누르면 페이지 자체가 바뀌어 이동) 대신 Link (누르면 같은 페이지 안에서 랜더링되는 부분만 바뀜) 활용

-훨씬 더 빠르게 이동 가능하여 사용자 경험을 더 좋게 함

-크롬 Network 에서 a태그 누르면 위로 새로 생기는데, Link활용시엔 아래로 누적됨을 알 수 있음

▼ a 태그 구버전 (HomePage.jsx)

export default function HomePage() {
  return (
    <div>
      <h2>Home</h2>
      <ul>
        <li><a href="/about">About</a></li>
        <li><a href="/restaurants">Rrestaurants</a></li>
        <li><a href="/xxx">멸망의 길</a></li>
      </ul>
    </div>
  );
}

 

▼ Link 수정버전

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

export default function HomePage() {
  return (
    <div>
      <h2>Home</h2>
      <ul>
        <li><Link to="/about">About</Link></li>
        <li><Link to="/restaurants">Rrestaurants</Link></li>
        <li><Link to="/xxx">멸망의 길</Link></li>
      </ul>
    </div>
  );
}

 

▼그에 따라 HomePage.test.jsx 도 바뀜

import { MemoryRouter } from 'react-router-dom';

import { render } from '@testing-library/react';

import HomePage from './HomePage';

describe('HomePage', () => {
  it('HomePage가 랜더링된다', () => {
    render((
      <MemoryRouter>
        <HomePage />
      </MemoryRouter>
    ));
  });
});

 

 

* 다양한 주소이동을 보고 싶으면,

App.jsx 에서  <BrowserRouter>를 index.jsx 로 옮김


index.jsx

import ReactDOM from 'react-dom';

import {
  BrowserRouter,
} from 'react-router-dom';

import { Provider } from 'react-redux';

import App from './App';

import store from './store';

ReactDOM.render(
  (
    <Provider store={store}>
      <BrowserRouter>
        <App />
      </BrowserRouter>
    </Provider>
  ),
  document.getElementById('app'),
);

 

App.jsx

import {
  Routes,
  Route,
  Link,
} from 'react-router-dom';

import HomePage from './HomePage';
import AboutPage from './AboutPage';
import RestaurantsPage from './RestaurantsPage';
import NotFoundPage from './NotFoundPage';

export default function App() {
  return (
    <div>
      <header>
        <h1>
          <Link to="/">헤더 영역</Link>
        </h1>
      </header>
      <Routes>
        <Route exact path="/" element={<HomePage />} />
        <Route path="/about" element={<AboutPage />} />
        <Route path="/restaurants" element={<RestaurantsPage />} />
        <Route path="*" element={<NotFoundPage />} />
      </Routes>
    </div>

  );
}

 

App.test.jsx

import { MemoryRouter } from 'react-router-dom';

import { useDispatch, useSelector } from 'react-redux';

import { render } from '@testing-library/react';

import App from './App';

describe('App', () => {
  beforeEach(() => {
    const dispatch = jest.fn();

    useDispatch.mockImplementation(() => dispatch);

    useSelector.mockImplementation((selector) => selector({
      regions: [
        { id: 1, name: '서울' },
      ],
      categories: [],
      restaurants: [],
    }));
  });

  function renderApp({ path }) {
    return render((
      <MemoryRouter initialEntries={[path]}>
        <App />
      </MemoryRouter>
    ));
  }

  context('path / 일 경우', () => {
    it('HomePage가 랜더링된다', () => {
      const { container } = renderApp({ path: '/' });

      expect(container).toHaveTextContent('Home');
    });
  });

  context('path /about 일 경우', () => {
    it('AboutPage가 랜더링된다', () => {
      const { container } = renderApp({ path: '/about' });

      expect(container).toHaveTextContent('20명에게 추천');
    });
  });

  context('path /restaurants 일 경우', () => {
    it('RestaurantsPage가 랜더링된다', () => {
      const { container } = renderApp({ path: '/restaurants' });

      expect(container).toHaveTextContent('서울');
    });
  });

  context('잘못된 경로일 경우', () => {
    it('NotFoundPage가 랜더링된다', () => {
      const { container } = renderApp({ path: '/xxx' });

      expect(container).toHaveTextContent('Not Found');
    });
  });

  /* it('App이 랜더링된다', () => {
    useSelector.mockImplementation((selector) => selector({
      regions: [],
      categories: [],
      restaurants: [],
    }));

    render((
      <MemoryRouter>
        <App />
      </MemoryRouter>
    ));
  }); */
});