Notice
Recent Posts
Recent Comments
Link
«   2026/02   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
Archives
Today
Total
관리 메뉴

효뚜르팝의 Dev log

[항해 플러스] 1주차 회고 - 바닐라 JS로 SPA 구조 직접 구현해보기 본문

회고록

[항해 플러스] 1주차 회고 - 바닐라 JS로 SPA 구조 직접 구현해보기

hyodduru 2025. 3. 28. 23:31

바닐라 JS로 SPA 구조 직접 구현해보기

이번 주는 프레임워크 없이 순수 JavaScript로 SPA(Single Page Application)을 직접 구현하며
라우터, 상태 관리, 이벤트 위임, 정적 호스팅 배포까지 웹앱의 핵심 동작을 체감해본 한 주였다.


✅ Keep – 이번 주에 잘한 점 / 계속 유지하고 싶은 시도

  • React 없이 상태 기반 렌더링 구조를 직접 구성해보며 프레임워크가 감춰주던 내부 동작 원리를 이해함
  • createRouter()를 직접 구현해 라우팅 흐름의 본질을 체험했고, 라우트 등록 시 guard, redirect, render로 역할을 분리해 설계함
  • **기능 중심 폴더 구조(FSD)**를 적용하여 features, app, shared로 각 폴더의 책임을 명확히 나눔
  • 페이지 전환 등의 이벤트는 이벤트 위임 방식으로 처리, 동적 DOM에도 안정적으로 이벤트 적용 가능하도록 설계함
  • 다음과 같은 라우트 설정 구조도 직접 작성함: (라우트 객체에 path, guard, redirect, render를 분리해 명확하게 책임을 나눈 점이 특히 만족스러웠다. ) 
export const routes = 
[ 
	{ 
      path: `${BASE_URL}/`, 
      render: (container) => { 
      container.innerHTML = MainPage(); 
      }, 
    }, 
    { 
      path: `${BASE_URL}/login`, 
      guard: () => !authStore.isLoggedIn(), 
      redirect: `${BASE_URL}/`, 
      render: (container, router) => { 
      container.innerHTML = LoginPage(); 
      bindLoginEvent(container, router); 
      }, 
    }, 
    { 
      path: `${BASE_URL}/profile`, 
      guard: () => authStore.isLoggedIn(), 
      redirect: `${BASE_URL}/login`, 
      render: (container) => { 
      container.innerHTML = ProfilePage("/profile"); 
      bindProfileEvent(container); }, 
     }, 
     { 
       path: "404", 
       render: (container) => 
       { container.innerHTML = NotFoundPage(); }, 
      },
  ];
  
  
  export function setupRoutes(router) {
  routes.forEach(({ path, guard, redirect, render }) => {
    router.addRoute(path, (container) => {
      // guard 조건이 있고, 통과하지 못하면 redirect
      if (guard && !guard()) {
        router.navigateTo(redirect);
        return;
      }
      
        render(container, router);
    });
  });
}

 

🗂️ 폴더 구조 – 기능 중심(FSD: Feature-Sliced Design)으로 나눈 이유

이번 프로젝트는 규모는 작지만 구조적으로 확장성을 고려해 FSD(Feature-Sliced Design) 패턴을 적용했다.

폴더명역할 설명
features/ 로그인, 프로필, 메인 등 도메인 단위 기능의 UI/이벤트/상태 로직을 모은 폴더. 응집도가 높고 유지보수가 쉬움
app/ 앱 초기화, 전역 이벤트 처리, 라우터 설정 등 전체 앱 흐름을 제어하는 중심 폴더
shared/ BASE_URL, 공통 상수, 유틸 함수 등 여러 기능에서 공통으로 쓰이는 범용 리소스를 모은 폴더

처음에는 authStore를 features/auth에 두었지만,
인증 상태는 전체 앱 흐름에 영향을 주기 때문에 전역 상태로 분리하는 방향이 더 좋았을 수 있다고 느꼈다.
이러한 구조적 고민을 통해 도메인 로직과 전역 흐름 사이의 경계를 명확히 보는 시각을 키울 수 있었다.

 

🌐 배포 – GitHub Pages로 직접 SPA 배포해본 경험

이번 프로젝트는 GitHub Pages를 통해 정적 호스팅으로 직접 배포까지 진행해본게 재밌는 점중 하나다. 

📦 배포 과정 요약

  1. Vite 프로젝트에 base 경로 설정
// vite.config.js
export default defineConfig({ 
base: process.env.NODE_ENV === "production" ? "/프로젝트명/" : "/", });

 

  2. 404.html을 만들어서 브라우저 라우터 지원
→ GitHub Pages는 정적 파일만 서빙하기 때문에, 직접 URL로 접근하면 404가 발생하는데,
이 문제를 404.html에서 index.html로 리디렉션하도록 처리해 해결했습니다.

3. vite build + gh-pages 브랜치에 배포

npm run build
npx gh-pages -d dist

 


🧩 Problem – 고민이 필요했던 부분

  • 상태 변경 시 단순히 innerHTML만 갱신하면 guard, redirect, 404 처리 등 라우팅 로직이 동작하지 않는 문제가 있었음
  • 이 문제를 해결하기 위해 authStore.subscribe()에서 router.start()를 호출해 전체 라우팅 흐름을 다시 실행하는 방식을 선택함
    → 이 방식은 약간 새로고침에 가까운 처리지만, 라우팅 흐름을 다시 타기 때문에 guard, redirect 등의 처리를 놓치지 않음
  • 모든 경로에 ${BASE_URL}을 수동으로 붙이는 방식은 반복과 실수 가능성이 많았고,
    유지보수가 힘들다고 느껴 구조 개선의 필요성을 느꼈음
  • 브라우저 라우터(history mode)로 배포 시 정적 호스팅 환경(GitHub Pages 등)에서는
    직접 URL 접근 시 404가 발생하는 이슈를 처음 경험함 → 404.html과 쿼리 리다이렉션 스크립트로 해결

🚀 Try – 다음엔 이렇게 개선하고 싶다

  • 라우터 생성 시 base 값을 넘기고, 내부적으로 경로에 prefix를 붙이는 구조를 설계해보고 싶다:
const router = createRouter(rootElement, { base: "/my-project", isHashMode: false }); 

router.addRoute("/login", renderLoginPage); // 내부적으로 "/my-project/login" 등록됨
  • 상태 변화 시 전체 라우터 재시작(router.start())이 아닌, 경로별 컴포넌트 리렌더링 단위로 최적화해보는 방식 시도
  • authStore와 같은 전역 상태는 app/store 등 전용 위치로 분리하여 기능 로직과 앱 전역 흐름의 관심사를 명확히 구분
  • 정적 호스팅에서는 기본적으로 hash routing을 적용하고,
    browser routing을 쓸 경우에는 404.html과 index.html 간 리다이렉션 스크립트를 자동 포함시키는 구조 고려

✍️ 한 줄 회고

프레임워크 없이 SPA를 직접 구현하면서,
추상화된 개념들을 내 손으로 설계하고 조립해보며 진짜 프론트엔드 앱의 구조를 체득한 시간이었다.
또한 직접 만든 SPA를 GitHub Pages에 배포하며, 개발과 배포 사이의 경계를 넘는 경험을 해볼 수 있었던 한 주였다.