항해 플러스 7기 4주차

드엔트론프·2025년 11월 16일

항해플러스

목록 보기
4/9
post-thumbnail

4주차

SPA 바닐라 자바스크립트로 구현하기를 하는 주였다. 막 어렵다기보다 문제의 양과 시간 이슈, 어떻게 하면 더 잘하지 하는 고민들로 지나가는 한 주였다.

테스트 주차가 끝나고, 4, 5주차는 바닐라 자바스크립트를 활용하는 주다. 이번 4주차에는 바닐라 JS로 SPA 만들기였다.

이번 주차 3줄 요약

  1. 이번 주차 - 힘들었다. 하지만 여전히 배운건 있었다.
  2. 사람 만나는 걸 좋아한다. (기가 빨리지만)
  3. 착하게, 열심히 살자. 기회는 어디서 올 지 모르니까.

Keep

옵저버 패턴이라는 걸 공부하고 사용한 일, 어떤 방식으로 구현하면 더 좋을 지 고민한 일

옵저버 패턴

옵저버 패턴(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 컴포넌트를 호출한다.
  • HomePage 컴포넌트는 loading 상태를 받게 되면 로딩 상태의 메인 페이지를 그려준다.
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 })}
      		`,
        })}
      `;
	};    
  • 이 페이지 또한 SearchFormProductList 컴포넌트로 구성돼 있고, 이들이 로딩 상태에 따라 기다렸다가 화면을 그려주는 형태이다.

아무튼 중요한건, 이렇게 로딩 이후 그려진 화면에서 필터를 설정함에 따라(카테고리를 클릭하거나, 개수를 변경하거나, 상품명을 검색하거나) 상품이 다르게 그려져야 한다는것이고, 이러한 필터들의 상태는 다른 액션을 할때에도 유지돼야한다.

카테고리 선택 전선택하고 난 후
  • 지금까지는 이를 따로 처리 후 구현하다보니, 필터 상태에 대한 동기화가 없어 카테고리 선택했다가 상품명 검색하면 다 날아가고 상품명 검색에 대한 조건만 그려지는 거였다.

그래서 처음 작성했던 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 뿐 아니라 개수, 정렬등을 생각하면 그 각자의 상태를 유지하면서 가져와야하는데, 그것이 이슈였고 이를 위해 옵저버 패턴을 사용했다.

createStore

// 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: "" });
    }
  });
  • 맨 처음 init할 때 필터에 렌더 함수를 구독해둔다.
  • 어떤 이벤트가 발생했을 때 필터 변경이 필요하다면 setState를 통해 변경을 보내고, 이는 render함수를 다시한 번 호출하게 만든다.
  • 변경된 필터 상황을 토대로 다시 getProducts(filters.getState()) 하게 되고, HTML을 변경해준다!

Problem

라우팅, 라이프 사이클에 대한 부분을 크게 신경쓰지 못했다.

이번 과제에서는 크게 3가지를 알아가면 좋다고 했었다.
1. 라우팅
2. 상태 관리
3. 라이프 사이클

난 라우팅과 라이프 사이클에 대해서는 제대로 잡아가지 못하고 추후 팀원들 코드와 솔루션을 보며 그냥 감을 잡은 정도다.


Try

추후 감만 잡아본 라우팅과 라이프 사이클을 구현해보고, 좀 더 완성도 있는 바닐라 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(공백)*/ 을 정확히 맞춰주어야 정렬까지 된다는 점


마치며

  1. 핑계일수도 있지만 직장다니면서 병행하기가 참 쉽지 않다.
  • 앞서 Pass, Fail 과는 무관하게 매 주 나오는 키워드들을 습득하고 항해가 끝나고 다시금 정주행하는 느낌으로 해야지! 하는 마음이 있었는데, 매 번 Fail이 기본이다보니 이게 참 씁쓸하다.
    그래도 나름 열심히 하는데 흠,, 그래서 목표를 easy는 통과해보도록 하자로 삼으려 한다.그리고 남은 키워드들을 잘 갖고 있다가 항해가 끝나고 하나씩 더 파봐야지.

 

  1. 중간 네트워킹이 있었다.
  • 발제 이후 오프라인에 모인 7기 사람들과 네트워킹을 갖는 자리였다.
    진짜 재밌었는데, 사진이 많이 없는게 아쉽.
  • 아쉽지만 그나마 찍어놓은 네트워킹 시작에 담은 피자 사진을 메인으로 두었음. (피자 말고 엽떡이랑 육회도 시켜 먹었는데 사진을 안찍어둠)
  • 다른 팀과는 말 할 기회가 많이 없는데, 정식 네트워킹 시간인 9시 이후 더 남아있는 사람들과 자리를 모여 앉으며 잠시 말 할 기회를 갖게 됐다.
  • 항플 2번째라고, 이제는 아주 쉬워서 11시면 잔다고 거짓말했는데 믿는 분들 있어서 웃겨
  • 역시 사람들과 어울리는 걸 좋아하는 반 내향인 나

 

  1. 세상은 좁다 (착하게 살자)
  • 개발자 이전 운영/기획자로 근무하던 시절 거의 3년간 같이 일했던 'A 형님'이 있는데, 그 이후에도 계속 연락을 주고받았다. 최근에도 연락을 주고받는 와중에, 항해 학습 메이트 중 한 분이 블로그에 A 형님 회사로 이직했다고 적어둔 걸 봤다.
  • 신기해서 A 형님이 마침 카톡왔을 때, 오 거기에 저 공부하는 곳에 있는 학습 메이트 분이 이직하셨다고 하더라~ 했더니 인싸인 A 형님이 그 분에게 가서 인사를 나누셨다고 한다.
  • 이후에 학습메이트 분께서 DM 주시고, 이번 중간 네트워킹 때 대화를 조금 나누었다.
  • 해당 도메인에 관한 이야기를 잠시 나누다보니, 내가 잘 알고있는 도메인임에도 개발자로 갔을 때 내가 이해한대로 구현할 수 있을까 진짜..? 싶은 생각도 들고,, 더 열심히 살아야겠다는 생각이 가득 들었다.
profile
왜? 를 깊게 고민하고 해결하는 사람이 되고 싶은 개발자

2개의 댓글

comment-user-thumbnail
2025년 11월 16일

중네때 사진 저도 못찍어서 너무 아쉽더라구요,,🥹

1개의 답글