Vanilla React로 Router 구현하기 - History API, popState, window.location

지은·2023년 7월 6일
0

🚂 토이 프로젝트

목록 보기
8/10

알아야 할 개념

1. window.history

: History 객체에 접근할 수 있는 읽기 전용 속성

  • History 객체는 브라우저의 세션 기록을 조작하고 관리할 때 사용한다.
  • History API를 사용해 브라우저 히스토리를 직접 조작하고, 페이지의 상태를 변경하거나 URL을 업데이트할 수 있다.
history.back();    // 뒤로 가기
history.forward(); // 앞으로 가기
history.go(-2);    // 뒤로 2번 가기

history.pushState()

: 세션 기록 스택(history)에 상태를 추가하는 메소드
현재 페이지를 다시 로드하거나 이동하지 않고도 브라우저의 URL을 변경할 수 있다.
history.pushState(state, title[, url])

  • state : 새로운 세션 기록 항목에 연결할 상태 객체
    사용자가 새로운 상태로 이동할 때마다 popstate 이벤트가 발생하는데, 이 때 이벤트 객체의 state 속성에 해당 상태를 복제본이 담겨있다.
  • title : 상태에 대한 짧은 제목
    (Safari를 제외한 모든 브라우저가 title 매개변수를 무시하지만, 미래에 쓰일 수도 있음)
  • url(optional) : 새로운 세션 기록 항목의 URL
const state = { 'page_id': 1, 'user_id': 5 };
const title = '';
const url = 'hello-world.html';

history.pushState(state, title, url);

history.replaceState()

세션 기록 스택(history)의 제일 최근 항목을 주어진 데이터, 제목, URL로 대체하는 메소드
history.replaceState(stateObj, title[, url])


2. popstate 이벤트

: 사용자가 브라우저의 히스토리를 탐색(history.back(), history.forward(), history.go())하거나 앞으로/뒤로 가기 버튼을 클릭할 때 발생하는 이벤트

  • 뒤로 가기 및 앞으로 가기 버튼과 같은 브라우저 내장 네비게이션 요소와 연동하여 페이지의 상태를 업데이트하고 관리하는 데 사용된다.
  • 이벤트 객체를 메소드로 받는다.
  • 주로 SPA를 구현할 때 History API와 함께 사용된다.
window.addEventListener('popstate', (e) => {
  // 히스토리 상태 변경에 대한 처리
})

3. window.location

Location 객체에 접근할 수 있는 읽기 전용 속성

location.assign('https://www.naver.com'); // 네이버로 이동
location = 'https://www.naver.com'; // 네이버로 이동
location.reload(true); // 서버로부터 현재 페이지 강제로 다시 로드

window.location.pathname

: /와 URL의 경로를 담은 문자열을 반환한다.

// e.g. location = 'https://www.naver.com/abc/1234'
console.log(location.pathname); // '/abc/1234'


React에서 History API를 이용해 SPA 구현하기

히스토리 객체에 새로운 항목 추가하기

네비게이션 바에 있는 버튼을 클릭하면 updateURL() 함수가 실행되고,
해당 함수 내에서 pushState() 메소드를 이용해 히스토리 객체에 새로운 항목을 추가한다.
전달할 상태와 타이틀이 없기 때문에 state는 null, title은 빈 문자열을 주었고, URL로는 원하는 경로를 전달한다.
이렇게 하고 버튼을 클릭하면 주소창의 경로가 전달한 path로 업데이트되는 것을 확인할 수 있다.

function App() {
  const updateURL = (path) => {
    window.history.pushState(null, '', path);
  }

  return (
    <>
      <nav>
        <button onClick={() => updateURL('/')}>Home으로 가기</button>
        <button onClick={() => updateURL('/about')}>About으로 가기</button>
      </nav>
    </>
  );
}

아직까지는 경로의 변화에 따라 화면은 업데이트되지는 않는다.
이제 이 경로를 받아와서 경로별로 원하는 컴포넌트를 렌더링하도록 구현해보자.


location.pathname 속성을 사용해 경로별로 컴포넌트 렌더링하기

