JavaScript pushState 완전 가이드: 모던 웹 라우팅의 핵심 이해하기

한상우·2025년 6월 5일

리액트

목록 보기
23/24
post-thumbnail

JavaScript pushState 완전 가이드: 모던 웹 라우팅의 핵심 이해하기

페이지 새로고침 없이 URL을 변경하는 마법, pushState API를 파헤쳐보자

목차

  1. pushState란 무엇인가?
  2. pushState 동작 원리
  3. 기본 사용법과 예제
  4. popstate 이벤트 이해하기
  5. React에서 pushState 활용하기
  6. 인터셉팅 라우트 구현
  7. 패러럴 라우트 구현
  8. 실무에서 주의할 점

1. pushState란 무엇인가?

history.pushState()HTML5 History API의 핵심 메서드입니다. 이 API는 브라우저의 세션 히스토리를 조작할 수 있게 해주는 강력한 도구로, 페이지를 새로고침하지 않고도 URL을 변경할 수 있습니다.

왜 pushState가 중요한가?

전통적인 웹에서는 URL이 바뀌면 항상 새로운 페이지를 서버에서 불러와야 했습니다. 하지만 현대의 SPA(Single Page Application)에서는 하나의 페이지에서 모든 것을 처리하면서도 URL을 통해 다양한 상태를 표현해야 합니다.

// 전통적인 방식 (페이지 새로고침 발생)
window.location.href = '/about';

// pushState 방식 (페이지 새로고침 없음)
history.pushState(null, null, '/about');

2. pushState 동작 원리

기본 문법

history.pushState(state, title, url);

매개변수 상세 설명:

  • state: 새로운 히스토리 엔트리와 연관된 JavaScript 객체
  • title: 페이지 제목 (현재 대부분 브라우저에서 무시됨)
  • url: 변경할 URL (반드시 같은 도메인이어야 함)

pushState vs replaceState

// pushState: 새로운 히스토리 엔트리 추가
history.pushState({page: 1}, null, '/page1');
history.pushState({page: 2}, null, '/page2');
// 뒤로가기 시: /page2 → /page1 → 이전 페이지

// replaceState: 현재 히스토리 엔트리 교체
history.replaceState({page: 1}, null, '/page1');
history.replaceState({page: 2}, null, '/page2');
// 뒤로가기 시: /page2 → 이전 페이지 (page1 건너뜀)

3. 기본 사용법과 예제

간단한 예제

// 현재 URL: https://example.com/
console.log(window.location.pathname); // "/"

// URL 변경 (페이지 새로고침 없음)
history.pushState({section: 'about'}, null, '/about');
console.log(window.location.pathname); // "/about"

// 상태 정보 확인
console.log(history.state); // {section: 'about'}

상태(state) 활용하기

const userInfo = {
  id: 123,
  name: '김개발',
  role: 'frontend'
};

// 상태와 함께 URL 변경
history.pushState(userInfo, null, '/user/123');

// 나중에 상태 정보 사용
if (history.state && history.state.role === 'frontend') {
  console.log('프론트엔드 개발자입니다!');
}

4. popstate 이벤트 이해하기

사용자가 브라우저의 뒤로가기/앞으로가기 버튼을 클릭하면 popstate 이벤트가 발생합니다.

기본 popstate 처리

window.addEventListener('popstate', function(event) {
  console.log('사용자가 뒤로가기/앞으로가기를 눌렀습니다');
  console.log('현재 URL:', window.location.pathname);
  console.log('상태 정보:', event.state);
});

실용적인 예제

function setupNavigation() {
  // 페이지 변경 함수
  function navigateTo(path, data = null) {
    history.pushState(data, null, path);
    renderPage(path, data);
  }
  
  // 뒤로가기/앞으로가기 처리
  window.addEventListener('popstate', function(event) {
    const currentPath = window.location.pathname;
    renderPage(currentPath, event.state);
  });
  
  // 페이지 렌더링 함수
  function renderPage(path, data) {
    const content = document.getElementById('content');
    
    switch(path) {
      case '/':
        content.innerHTML = '<h1>홈 페이지</h1>';
        break;
      case '/about':
        content.innerHTML = '<h1>소개 페이지</h1>';
        break;
      case '/contact':
        content.innerHTML = '<h1>연락처 페이지</h1>';
        break;
    }
  }
  
  // 네비게이션 버튼에 이벤트 추가
  document.getElementById('home-btn').onclick = () => navigateTo('/');
  document.getElementById('about-btn').onclick = () => navigateTo('/about');
  document.getElementById('contact-btn').onclick = () => navigateTo('/contact');
}

