SPA 바닐라 자바스크립트로 구현하기를 하는 주였다. 막 어렵다기보다 문제의 양과 시간 이슈, 어떻게 하면 더 잘하지 하는 고민들로 지나가는 한 주였다.
테스트 주차가 끝나고, 4, 5주차는 바닐라 자바스크립트를 활용하는 주다. 이번 4주차에는 바닐라 JS로 SPA 만들기였다.
이번 주차 3줄 요약
- 이번 주차 - 힘들었다. 하지만 여전히 배운건 있었다.
- 사람 만나는 걸 좋아한다. (기가 빨리지만)
- 착하게, 열심히 살자. 기회는 어디서 올 지 모르니까.
옵저버 패턴이라는 걸 공부하고 사용한 일, 어떤 방식으로 구현하면 더 좋을 지 고민한 일
옵저버 패턴(Observer Pattern)은 옵저버(관찰자)들이 관찰하고 있는 대상자의 상태가 변화가 있을 때마다 대상자는 직접 목록의 각 관찰자들에게 통지하고, 관찰자들은 알림을 받아 조치를 취하는 행동 패턴
옵저버 패턴을 사용한 이유는, 페이지 내에서의 필터에 따른 관리 때문이었다.
처음 이러한 패턴 같은 것 없이 작성했었다.
가령, 카테고리를 선택하고 검색창을 입력했을 때라던가, 개수를 변경하고 정렬을 바꾸는 등 4개 정도의 조건들이 있는데, 이를 상태관리로 엮지 않으면 전체 조건을 유지하기가 무척 피곤하게 된다.
안해도 되는 방법을 찾고있었는데, 이번 과제가 SPA를 만들어보기이고, 라이프사이클과 라우터와 함께 중요한 배움 중 하나라고 코치님도 이야기 해주었다
그렇다면, 어떻게 접근해볼까 ?
화면은 render 함수를 통해 그려지게 해두었었다.
const init = async () => {
const $root = document.getElementById("root");
if (window.location.pathname === "/") {
$root.innerHTML = HomePage({ isLoading: true });
const data = await getProducts();
const categories = await getCategories();
$root.innerHTML = HomePage({ ...data, categories, isLoading: false });
...
...
}
async function main() {
init();
}
main에서는 render함수가 실행된다. index의 root를 들고와서, 메인(”/”) 일 때 HomePage 컴포넌트를 호출한다.import { PageLayout } from "./PageLayout";
import { SearchForm, ProductList } from "../components";
export const HomePage = ({ filters, pagination, products, categories, isLoading = false }) => {
return /* HTML */ `
${PageLayout({
children: `
${SearchForm({ filters, categories, isLoading })}
${ProductList({ pagination, products, isLoading })}
`,
})}
`;
};
SearchForm 과 ProductList 컴포넌트로 구성돼 있고, 이들이 로딩 상태에 따라 기다렸다가 화면을 그려주는 형태이다. 아무튼 중요한건, 이렇게 로딩 이후 그려진 화면에서 필터를 설정함에 따라(카테고리를 클릭하거나, 개수를 변경하거나, 상품명을 검색하거나) 상품이 다르게 그려져야 한다는것이고, 이러한 필터들의 상태는 다른 액션을 할때에도 유지돼야한다.
| 카테고리 선택 전 | 선택하고 난 후 |
|---|---|
![]() | ![]() |
그래서 처음 작성했던 breadcrumb 변경되는 부분을 이렇게 작성했다.
// breadcrumb 카테고리 1 버튼 클릭 시 필터 적용
document.addEventListener("click", async (event) => {
if (event.target.closest("button[data-breadcrumb='category1']")) {
const category1 = event.target.closest("button[data-breadcrumb='category1']").dataset.category1;
const data = await getProducts({ category1 });
$root.innerHTML = HomePage({ ...data, categories, isLoading: false });
// 카테고리 1 필터에 적용
const categoryBreadcrumb = CategoryBreadcrumb(category1);
// data-breadcrumb="reset" 옆에 categoryBreadcrumb를 추가
const categoryBreadcrumbContainer = document.querySelector("button[data-breadcrumb='reset']");
categoryBreadcrumbContainer.insertAdjacentHTML("afterend", categoryBreadcrumb);
// 카테고리 2 목록 버튼 보여주기
const category2Buttons = Object.keys(categories[category1])
.map((category2) => Category2Button(category1, category2))
.join("");
const categoryFilterButtons = document.getElementById("category-filter-buttons");
categoryFilterButtons.innerHTML = category2Buttons;
}
});
위 사진에서 카테고리: 전체 > 디지털/가전 > 노트북 에서 디지털/가전 breadcrumb을 누르면 breadcrumb이 바뀌어야 되니 그걸 다시 바꾸고, 아래 나오는 카테고리도 다시 디지털/가전에서 나오는 카테고리로 바꾸고.. 이렇게 명시적으로 작성해줬어야되는 일이었다. 이게 breadcrumb 뿐 아니라 개수, 정렬등을 생각하면 그 각자의 상태를 유지하면서 가져와야하는데, 그것이 이슈였고 이를 위해 옵저버 패턴을 사용했다.
// store/store.js
export function createStore(initialState) {
let state = initialState;
const observers = new Set();
const getState = () => state;
const setState = (rest) => {
state = { ...state, ...rest };
observers.forEach((observer) => observer(state));
};
const subscribe = (render) => {
observers.add(render);
return () => observers.delete(render);
};
return { getState, setState, subscribe };
}
// store/filters.js
import { createStore } from "./store";
export const filters = createStore({
limit: "20",
search: "",
category1: "",
category2: "",
sort: "price_asc",
});
나는 필터에 관련된 내용만 따로 빼서, 필터가 변경 될 때 이를 감지하고 다시 렌더해줘 ! 를 그릴 수 있었다.
// main.js
const init = async () => {
const $root = document.getElementById("root");
$root.innerHTML = HomePage({ isLoading: true });
const data = await getProducts();
...
...
filters.subscribe(render);
};
const render = async () => {
..
생략
...
const $root = document.getElementById("root");
const data = await getProducts(filters.getState());
$root.innerHTML = HomePage({ ...data, categories, isLoading: false });
...
...
};
// breadcrumb 전체 버튼 클릭 시 필터 초기화
document.addEventListener("click", (event) => {
if (event.target.closest("button[data-breadcrumb='reset']")) {
filters.setState({ category1: "", category2: "" });
}
});
setState를 통해 변경을 보내고, 이는 render함수를 다시한 번 호출하게 만든다.getProducts(filters.getState()) 하게 되고, HTML을 변경해준다!라우팅, 라이프 사이클에 대한 부분을 크게 신경쓰지 못했다.
이번 과제에서는 크게 3가지를 알아가면 좋다고 했었다.
1. 라우팅
2. 상태 관리
3. 라이프 사이클
난 라우팅과 라이프 사이클에 대해서는 제대로 잡아가지 못하고 추후 팀원들 코드와 솔루션을 보며 그냥 감을 잡은 정도다.
추후 감만 잡아본 라우팅과 라이프 사이클을 구현해보고, 좀 더 완성도 있는 바닐라 JS SPA 페이지를 만들어보고싶다.
바닐라 JS로 작성하게 되면 주로 백틱을 사용해서 컨텐츠를 그려주는데, 이게 정렬이 안돼서 처음에 엄청 빡셌다.
이건 vscode 익스텐션에 es6-string-html 이라는 걸 설치하면 되는데,

이걸 설치 후 사용하는 건 아래처럼 사용하면 된다.
export const PageLayout = ({ children, title = "쇼핑몰" }) => {
return /* HTML */ `
<div class="min-h-screen bg-gray-50">
${Header({ title })}
<main class="max-w-md mx-auto px-4 py-4">${children}</main>
<div class="cart-modal-container"></div>
${Footer()}
</div>
`;
};
여기 return 다음에 /* HTML */ 로 적어주면 정렬과 하이트라이트가 모두 된다!
중요한건,
/*html*/,/*HTML*/다 안되고,/*(공백)HTML(공백)*/을 정확히 맞춰주어야 정렬까지 된다는 점
중네때 사진 저도 못찍어서 너무 아쉽더라구요,,🥹