location 객체를 활용하면 현재 페이지의 URL 정보에 접근할 수 있다.
그 중에서도 pathname 속성을 이용해 경로를 받아올 수 있다.

먼저 현재 경로를 저장할 currentPath 상태를 만들어준다.
그리고 updateURL() 함수가 실행될 때 currentPath 상태를 업데이트하도록 해준다.

이렇게 하면 About 버튼을 클릭했을 때 → 주소창이 .../about으로 업데이트되고, → location.pathname 속성으로 이 값을 받아와 상태에 /about이라는 텍스트가 저장된다.

function App() {
  const [currentPath, setCurrentPath] = useState(window.location.pathname);

  const updateURL = (path) => {
    window.history.pushState(null, "", path);
    setCurrentPath(window.location.pathname);
  };

  return (
    <>
      <nav>
        <button onClick={() => updateURL("/")}>Home으로 가기</button>
        <button onClick={() => updateURL("/about")}>About으로 가기</button>
      </nav>

      <Route path="/" component={<Root />} />
      <Route path="/about" component={<About />} />
    </>
  );
}

const Route = ({ currentPath, path, children }) => {
  return window.location.pathname === path ? component : null;
  // pathname이 /이라면 <Root /> 컴포넌트를 렌더링한다.
  // pathname이 /about이라면 <About /> 컴포넌트를 렌더링한다.
};

const Root = () => {
  return <div>Home</div>;
};

const About = () => {
  return <div>About</div>;
};

그리고 Route라는 컴포넌트는 props로 경로와 렌더링할 컴포넌트를 받아서 location.pathname 이 전달한 경로(path)와 같다면 컴포넌트를 렌더링하도록 한다.

  • / 경로일 경우 <Root /> 컴포넌트가,
  • /about 경로일 경우 <About /> 컴포넌트가 렌더링된다.

여기까지 이제 경로에 따라 다른 컴포넌트가 렌더링되도록 했다.


하지만 브라우저의 뒤로 가기/앞으로 가기 기능은 제대로 동작하지 않는다.
앞서 pushState()를 이용해 히스토리 스택에 상태를 추가해줬기 때문에 뒤로 가기, 앞으로 가기를 눌렀을 때 주소창은 변경되지만 그에 따른 화면이 렌더링되도록 하는 것은 따로 구현해줘야 한다.

이를 해결하기 위해서는 popState 이벤트를 처리해줘야 한다.

onPopState 이벤트 처리

popState 이벤트는 뒤로 가기/앞으로 가기를 누르면 발생하는데, 이 이벤트를 listen해서 popState 이벤트가 발생할 때마다 currentPath를 업데이트시켜주면 된다.

useEffect 훅을 사용하여 컴포넌트가 마운트될 때 이벤트 리스너를 등록하고, 컴포넌트가 언마운트될 때 리스너를 제거해준다.

useEffect(() => {
  window.addEventListener('popstate', handlePopState);

  return () => {
    window.removeEventListener('popstate', handlePopState);
  };
}, []);

// popState 이벤트가 발생할 때마다 currentPath를 업데이트해준다.
function handlePopState() {
  setCurrentPath(window.location.pathname);
}

이렇게 하면 이제 브라우저에서 뒤로 가기/앞으로 가기를 통해 경로가 변경될 때마다 currentState 상태가 업데이트되고, 화면이 제대로 리렌더링된다.

profile
블로그 이전 -> https://janechun.tistory.com

4개의 댓글

comment-user-thumbnail
2023년 7월 7일

오 ㅎㅎ 저도 목요일에 히스토리 관리하는 거 했었는데 잘만드셨네요!
정리도 좋습니다!

답글 달기
comment-user-thumbnail
2023년 7월 8일

오 신기하네요 ㅎㅎ 고생하셨습니당!

답글 달기
comment-user-thumbnail
2023년 7월 9일

개념 정리 잘하고 갑니다 !!

답글 달기
comment-user-thumbnail
2023년 7월 9일

잘 보고 갑니다 !!

답글 달기