5. React에서 pushState 활용하기

간단한 React 라우터

import React, { useState, useEffect } from 'react';

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

  useEffect(() => {
    // 뒤로가기/앞으로가기 감지
    const handlePopState = () => {
      setCurrentPath(window.location.pathname);
    };

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

  // 페이지 이동 함수
  const navigateTo = (path) => {
    history.pushState(null, null, path);
    setCurrentPath(path);
  };

  // 현재 경로에 따른 컴포넌트 렌더링
  const renderCurrentPage = () => {
    switch (currentPath) {
      case '/':
        return <div>홈 페이지</div>;
      case '/about':
        return <div>소개 페이지</div>;
      case '/contact':
        return <div>연락처 페이지</div>;
      default:
        return <div>404 - 페이지를 찾을 수 없습니다</div>;
    }
  };

  return (
    <div>
      <nav>
        <button onClick={() => navigateTo('/')}></button>
        <button onClick={() => navigateTo('/about')}>소개</button>
        <button onClick={() => navigateTo('/contact')}>연락처</button>
      </nav>
      
      <main>
        {renderCurrentPage()}
      </main>
    </div>
  );
}

커스텀 useRouter 훅

import { useState, useEffect } from 'react';

function useRouter() {
  const [location, setLocation] = useState({
    pathname: window.location.pathname,
    state: history.state
  });

  useEffect(() => {
    const handlePopState = (event) => {
      setLocation({
        pathname: window.location.pathname,
        state: event.state
      });
    };

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

  const push = (path, state = null) => {
    history.pushState(state, null, path);
    setLocation({ pathname: path, state });
  };

  return { location, push };
}

// 사용 예제
function App() {
  const { location, push } = useRouter();

  return (
    <div>
      <p>현재 경로: {location.pathname}</p>
      <button onClick={() => push('/home', { from: 'button' })}>
        홈으로 이동
      </button>
    </div>
  );
}

6. 인터셉팅 라우트 구현

인터셉팅 라우트는 특정 경로로의 이동을 가로채서 다른 방식(예: 모달)으로 표시하는 기법입니다.

모달을 이용한 인터셉팅

import React, { useState, useEffect } from 'react';

function InterceptingRouter() {
  const [currentPath, setCurrentPath] = useState(window.location.pathname);
  const [showModal, setShowModal] = useState(false);

  useEffect(() => {
    const handlePopState = () => {
      const newPath = window.location.pathname;
      setCurrentPath(newPath);
      
      // 모달 상태에서 뒤로가기 시 모달 닫기
      if (showModal && !newPath.includes('/photo/')) {
        setShowModal(false);
      }
    };

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

  // 일반 네비게이션
  const navigateTo = (path) => {
    history.pushState(null, null, path);
    setCurrentPath(path);
    setShowModal(false);
  };

  // 인터셉팅 네비게이션 (모달로 표시)
  const openPhotoModal = (photoId) => {
    history.pushState({ modal: true }, null, `/photo/${photoId}`);
    setCurrentPath(`/photo/${photoId}`);
    setShowModal(true);
  };

  // 모달 닫기
  const closeModal = () => {
    setShowModal(false);
    history.pushState(null, null, '/gallery');
    setCurrentPath('/gallery');
  };

  const renderPage = () => {
    const basePath = showModal ? '/gallery' : currentPath;
    
    switch (basePath) {
      case '/':
        return <div>홈 페이지</div>;
      case '/gallery':
        return (
          <div>
            <h1>갤러리</h1>
            <button onClick={() => openPhotoModal(1)}>사진 1 보기</button>
            <button onClick={() => openPhotoModal(2)}>사진 2 보기</button>
          </div>
        );
      default:
        return <div>404 페이지</div>;
    }
  };

  const renderModal = () => {
    if (!showModal) return null;
    
    const photoId = currentPath.match(/\/photo\/(\d+)/)?.[1];
    
    return (
      <div className="modal-overlay" onClick={closeModal}>
        <div className="modal-content" onClick={e => e.stopPropagation()}>
          <h2>사진 {photoId}</h2>
          <p>모달에서 표시되는 사진 상세 정보</p>
          <button onClick={closeModal}>닫기</button>
        </div>
      </div>
    );
  };

  return (
    <div>
      <nav>
        <button onClick={() => navigateTo('/')}></button>
        <button onClick={() => navigateTo('/gallery')}>갤러리</button>
      </nav>
      
      {renderPage()}
      {renderModal()}
    </div>
  );
}

7. 패러럴 라우트 구현

패러럴 라우트는 하나의 페이지에서 여러 영역을 독립적으로 라우팅하는 기법입니다.

기본 패러럴 라우트

import React, { useState, useEffect } from 'react';

function ParallelRouter() {
  const [routes, setRoutes] = useState({
    main: '/',
    sidebar: '/sidebar-home'
  });

  useEffect(() => {
    const handlePopState = (event) => {
      if (event.state?.routes) {
        setRoutes(event.state.routes);
      }
    };

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

  // 패러럴 라우트 업데이트
  const updateRoute = (area, path) => {
    const newRoutes = { ...routes, [area]: path };
    
    // URL에 상태 반영
    const url = `${newRoutes.main}?sidebar=${encodeURIComponent(newRoutes.sidebar)}`;
    history.pushState({ routes: newRoutes }, null, url);
    
    setRoutes(newRoutes);
  };

  const renderMain = () => {
    switch (routes.main) {
      case '/':
        return <div>메인 홈</div>;
      case '/about':
        return <div>메인 소개</div>;
      default:
        return <div>메인 404</div>;
    }
  };

  const renderSidebar = () => {
    switch (routes.sidebar) {
      case '/sidebar-home':
        return <div>사이드바 홈</div>;
      case '/sidebar-menu':
        return <div>사이드바 메뉴</div>;
      default:
        return <div>사이드바 기본</div>;
    }
  };

  return (
    <div style={{ display: 'flex' }}>
      <main style={{ flex: 1, padding: '20px' }}>
        <nav>
          <button onClick={() => updateRoute('main', '/')}>메인 홈</button>
          <button onClick={() => updateRoute('main', '/about')}>메인 소개</button>
        </nav>
        {renderMain()}
      </main>
      
      <aside style={{ width: '200px', padding: '20px', background: '#f0f0f0' }}>
        <nav>
          <button onClick={() => updateRoute('sidebar', '/sidebar-home')}>사이드바 홈</button>
          <button onClick={() => updateRoute('sidebar', '/sidebar-menu')}>사이드바 메뉴</button>
        </nav>
        {renderSidebar()}
      </aside>
    </div>
  );
}

8. 실무에서 주의할 점

브라우저 호환성

// pushState 지원 여부 확인
if (window.history && window.history.pushState) {
  // pushState 사용
  history.pushState(null, null, '/new-path');
} else {
  // 폴백: 전통적인 방식
  window.location.href = '/new-path';
}

SEO 고려사항

// 페이지 제목과 메타 정보 업데이트
function updatePageMeta(title, description) {
  document.title = title;
  
  const metaDesc = document.querySelector('meta[name="description"]');
  if (metaDesc) {
    metaDesc.setAttribute('content', description);
  }
}

// 라우트 변경 시 SEO 정보 업데이트
function navigateWithSEO(path, pageInfo) {
  history.pushState(pageInfo, null, path);
  updatePageMeta(pageInfo.title, pageInfo.description);
}

성능 최적화

// 디바운싱으로 과도한 히스토리 엔트리 방지
let pushTimeout;
function debouncedPush(path, state, delay = 300) {
  clearTimeout(pushTimeout);
  pushTimeout = setTimeout(() => {
    history.pushState(state, null, path);
  }, delay);
}

서버 설정 필요

SPA에서 pushState를 사용할 때는 서버에서 모든 경로를 index.html로 리다이렉트하도록 설정해야 합니다.
하지 않는다면 에러가 발생할것입니다.

# Nginx 설정 예제
location / {
  try_files $uri $uri/ /index.html;
}

마무리

pushState를 제대로 이해하면

  • SPA의 동작 원리를 깊이 이해할 수 있습니다
  • React Router 같은 라이브러리의 내부 동작을 파악할 수 있습니다
  • 커스텀 라우팅 솔루션을 직접 구현할 수 있습니다
  • Next.js의 고급 라우팅 기능을 순수 React로 구현할 수 있습니다(페러럴라우팅, 인터세브 라우팅)
profile
안녕하세요

0개의 댓글