Skip to main content

Command Palette

Search for a command to run...

SPA에서 History API로 직접 라우팅 구현하기 ①

Updated
•3 min read•View as Markdown
D

프론트엔드 개발자 doit입니다.

이번 포스팅에서는 라우팅을 직접 구현하면서 마주한 문제들과, 이를 해결해가는 사고의 흐름을 정리해보고자 한다.

🧭 목표

SPA에서 a 태그나 URL 이동을 했을 때 페이지 새로고침 없이,
적절한 화면(UI)만 바꿔주는 라우팅을 직접 구현한다.

1️⃣ 문제 정의

보통 페이지 전환을 어떻게 구현하지..?

  • 기존 MPA 방식: 브라우저 + 서버가 페이지와 라우팅을 처리

    • URL 해석: 브라우저가 경로를 보고 서버에 요청

    • 라우팅 로직: 서버가 요청 경로에 맞는 HTML 반환

    • 화면 전환: 서버가 새 HTML 전송 → 전체 페이지 새로고침

  • 반면 SPA 방식: 브라우저 + JavaScript가 라우팅을 처리

    • URL 해석: 브라우저는 주소만 바꾸고, 실제 화면 변경은 JavaScript가 처리

    • 라우팅 로직: JavaScript(라우터)가 URL에 따라 컴포넌트를 렌더링

    • 화면 전환: JavaScript가 기존 HTML 유지한 채 일부만 변경

그런데 문제는…

❌ 브라우저는 기본적으로 URL이 바뀌면 서버에 요청하려고 한다
👉 하지만 SPA에서는 URL은 바뀌되, 새로고침 없이
해당 URL에 맞는 화면을 JS로 직접 렌더링해야 한다.


2️⃣ 필요에 따른 도구 및 방법 탐색

  • 그럼 새로고침 없이 브라우저의 URL 변경은 해주는 방법이 뭐지?

→ 아 이걸 도와주는 게 바로 History API 구나!


3️⃣ 도구 학습 : History API 학습

  • history.pushState() : URL 변경 + 새로고침 없음

  • popstate 이벤트 : 뒤로/앞으로 버튼 클릭 시 발생

→ 이걸 이용하면 SPA에서도 브라우저 주소창과 UI를 동기화할 수 있겠다!


4️⃣ 구조 설계: 라우터가 해야 할 일

기능설명
경로 등록URL에 맞는 렌더링 함수(컴포넌트)를 등록
렌더 함수현재 URL에 맞는 화면을 렌더링
이동 함수JS로 URL을 바꾸고 렌더링도 실행
popstate 대응뒤로가기/앞으로가기 시 렌더링 다시 실행
초기 진입 처리새로고침 시 현재 URL에 맞는 화면 보여주기

5️⃣ 구현하기: 라우터 기능별 코드 작성

① 경로 등록

const routes = {};

function addRoute(path, pageRenderer) {
  routes[path] = pageRenderer;
}

② 렌더 함수

function render(path) {
  const page = routes[path] || routes["/404"];
  document.querySelector("#content").innerHTML = page();
}

③ 이동 함수 (JS로 경로 바꾸기)

function navigateTo(path) {
  history.pushState(null, "", path);  // 주소는 바뀌지만 새로고침 X
  render(path);
}

④ 뒤로/앞으로 가기 대응 및 초기 진입 처리

function initRouter() {
  window.addEventListener("popstate", () => {
    render(window.location.pathname);
  });

  render(window.location.pathname);
}

⑤ 전체 라우터 조립

export const router = function () {
  const routes = {};

  function addRoute(path, pageRenderer) {
    routes[path] = pageRenderer;
  }

  function render(path) {
    const page = routes[path] || routes["/404"];
    document.querySelector("#content").innerHTML = page();
  }

  function navigateTo(path) {
    history.pushState(null, "", path);
    render(path);
  }

  function initRouter() {
    window.addEventListener("popstate", () => {
      render(window.location.pathname);
    });
    render(window.location.pathname);
  }

  return { addRoute, navigateTo, initRouter };
};

목표 확인 → 문제 발견 및 정의 → 필요에 따른 도구 및 방법 탐색 → (필요한 경우) 도구 및 방법 학습 → 구조 설계 → 기능 구현

