효뚜르팝의 Dev log
[항해 플러스] 1주차 회고 - 바닐라 JS로 SPA 구조 직접 구현해보기 본문
바닐라 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를 통해 정적 호스팅으로 직접 배포까지 진행해본게 재밌는 점중 하나다.
📦 배포 과정 요약
- 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에 배포하며, 개발과 배포 사이의 경계를 넘는 경험을 해볼 수 있었던 한 주였다.
'회고록' 카테고리의 다른 글
| [항해플러스] 3주차 회고 - React, Beyond the Basics (0) | 2025.04.13 |
|---|---|
| [항해플러스] 2주차 회고 - 프레임워크 없이 SPA 만들기 (0) | 2025.04.06 |
| 2024 회고 (3) | 2025.01.05 |
| 사이드프로젝트, Quokka Letter 회고 (5) | 2024.02.27 |
| 2023 회고 (3) | 2023.12.23 |