Logo

React에서 location 객체 접근하기

React로 SPA(Single Page Application)를 개발하는 경우 window.location 객체에 접근할 때 여러가지 문제가 발생할 수 있는데요. SPA에서는 보통 클라이언트 단에서 랜더링을 하기 때문에 브라우저의 location 객체를 사용할 때 조금 더 세심한 고려가 필요합니다.

이번 포스팅에서는 React 앱에서 브라우저의 location 객체에 안전하게 접근하는 방법과 이를 위한 커스텀 훅(hook)을 구현해보도록 하겠습니다.

브라우저의 location 전역 객체

우선 브라우저의 location 객체에 대해서 간단하기 짚고 넘어가는 게 좋을 것 같아요.

자바스크립트에서 보통 window.location이나 document.location을 통해서 접근할 수 있는 location 객체에는 현재 브라우저 열려있는 페이지의 위치가 담겨 있는데요. 쉽게 말해서 현재 페이지의 URL이 객체화되어 담겨 있으며, 프로토콜(protocol), 호스트네임(hostname), 포트(port), 경로명(pathname)과 같은 프로퍼티로 구성됩니다.

브라우저의 location 전역 객체에는 현재 브라우저 열려있는 페이지의 위치가 담겨 있는데요. 인터넷에서는 기본적적으로 URL을 통해서 페이지를 식별하기 때문에, 쉽게 말해서 현재 페이지의 URL 정보라고 보시면 되겠습니다.

window.location.hostname; // 'www.daleseo.com'
document.location.hostname; // 'www.daleseo.com'
location.hostname; // 'www.daleseo.com'

브라우저의 location 전역 객체를 사용하여 자바스크립트로 웹 페이지를 이동하는 방법에 대해서는 관련 포스팅을 참고 바랍니다.

window.location에 접근하면 발생하는 문제

우선 React 컴포넌트 내에서 웹 페이지 위치 정보를 window.location를 통해 바로 읽어오게 되면 어떤 문제가 일어나는지 알아보겠습니다.

실습을 위해서 클라이언트 단 라우팅을 위한 4개의 버튼과 현재 경로를 출력해주는 간단한 컴포넌트를 작성해보았습니다. 버튼을 클릭하면 window.history 객체의 pushState() 메서드를 호출되어 페이지 로딩없이 브라우저의 주소 표시줄만 바뀌게 됩니다. 그리고 <h1> 요소 안에 현재 경로가 무엇인지를 window.location 객체의 pathname 프로퍼티를 읽어서 화면에 보여주고 있습니다.

function App() {
  const navigate = (path) => {
    window.history.pushState({}, "", path);
  };

  return (
    <>
      <header>
        <button onClick={() => navigate("/home")}>Home</button>
        <button onClick={() => navigate("/about")}>About</button>
        <button onClick={() => navigate("/users/1")}>User 1</button>
        <button onClick={() => navigate("/users/2")}>User 2</button>
      </header>
      <main>
        <h1>현재 경로는 {window.location.pathname} 입니다.</h1>
      </main>
    </>
  );
}

하지만 아래 CodeSandbox의 내장 브라우저에서 컴포넌트를 테스트해보시면 주소 표시줄의 URL은 바뀌지만 <h1> 요소 안의 현재 경로는 그에 따라 갱신이 되지 않는 것을 볼 수 있습니다.

왜 이러한 문제가 발생하는 걸까요? 한번 아무 버튼을 눌러서 URL을 먼저 바꾸고 그 다음 주소 표시줄 왼쪽에 있는 새로 고침을 눌려보세요. 그러면 화면에 맞는 현재 경로가 찍힐 것입니다.

즉, 이 문제는 우리가 작성한 React 컴포넌트가 window.location이 바뀔 때 마다 브라우저에 다시 그려지지 않아서 발생하는 문제라는 것을 알 수 있는데요. History API의 pushState() 메서드를 사용하여 window.location을 변경할 때는 브라우저가 페이지 로딩을 하지 않기 때문에 우리가 컴포넌트를 적절히 재 린더링(rendering)을 해줘야 합니다.

자바스크립트의 History API가 클라이언트 단 라우팅을 구현하는데 어떻게 활용되는지는 별도 포스팅에 자세히 설명 해놓았으니 참고 바랍니다.

location 상태 추가

어떻게 하면 URL이 바뀔 때 마다 UI도 갱신되게 할 수 있을까요? 현재 URL을 컴포넌트의 상태로 저장하면 어떨까요? React가 자동으로 해당 상태가 바뀔 때 마다 컴포넌트를 화면에 다시 그려주겠죠? 👍

import React from "react";

