6주차에는 라우팅에 대해서 배웠다.
webpack devserver 공식문서
대다수의 웹 서비스를 보면 존재하지 않는 페이지에 접근하려고 할 때, 404 응답 대신 index.html로 redirect 해주는 것을 개발 환경에서 webpack devServer에서 설정할 수 있다. HTML5 History API를 사용할 때, devServer.historyApiFallback
을 true로 설정함으로써 사용할 수 있다.
// webpack.config.js
module.exports = {
devServer: {
historyApiFallback: true,
}
}
window.location
을 통해서 Location 객체를 얻어올 수 있다. Location 인터페이스는 객체가 연결된 장소(URL)을 표현한다. Location 인터페이스에 변경을 가하면 연결된 객체에도 반영되는데, Document와 Window 인터페이스가 이런 Location을 가지고 있다.
import RestaurantsPage from './RestaurantsPage';
import HomePage from './HomePage';
export default function App() {
const {
location: { pathname },
} = window;
const MyComponent =
{
'/': HomePage,
'/restaurants': RestaurantsPage,
'/about': AboutPage,
}[pathname] || NotFoundPage;
return <MyComponent />;
}
React-router-dom 5 버전과 6 버전은 차이가 크다.
https://reactrouter.com/en/v6.3.0/upgrading/v5
Switch
는 Routes
로 이름이 바뀌었다.<Route childeren>
에서 <Route element>
로 바뀌었다./route
의 경우 React Router의 디폴트 매칭 규칙으로 인해 앞부분만 일치해도 전부 매칭되기 때문에 정확히 라우트를 일치시키고자 exact 속성을 사용했으나, v6부터 기본적으로 정확히 일치하도록 매칭 규칙이 변하여 사용하지 않는다.*
를 사용한다.// index.js
<BrowserRouter>
<App />
</BrowserRouter>
// App.jsx
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/restaurants" element={<RestaurantsPage />} />
<Route path="/restaurants/:id" element={<RestaurantDetail />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
Link
태그를 테스트에서 처리해주는 방법은 2가지가 있다.
1. MemoryRouter를 이용해서 감싸주기
2. Link를 Props로 받아와서 사용하기
<MemoryRouter>
는 내부적으로 배열에 위치를 저장한다. <BrowserHistory>
,<HashHistory>
와 다르게 브라우저의 히스토리 스택 같이 외부 소스에 얽매이지 않는다. 따라서 테스트처럼 히스토리 스택을 완벽하게 제어해야 하는 시나리오에 이상적이다.
<MemoryRouter initialEntries >
// default: ["/"]
// 사용방법 예시
import * as React from "react";
import { create } from "react-test-renderer";
import {
MemoryRouter,
Routes,
Route,
} from "react-router-dom";
describe("My app", () => {
it("renders correctly", () => {
let renderer = create(
<MemoryRouter initialEntries={["/users/mjackson"]}>
<Routes>
<Route path="users" element={<Users />}>
<Route path=":id" element={<UserProfile />} />
</Route>
</Routes>
</MemoryRouter>
);
expect(renderer.toJSON()).toMatchSnapshot();
});
});
describe('App', () => {
function renderApp({ path }) {
return render((
<MemoryRouter initialEntries={[path]}>
<App />
</MemoryRouter>
));
}
context('with path "/"', () => {
it('renders HomePage', () => {
const { container } = renderApp({ path: '/' });
expect(container).toHaveTextContent('Home');
});
});
context('with path "/about"', () => {
it('renders AboutPage', () => {
const { container } = renderApp({ path: '/about' });
expect(container).toHaveTextContent('EATGO?');
});
});
});
initialEntries
를 이용해서 주소에 대한 것을 배열로 넣어줄 수 있다. MemoryRouter를 알 필요가 없을 경우에는 Link를 Props로 받아와서 사용하는 것이다.
// RestaurantsContainer.jsx
export default function RestaurantsContainer({ Link }) {
const restaurants = useSelector(get('restaurants'));
return (
<ul>
{restaurants.map((restaurant) => (
<li key={restaurant.id}>
<Link to="/restaurants/1">
{restaurant.name}
</Link>
</li>
))}
</ul>
);
}
// RestaurantsContainer.test.jsx
describe('RestaurantsContainer', () => {
useSelector.mockImplementation((selector) => selector({ restaurants }));
const Link = ({ children }) => (<>{children}</>);
it('renders RestaurantsContainer', () => {
const { getByText } = render((<RestaurantsContainer Link={Link} />));
expect(getByText(/김밥천국/)).not.toBeNull();
});
});
// RestaurantsPage.jsx
import { Link } from "react-router-dom"
import RegionsContainer from "./RegionsContainer"
import CategoriesContainer from "./CategoriesContainer"
import RestaurantsContainer from "./RestaurantsContainer"
export default function RestaurantsPage() {
return (
<div>
<RegionsContainer />
<CategoriesContainer />
<RestaurantsContainer Link={Link} />
</div>
)
}
// 처음에는 이렇게 id.id라고 해서 사용했다.
const id = useParams();
useEffect(() => {
dispatch(loadRestaurantDetail(id.id));
}, [])
// destructuring을 활용하면 이렇게 깔끔하게 작성할 수 있다.
const {id} = useParams();
useEffect(() => {
dispatch(loadRestaurantDetail(id));
}, [])
length
를 활용// 처음에 빈 배열과 같을 경우로 조건을 설정했다
if (restaurantDetail === []) {
return null;
}
// length를 활용해서
if (restaurantDetail.length === 0) {
return null;
}
// restaurantDetail의 초기값이 초기에는 빈 배열로 할당
// 빈 배열로 할당하면 안된다는 것을 알았고, null 값으로 수정
if (restaurantDetail === null) {
return null;
}
// 또는 이렇게 사용자에게 친화적인 UI를 넣어주는 게 좋다.
if (!restaurantDetail) {
return <p>loading...</p>
}
_
구분자로 구별// RestaurantDetail.test.jsx
import { useDispatch, useSelector } from 'react-redux';
import RestaurantDetail from './RestaurantDetail';
import RESTAURANTDETAIL from '../fixtures/restaurantDetail';
// 두 단어가 합쳐진 경우 _ 구분자로 구별해주기
// 수정하면? 이렇게!
import RESTAURANT_DETAIL from '../fixtures/restaurantDetail';
http://localhost:8080/restaurant/1
로 들어갔을 때, 에러가 뜨는 것을 확인할 수 있었다. restaurant
뒤에 /1
만 붙으면 에러가 뜨길래 처음에는 url에 저주가 걸렸나 생각했다.index.html
파일에 스크립트 경로를 변경해주었더니 문제를 해결할 수 있었다.main.js
→ /main.js
index.html
파일의 head
에 <base href="/">
를 추가해서 해결할 수도 있다. (참고)