🔄 흐름 요약

SPA를 만들고 싶은데 → 페이지 전환은 어떻게 하지?
→ 브라우저 주소는 바뀌어야 하는데, 새로고침은 안 돼고...
→ 근데 기본 브라우저 동작은 새로고침이 일어남..
→ 그럼 새로고침 없이 주소를 바꿀 방법이 뭐지?
→ 아 History API가 그걸 해줌! (pushState, popstate)
→ 그럼 이제 JS가 URL을 보고 화면을 바꾸는 "라우터" 역할을 직접 해야겠다!

10 views

More from this blog

Agent Skill로 README 자동화하기

저는 요즘 업무에서 AI 에이전트를 적극적으로 활용하여 반복적인 업무를 최대한 AI로 자동화하기 위해 공부 중인데요. 이번에 했던 작업 중 하나가 README 파일 생성 자동화입니다. AI에게 README를 써달라고 하면 꽤 그럴싸한 결과물이 나옵니다. 하지만 여러 프로젝트에 반복하다 보면 프롬프트가 모호할 때마다 매번 다른 방식으로 문제를 풀기 때문에 들

Mar 31, 20268 min read8

배열을 처리하는 세 가지 방법 (chaining, reduce, for loop)

개발을 하다 보면 배열 데이터를 가공할 일이 정말 많은데요.저는 평소에는 filter, map 같은 메서드 체이닝 방식을 주로 사용합니다. 읽기 쉽고, 데이터 흐름이 단계별로 명확하게 보이기 때문이죠. 하지만 어떠한 계기로(?) 인해 이런 의문을 가지게 되었습니다.. "만약 데이터가 수천만 건으로 늘어난다면? 배열을 여러 번 순회하는 체이닝 방식이 성능 병목을 일으키지는 않을까?" 혹은 "reduce 하나로 합치는 게 베스트일까?" 이 질문을 ...

Feb 4, 20263 min read19

[React] 좋은 추상화란 무엇인가(2) 잘못된 추상화는 중복보다 비싸다

지난 글을 보면 성급했던 재사용 컴포넌트 경험으로 고생을 한 적이 있었는데요.(아직 안 보셨다면? [1편: 좋은 추상화란 무엇인가(1)?] ) 애초에 왜 이런 고생을 했을까를 생각해 보면이유는 단순했습니다. 중복 코드가 거슬리니 바로 추상화를 해버린 것이었죠..똑같은 실수를 반복하지 않기 위해 저만의 기준이 필요하다고 느꼈습니다. 이에 대해 이미 깊이 고민하셨던 Cher Scarlett님과 Kent C. Dodds님의 글을 보면서, 제가 취해야...

Dec 16, 20253 min read25

[React] 좋은 추상화란 무엇인가(1) ? 유연한 컴포넌트 설계를 위한 제어의 역전

재사용을 위해 만든 컴포넌트가, 시간이 지날수록 오히려 재사용하기 어려워진 경험이 있으신가요? 저는 최근 프로젝트에서 SearchFilter 컴포넌트가 그러한데요 프로젝트에는 여러 목록 페이지가 존재했고, 페이지마다 검색 조건이 제각각이었습니다. 그래서 처음에는 Configuration 배열만 넘기면 알아서 그려주는 컴포넌트를 만들었습니다. // ❌ 초기의 설정 기반 접근 (Configuration) const filterFields = [ ...

Dec 9, 202510 min read36

React 시대의 함수형 프로그래밍

바닐라 JS처럼 개발자가 DOM과 상태 변경을 직접 제어하던 시기에는 불변성·순수 함수 같은 함수형 프로그래밍 철학이 지금처럼 중요하지는 않았습니다. 하지만 React(Vue/Redux/SWR/Recoil 등)처럼 선언적·상태 기반 UI가 등장하면서 상태를 “데이터 흐름의 스냅샷”으로 다뤄야 했고, 그때부터 함수형 프로그래밍이 중요해지기 시작했습니다. 🔥 바닐라 JS에서는 왜 FP가 별로 중요하지 않았을까? 바닐라 JS 시대의 UI 개발은...

Dec 8, 20255 min read35
D

Dlog!

39 posts

배우고, 나누며, 함께 성장하는 개발자 🚀