function App() {
  const [location, setLocation] = React.useState({ ...window.location });
  const navigate = (path) => {
    window.history.pushState({}, "", path);
    setLocation({ ...window.location });  };

  return (
    <>
      <header>
        <button onClick={() => navigate("/home")}>Home</button>
        <button onClick={() => navigate("/about")}>About</button>
        <button onClick={() => navigate("/users/1")}>User 1</button>
        <button onClick={() => navigate("/users/2")}>User 2</button>
      </header>
      <main>
        <h1>현재 경로는 {location.pathname} 입니다.</h1>
      </main>
    </>
  );
}

아래 CodeSandbox의 내장 브라우저에서 수정된 컴포넌트를 테스트해보시면 주소 표시줄의 URL과 <h1> 요소 안의 현재 경로가 함께 바뀌는 것을 볼 수 있으실 것입니다.

하지만 여기서도 한 가지 문제가 발생하는데요. 바로 뒤로 가기가 앞으로 가기를 했을 때 주소 표시줄의 URL만 바뀌고 <h1> 요소 안의 현재 경로는 마지막으로 버튼이 눌렀을 때 상태에서 변하지 않는다는 것입니다.

PopState 이벤트 처리

어떻게 하면 브라우저에서 뒤로 가기나 앞으로 가기를 했을 때도 UI가 갱신되게 할 수 있을까요?

브라우저에서 뒤로 가기나 앞으로 가기를 하면 window에서 popstate 이벤트가 발생하는데요. 이 이벤트가 발생한 순간 window.location을 읽어서 location 상태에 설정해주면 될 것입니다.

import React from "react";

function App() {
  const [location, setLocation] = React.useState({ ...window.location });

  const handlePopState = () => setLocation({ ...window.location });
  React.useEffect(() => {    window.addEventListener("popstate", handlePopState);    return () => window.removeEventListener("popstate", handlePopState);  }, []);
  const navigate = (path) => {
    window.history.pushState({}, "", path);
    setLocation({ ...window.location });
  };

  return (
    <>
      <header>
        <button onClick={() => navigate("/home")}>Home</button>
        <button onClick={() => navigate("/about")}>About</button>
        <button onClick={() => navigate("/users/1")}>User 1</button>
        <button onClick={() => navigate("/users/2")}>User 2</button>
      </header>
      <main>
        <h1>현재 경로는 {location.pathname} 입니다.</h1>
      </main>
    </>
  );
}

아래 CodeSandbox의 내장 브라우저에서 수정된 컴포넌트를 테스트해보시면 뒤로 가기와 앞으로 가기를 할 때도 화면 내의 현재 경로가 잘 갱신되는 것을 볼 수 있으실 거에요.

커스텀 훅으로 추출

드디어 웹 페이지의 위치 정보에 제대로 반응하는 컴포넌트가 완성이 된 것 같아요. 그런데 다른 컴포넌트에서도 location 객체에 접근해야 한다면 어떻게 해야 할까요?

관련 로직을 useLocation()이라는 커스텀 훅으로 추출하면 재사용하기가 용이하겠죠? 💫

useLocation.js
import React from "react";

function useLocation() {
  const [location, setLocation] = React.useState({ ...window.location });

  React.useEffect(() => {
    const handlePopState = () => setLocation({ ...window.location });
    window.addEventListener("popstate", handlePopState);
    return () => window.removeEventListener("popstate", handlePopState);
  }, []);

  const navigate = (path) => {
    window.history.pushState({}, "", path);
    setLocation({ ...window.location });
  };

  return { location, navigate };
}

이제 컴포넌트가 다시 맨 처음처럼 매우 깔끔해진 것을 볼 수 있습니다. 🎉

App.js
import useLocation from "./useLocation";
function App() {
  const { location, navigate } = useLocation();

  return (
    <>
      <header>
        <button onClick={() => navigate("/home")}>Home</button>
        <button onClick={() => navigate("/about")}>About</button>
        <button onClick={() => navigate("/users/1")}>User 1</button>
        <button onClick={() => navigate("/users/2")}>User 2</button>
      </header>
      <main>
        <h1>현재 경로는 {location.pathname} 입니다.</h1>
      </main>
    </>
  );
}

커스텀 훅과 컴포넌트에 대한 최종 코드는 아래 CodeSandbox에서 직접 확인하고 실행해 보실 수 있습니다.

마치면서

지금까지 실습을 통해서 클라이언트 단에서 라우팅이 일어나는 React 앱에서 어떻게 브라우저의 location 전역 객체를 읽어올 수 있는지에 대해서 알아보았습니다. 그리고 효과적인 코드 재사용을 위해서 useLocation()이라는 커스텀 훅도 구현해보았습니다.

참고로 상용 프로젝트에서 많이 사용되는 라우팅 라이브러리에는 이러한 훅이 이미 포함되어 있습니다. 예를 들어, React Router는 useLocation를 제공하고 있으니 활용해보시기 바랍니다.