티스토리 뷰

1. React Router의 Hash Router와 Browser Router

 리액트 라우터에는 자주 사용되는 두 가지의 라우터가 있다. 바로 Hash Router와 Browser Router이다.

이전 2-2, 2-3 미션에는 Hash Router를 사용하였고 2-4 단계 미션에는 Browser Router를 사용하였다.

간단하게 두 라우터의 차이를 짚어보면 다음과 같다.

 

Hash Router

  • 주소에 해쉬(#)가 붙는다.
  • 검색 엔진이 읽지 못한다.
  • 별도의 서버 설정을 하지 않더라도 새로고침 시 오류가 발생하지 않는다.

 

Browser Router

  • History API를 사용한다.
  • 별도의 서버 설정을 하지 않으면 새로고침 시 404 에러가 발생한다.

 

 Hash Router는 검색 엔진 최적화가 지원되지 않기 때문에 사용하지 않는 것이 좋다. 그럼에도 불구하고 이전 두 개의 미션에서 Hash Router를 쓴 이유는 바로 경로 찾기 문제 때문이다.

 

 Browser Router를 별도의 서버 설정 없이 사용하면 새로고침이나 올바르지 않은 URL 접근을 리디렉션 할 때 404 에러가 발생한다. 이는 우리가 만든 앱이 SPA이기 때문에 발생한다. SPA는 기본적으로 하나의 entry point를 가진다. 도메인에 접근하면 클라이언트는 서버로부터 index.html과 JS, CSS 파일 등을 전달받고 이를 실행시킨다. 기본적으로 한번의 페이지 로드만이 존재하고 이후부터는 History API에 의해 렌더링 될 뿐이다.

 

 Brower Router를 사용할 때 도메인의 루트 주소에서의 새로고침은 전혀 문제되지 않지만 다른 path에서의 새로고침이나 리디렉션이 불가능한 이유는 바로 여기에 있다. 우리가 다른 path에서 새로고침을 하게 되면 브라우저는 도메인 주소를 보고 우리의 서버를 찾아간 뒤 path를 보고 path 명과 같은 디렉토리를 찾는다. 하지만 우리의 앱은 서버사이드 렌더링이 아니기에 path명과 같은 폴더를 가지지 않을 뿐더러 그 안에 index.html이 존재하지도 않는다. 결국, 브라우저는 렌더링에 필요한 파일을 서버로부터 찾지 못했기 때문에 404 에러를 띄운다.

 

 그렇다면 Hash Router는 어떻게 새로고침이나 리디렉션의 문제로부터 자유로울 수 있는 것일까? 그것은 바로 브라우저가 서버에 요청을 보낼 때, # 이전의 도메인 주소로 요청을 보내기 때문이다. Hash Router는 # 이후에 path가 오도록 만든다. Hash Router를 사용하게 되면 다음과 같은 URL 주소를 가진다.

 

www.your-domain.com/#/path/1

 

 따라서 Hash Router를 사용하면 # 뒤에 path가 오기 때문에 새로고침과 리디렉션을 해도 모두 올바른 entry point로 요청이 갈 수 있게 된다.

 

 그렇다면 Browser Router에서는 새로고침과 리디렉션 404 에러를 해결할 수 있는 방법이 없는 것일까? 그것은 아니다. 앞서 언급했듯이 서버에서 별도의 설정을 하면 된다. '호곡, 서버 설정이라니 너무 어려운 것 아닐까?' 라고 생각할 수 있지만 의외로 간단하게 해결이 가능하다.

 

 정적 페이지 배포에 자주 쓰이는 netlify를 예시로 어떻게 서버 설정을 할 수 있는지 알아보자.

 리액트 프로젝트의 public 폴더에 'netlify.toml'이라는 파일을 생성한 뒤 다음과 같은 코드를 입력한다.

 

[[redirects]]   
  from = "/*"   
  to = "/index.html"   
  status = 200

 

 이렇게 작성하면 netlify 서버에서 다른 path로 들어온 모든 요청에 대해 index.html로 리디렉션 해준다. 따라서 Browser Router를 사용하더라도 새로고침과 리디렉션 시 우리가 원하는 entry point로 진입하고 React Router가 path를 읽어드려 우리가 원하던 페이지를 렌더링 할 수 있게 된다.

 

 

2. Redux Toolkit / Redux Saga / Redux Saga Test Plan

Redux Toolkit

 리덕스는 단 한개의 액션을 생성하는 데도 많은 양의 코드를 필요로 한다. 이러한 문제를 해결하고자 리덕스 툴킷이라는 공식 툴이 등장했다. 

 

 리덕스 툴킷을 쓰면서 확실히 작성해야하는 코드 양이 줄어듬을 느꼈다. 특히 툴킷의 'createSlice'를 이용하면 액션과 액션 생성 함수 그리고 리듀서까지 한번에 작성할 수 있어 편리했다. createSlice를 쓰면 Ducks 패턴을 따르게 되어 리덕스 관련 코드를 한 곳에서 관리할 수 있어 좋았다. 또한 툴킷 내부에서 immer를 사용하여 불변성을 보장해주기 때문에 객체를 관리하기 훨씬 수월했다.

 

Redux Saga

 Redux Saga는 Redux Thunk와 같이 자주 사용되는 비동기 처리를 위한 Redux 미들웨어이다. Saga는 ES6에서 도입된 제네레이터(generator) 함수를 사용한다는 특징이 있다. 제네레이터는 코드 블록의 실행을 일시 중지했다가 필요한 시점에 재개할 수 있는 특수한 함수이다. 일반 함수를 호출하면 제어권이 함수에게 넘어가고 함수 코드를 실행한다. 즉, 함수 호출자는 함수를 호출한 이후 함수 실행을 제어할 수 없다. 반면 제네레이터 함수는 함수 실행을 함수 호출자가 제어할 수 있다. 다시 말해, 함수 호출자가 함수 실행을 일시 중지시키거나 재개시킬 수 있다.

 

 이번 미션을 하면서 Saga가 Thunk 보다 훨씬 편리하고 유용하다는 느낌은 받지 못했다. 오히려 Saga만을 위한 액션과 리듀서를 생성해야한다는 점이 귀찮게 느껴지기도 했다. 특히 Saga와 타입스크립트를 같이 사용하는데에 꽤나 애를 먹었다. 하지만 Saga는 Thunk가 가지지 못하는 명확한 장점들이 있었다.

 

 먼저, 'TakeLatest'와 'TakeEvery'를 통해 비동기 처리를 효율적으로 관리할 수 있다. 'TakeLatest' 사용하면 기존 요청을 모두 취소하고 가장 마지막에 들어온 요청만 처리하게 만든다.

 

 두번째는 특정 액션이 디스패치 될 때를 구독할 수 있다는 것이다. 하나의 사가를 여러개의 액션에서 활용할 수 있게 된다.

 

 마지막으로 Saga만을 위한 테스트를 진행할 수 있다. Redux Saga Test Plan을 이용하면 Saga의 동작과 그에 따른 리듀서의 상태를 테스트 할 수 있어 테스트 단위를 작게 가져갈 수 있었다.

 

export function* addStationSaga(action: PayloadAction<AddStationPayload>) {
  yield put(pending());
  const response: HttpResponse<Station> = yield call(stationAPI.addStation, action.payload);

  if (response.error) {
    yield put(error(response.error));
    return;
  }

  const stations: Station[] = yield select(selectStations);
  yield put(setStations([Object.assign(response.data, { lines: [] }), ...stations]));
}
// addStationSaga test
it('지하철 역 목록을 성공적으로 추가한다.', async () => {
  return expectSaga(addStationSaga, { type: addStationAsync.type, payload: newStation.name })
    .withReducer(stationReducer)
    .put(pending())
    .provide([
      [call(stationAPI.addStation, newStation.name), { data: newStation }],
      [select(selectStations), stationList],
    ])
    .put(setStations([Object.assign(newStation, { lines: [] }), ...stationList]))
    .hasFinalState({ stations: [Object.assign(newStation, { lines: [] }), ...stationList], error: '' })
    .run();
});

 

 

3. px보다는 rem

 웹 퍼블리싱을 해본 경험이 거의 전무하다보니 이전까지는 주먹구구식으로 정적 페이지를 만들었다. 내가 보고 있는 모니터에서만 가장 자연스러운 형태로 작업을 진행했고 그 결과 다른 디바이스에서는 화면이 굉장히 어색한 형태로 나타났다. 이번 미션도 기능 구현에만도 많은 시간이 필요해서 화면을 반응형으로 만들지는 못했다. 다만, 최소한 px 대신 rem을 사용하여 조금이라도 반응형처럼 만들어보았다. rem을 사용하니 미디어 쿼리로 html의 font-size를 조절해주는 것만으로도 레이아웃이 자연스럽게 변경됐다. 앞으로는 px이 꼭 필요한 경우를 제외하고는 rem을 사용하도록 해야겠다.

 

 

4. 디렉토리명에 @ 붙이기

 인치가 알려준 꿀팁으로 디렉토리나 파일 명에 '@'를 붙이면 디렉토리 가장 상단에 위치시킬 수 있다!