[코드숨 리액트 13기] 6주차 과제 피드백

박세진·2023년 3월 3일
0

6주차에는 라우팅에 대해서 배웠다.

라우팅

historyApiFallback

webpack devserver 공식문서
대다수의 웹 서비스를 보면 존재하지 않는 페이지에 접근하려고 할 때, 404 응답 대신 index.html로 redirect 해주는 것을 개발 환경에서 webpack devServer에서 설정할 수 있다. HTML5 History API를 사용할 때, devServer.historyApiFallback을 true로 설정함으로써 사용할 수 있다.

// webpack.config.js
module.exports = {
	devServer: {
		historyApiFallback: true,
	}
}

Window.location

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

React-router-dom 5 버전과 6 버전은 차이가 크다.

https://reactrouter.com/en/v6.3.0/upgrading/v5

해당 과제를 하면서 찾아본 바뀐 것들

  • router들을 구성하는 부모 요소인 SwitchRoutes로 이름이 바뀌었다.
  • <Route childeren>에서 <Route element>로 바뀌었다.
  • 기존의 /route의 경우 React Router의 디폴트 매칭 규칙으로 인해 앞부분만 일치해도 전부 매칭되기 때문에 정확히 라우트를 일치시키고자 exact 속성을 사용했으나, v6부터 기본적으로 정확히 일치하도록 매칭 규칙이 변하여 사용하지 않는다.
    • 대신 하위 routes(다른 컴포넌트 정의됨)는 일치함을 나타내기 위해서는 *를 사용한다.
// 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

<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();
  });
});
  • 6주차 과제에 적용했을 때
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를 이용해서 주소에 대한 것을 배열로 넣어줄 수 있다.

Link를 Props로 내려서 사용하기

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>
    )
}

피드백

  • destructuring을 활용
// 처음에는 이렇게 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="/">를 추가해서 해결할 수도 있다. (참고)

profile
경험한 것을 기록

0개의 댓글