Develop/JavaScript

Vanilla JS로 SPA 화면전환 구현해보기 (feat. 프레임워크는 소중하다)

안다희 2024. 10. 18. 06:50
728x90

순수 Valnilla JS를 사용해 가계부 웹페이지를 만드는 프로젝트를 진행하고 있습니다.

JS만을 사용하다보니, Next.js같은 프레임워크에서 제공하는 기능들을 직접 구현해보는 기회가 되었어요.

아무래도 실무에서는 프레임워크를 사용할 확률이 훨씬 높겠지만, 프레임워크에서 어떤 부분들이 개발자의 수고를 덜어주는지 안다면 작동원리를 한층 깊게 이해하며 사용할 수 있게됩니다.

 

✅그래서 오늘은! JavaScript만으로 SPA를 직접 만들어보고 작동원리를 이해해보는 시간을 가져볼게요.

 

SPA 화면 전환

 

오늘 만들어 볼 가계부 UI입니다.

(Figma에서 expense tracker를 검색하면 나오는 템플릿을 사용했어요.)

 

페이지는 3개입니다.

1. Home: 지출 내역을 날짜별로 보는 페이지

2. Add/Edit: 지출 내역을 추가/수정하는 페이지

3. Report: 지출 분석 내역을 보는 페이지

 

각 화면은 아래 URL로 접근하기로 합니다.

1. Home: `/`

2. Add/Edit: `/add`

3. Report: `/report`

1. Home 2. Add/Edit 3. Report

아주 간단히 만들어둔 각 페이지의 진입화면입니다.

 

화면 전환은 SPA(Single Page Application) 방식으로 진행합니다.

SPA란?
- 다른 페이지로 이동할 때마다 전체 페이지를 다시 로드하는 대신, 처음부터 하나의 HTML 페이지만 로드하는 방식.
- 빠른 페이지 전환을 통해 부드러운 UX를 제공하며 데이터 로드에 최적화되어있음.

 

 

먼저Home에서 Add/Edit으로 화면을 이동해봅시다.

 

Next.js로 화면 전환

import Link from 'next/link';

export default function Home() {
  return (
    ...
    <Link href="/add">
      <a>Go to Add/Edit Page</a>
    </Link>
    ...
  );
}

Next.js의 Link를 사용하면 간편하게 SPA 방식의 화면 전환이 가능해요.

각 화면에 해당하는 파일만 추가로 만들어두면 끝이에요!

 

JavaScript로 화면 전환

<a href="./add.html">Go To Add/Edit Page</a>

하지만 JavaScript로 SPA 방식의 화면 전환을 한다면 아래와 같은 세팅이 필요합니다.

 

1. index.html 세팅

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Expense Tracker</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <link rel="stylesheet" href="./css/style.css" />
  </head>
  <body>
    <script type="module" src="App.js"></script>

    <div>
      <!-- 1. Home -->
      <section id="homeSection" class="section">
        <h2 class="text-2xl mb-4">1. Home</h2>
        <br />

        <a href="/add"> <button>Go To Add/Edit Page</button></a>
        <div>
          <a href="/report">Go To Report Page</a>
        </div>
      </section>

      <!-- 2. Add/Edit -->
      <section id="addSection" style="display: none" class="section">
        <h2 class="text-2xl mb-4">2. Add/Edit</h2>
      </section>

      <!-- 3. Report -->
      <section id="reportSection" style="display: none" class="section">
        <div>
          <h2 class="text-2xl mb-4">3. Report</h2>
        </div>
      </section>
    </div>
  </body>
</html>

우선 `index.html`에 필요한 화면 기본 구조를 HTML로 작성합니다.

`id`가 `homeSection` / `addSection` / `reportSection` 인 부분 보이시죠!

이렇게 한곳에 모두 작성을 해두고, 화면 전환이 필요할 때마다 해당 section의 style을 `display: none;`에서 `display: block;`으로 변경해주어 페이지 전환이 이루어지도록 하는 것입니다.

 

2. App.js 세팅

// route page
const routes = [
  {
    path: "/",
    sectionId: "homeSection",
  },
  {
    path: "/add",
    sectionId: "addSection",
  },
  {
    path: "/report",
    sectionId: "reportSection",
  },
];

// be global function by appending `window.`
window.navigate = (url) => {
  window.history.pushState(null, null, url);
  App();
};

// render page by url & state is maintained without refreshing.
const App = async () => {
  // hide all sections
  document.querySelectorAll(".section").forEach((section) => {
    section.style.display = "none";
  });

  // find page to show
  const pageMatches = routes.map((route) => ({
    route: route,
    isMatch: window.location.pathname === route.path,
  }));
  const match = pageMatches.find((pageMatch) => pageMatch.isMatch);
  if (match) {
    document.getElementById(match.route.sectionId).style.display = "block";
  }
};

// Fired when the HTML is fully parsed and the DOM is completely built.
document.addEventListener("DOMContentLoaded", () => {
  // add click event listener tag "<a>"
  document.body.addEventListener("click", (e) => {
    const target = e.target.closest("a");
    if (!(target instanceof HTMLAnchorElement)) return;

    e.preventDefault(); // prevent default page refresh
    navigate(target.href); // change url without refresh = SPA(Single Page Application)
  });

  App();
});

// Listen for 'popstate' events (triggered when the user navigates using the browser's back/forward buttons)
// This ensures the app correctly updates the view when the history state changes without a full page reload.
window.addEventListener("popstate", App);

이 모든 코드가 화면 전환을 위한 작업 코드입니다 하핫.

하나하나 자세히 볼까요?

 

2-1. routes

const routes = [
  {
    path: "/",
    sectionId: "homeSection",
  },
  {
    path: "/add",
    sectionId: "addSection",
  },
  {
    path: "/report",
    sectionId: "reportSection",
  },
];

우선 `routes`는, 해당 경로와 매칭되는 `sectionId`를 미리 정의한 배열이에요.

`url`에 따라 html에서 미리 세팅해둔 `section`을 보여줄 때 사용됩니다.

 

2-2. navigate

window.navigate = (url) => {
  window.history.pushState(null, null, url);
  App();
};

`navigate`는 화면을 전환할 때 사용되는 함수에요.

JavaScript의 `History API`를 사용하여, 페이지를 새로고침하지 않고도 브라우저의 URL을 변경하고 브라우저의 뒤로가기/앞으로가기 기능을 사용할 수 있어요.

 

💡`window.`를 붙여준 이유는, navigate를 전역 함수로 사용하기 위함입니다.

App.js 파일은 `<script type="module" scr="App.js"></script>` 이렇게 import하면서 모듈스코프를 가진 파일이 됩니다.

`window.`가 붙지 않는다면, navigate 함수는 모듈스코프(파일스코프) 내에 존재하기 때문에, `navigate` 함수가 사용되는 이벤트 핸들러에서는 참조가 불가능해요. 이벤트 핸들러에서 DOM 트리로부터 트리거된 함수는 전역스코프를 사용하기 때문이에요.

기껏 모듈스코프인 파일을 만들었는데 전역함수로 만드는 것은 좋지 않은 방법인 것 같지만, 일단은 SPA가 어떻게 동작하는지를 이해하기 위해 한 파일에 넣기로 하고 이어서 봅시다!

▷ 전역스코프와 모듈스코프(파일스코프)의 차이점 더 자세히 알아보기

 

2-3. App()

// render page by url & state is maintained without refreshing.
const App = async () => {
  // hide all sections
  document.querySelectorAll(".section").forEach((section) => {
    section.style.display = "none";
  });

  // find page to show
  const pageMatches = routes.map((route) => ({
    route: route,
    isMatch: window.location.pathname === route.path,
  }));
  const match = pageMatches.find((pageMatch) => pageMatch.isMatch);
  if (match) {
    document.getElementById(match.route.sectionId).style.display = "block";
  }
};

`App.js`에서는 `section` class를 가진 요소를 모두 `display: none;`으로 바꾼 뒤, 

현재 URL에 해당하는 요소만 다시 `display: block;`으로 바꿔주는 작업이 진행돼요.


2-4. click event listener

// Fired when the HTML is fully parsed and the DOM is completely built.
document.addEventListener("DOMContentLoaded", () => {
  // add click event listener tag "<a>"
  document.body.addEventListener("click", (e) => {
    const target = e.target.closest("a");
    if (!(target instanceof HTMLAnchorElement)) return;

    e.preventDefault(); // prevent default page refresh
    navigate(target.href); // change url without refresh = SPA(Single Page Application)
  });

  App();
});

`DOMContentLoaded`는 브라우저에서 HTML의 구조가 완전히 로드되고 파싱되었을 때 발생하는 이벤트에요.

DOM 요소들이 준비가 되었으니 요소에 이벤트를 붙일 준비가 된 것이죠.

우리는 `<a>` 클릭으로 화면전환을 할 것이기 때문에, 이곳에 이벤트리스너를 등록해줍니다.

 

2-5. popstate event listener

// Listen for 'popstate' events (triggered when the user navigates using the browser's back/forward buttons)
// This ensures the app correctly updates the view when the history state changes without a full page reload.
window.addEventListener("popstate", App);

마지막으로, 브라우저의 뒤로가기/앞으로가기 버튼을 눌렀을 때 앱의 상태를 업데이트하는데 사용되는 `popstate` 리스너 등록입니다.

이 이벤트가 발생할 때마다 `App()`함수가 호출되기 때문에, URL에 맞게 다시 렌더링/업데이트가 됩니다.


2-6. 정리

페이지로드가 완료되어 DOM요소가 준비되면 `<a>`태그에 직접 클릭이벤트를 등록하여, 클릭 시 페이지가 새로고침되지 않고 `navigate`함수가 동작되도록 합니다.

`navigate`함수에서는 `history.pushState()`를 사용하여 url을 수정하고 `App`함수를 호출합니다.

App함수에서는 현재 url에 맞는 section만을 동적으로 렌더링합니다.

 

👏이렇게 JavaScript만으로 SPA 방식의 페이지 전환을 구현하였습니다!

 

 

Next.js와 같은 프레임워크를 사용한다면 전부 생략되었을 과정을 직접 작성해보니,새로고침 없이 URL이 변경되면서 동적인 페이지 상태를 유지하는 SPA의 원리를 알 수 있었습니다.

 

직접 SPA를 구현하는 것이 실무에서는 드물겠지만, 단순히 프레임워크를 잘 활용하는 것보다 이것을 왜 쓰는지 이해하면서 활용할 줄 아는 것이 훨씬 중요합니다.

 

오늘의 포스팅은 마치도록 하겠습니다. 감사합니다:)

 

'Develop > JavaScript' 카테고리의 다른 글

중첩된 Promise<Promise<T>>를 조심해!  (0) 2023.01.25
[Javascript] 특정 month의 마지막 날짜 구하는 법  (0) 2020.03.27
[JavaScript] Week4  (0) 2019.02.18
[JavaScript] Week2  (0) 2019.01.28
[JavaScript] Week1  (0) 2019.01.21
출처: https://mingos-habitat.tistory.com/34 [밍고의서식지:티스토리]