-
Notifications
You must be signed in to change notification settings - Fork 49
[3팀 남은주] Chapter2-1. 프레임워크 없이 SPA 만들기 #55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- src/templates 디렉토리에 8개 템플릿 파일 생성 - layout, search, product, common, cart, detail, notFound 템플릿 분리 - main.js에 state 객체 및 데이터 로딩 로직 추가 - MSW 초기화 버그 수정 (enableMocking().then(() => initMain())) - 로딩 상태 표시 및 상품 목록 렌더링 기능 구현
- 장바구니 모달에 전체 선택/개별 선택 체크박스 추가 - 체크박스 상태를 store에 저장하고 localStorage에 유지 - 선택한 상품만 삭제하는 기능 구현 - 선택된 상품 총액 표시 기능 추가 - change 이벤트로 체크박스 상태 관리 - store mutations 추가: SELECT_ALL_CART_ITEMS, SET_CART_ITEM_SELECTED - ESLint 오류 수정: 미사용 import 및 변수 제거
- vite.config.js: 개발/프로덕션 환경에 따라 BASE_URL 분리 (개발: /, 프로덕션: /front_7th_chapter2-1/) - main.js: 검색/필터 변경 시 명시적으로 renderMainPage() 호출하여 DOM 업데이트 보장 - main.js: 로딩 상태에 products-grid 추가 및 data-testid 속성 추가 - main.js: Observer 패턴을 통한 products-grid innerHTML 직접 업데이트 - product.js: 빈 상품 목록에도 products-grid 요소 렌더링 보장 - urlUtils.js: 필터 파라미터를 항상 URL에 포함 (기본값 제외 로직 제거) 이제 모든 E2E 테스트(기본 14개, 심화 11개)가 통과합니다.
JunilHwang
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 피드백은 n8n + ai (gpt-5-mini)를 활용하여 자동으로 생성된 내용입니다.
전체 리뷰 요약
이번 리팩토링된 프로젝트는 옵저버 패턴을 활용한 상태 관리와 Vuex 스타일의 구조를 적극 도입해, 단순 바닐라 JS에서도 SPA에 가까운 사용자 경험을 제공하도록 설계되었습니다. 상태 변화에 따라 자동으로 특정 DOM 업데이트를 수행하고, URL 쿼리 파라미터와 화면 상태 연동, 그리고 상세 페이지와 장바구니 기능까지 통합 구현된 점이 인상적입니다.
현재 코드 구조
src/core/observer.js: 옵저버 패턴 기반 반응형 상태관리 구현, 한 개 전역currentObserver사용src/core/Store.js: Vuex 스타일 store 클래스,commit,dispatch패턴 적용src/store.js: 상태 및 mutations, actions 정의src/main.js: SPA 라우팅과 이벤트 관리, 옵저버 등록, DOM 렌더링 직접 제어src/util/urlUtils.js: URL 관리 유틸src/templates/: 다양한 UI 컴포넌트 템플릿 모음
개선 권장 사항
- 옵저버 중첩 및 복수 등록 지원을 위해
currentObserver를 스택 구조로 변경 - 배열 및 깊은 객체 상태 변화 감지 기능 추가해 반응형 완성도 향상
- 렌더링과 상태관리를 완전히 분리하고, 상태 변경 시 자동 렌더링(옵저버 함수) 활용 강화
- 이벤트 위임 일관성 유지, 불필요한 이벤트 전파 차단 최소화
- 장바구니 모달 등 UI는 상태에 따른 렌더링 함수로 완전히 대체해 DOM 조작 최소화
- URL과 라우팅 관련 로직을 별도 모듈(혹은 라우터 클래스)로 분리해 쉽게 분기 관리
- 코드 곳곳에 주석과 명확한 변수명을 보완하여 유지보수성 향상
좋은 점
- Vuex 스타일의 명확한 상태 관리로 이해하기 쉬움
requestAnimationFrame을 활용한 옵저버 최적화로 성능 고려- URL 유틸 함수와 SPA 네비게이션 구현으로 사용자 경험 향상
- HTML 템플릿을 모듈화해 UI 재사용성 좋음
- 철저한 E2E 테스트로 로직 안정성 보장
결론
현재 구현은 학습용으로 매우 좋은 수준이며, 실제로 작동하는 완성된 SPA 형태입니다. 다만 옵저버 패턴 완전한 구현, 렌더링 자동화, 이벤트 관리 일원화 등을 추가하면 유지보수성과 확장성이 훨씬 향상됩니다.
초보 개발자에게 옵저버 패턴과 상태 관리의 핵심 개념을 잘 체험할 수 있는 코드입니다. 필요에 따라 조금씩 리팩토링을 진행하며 더 깊이 있는 학습으로 발전하길 응원합니다. 🎉## 옵저버 패턴 구현 관련 피드백 및 학습 추천
1. 현재 구현은 옵저버 패턴의 기본 원리를 잘 반영하고 있습니다.
- 상태 객체를 getter/setter로 감싸고,
- getter 내에서 현재 관찰자 등록, setter에서 변경 시 관찰자 호출,
debounceFrame으로 성능 최적화까지 고려한 점이 훌륭합니다.
2. 개선할 점 및 보완 아이디어
-
복수 옵저버 동시 관리:
현재는currentObserver에 한 개 함수만 담기 때문에,
관찰 함수가 중첩 실행되거나 여러 개일 때 혼란이 발생합니다.
관찰자 스택 구조를 도입해 이 문제를 해결하세요. -
중첩 상태와 배열 감지:
observable은 1-depth 속성만 감지합니다. 깊은 상태, 배열 변화를 자동 감지하려면 재귀 호출과 배열 메서드 재정의가 필요합니다. -
자동 렌더링 구조 고민:
옵저버 패턴의 목적은 상태 변화에 맞춰 UI가 자동으로 변경되는 것입니다.
render함수를 수동 호출하는 방식과 섞이면 중복렌더링과 복잡한 흐름이 생긴다.
페이지 요소별로 옵저버를 세분화하고, 상태 변화마다 필요한 부분만 렌더하세요. -
단일 책임 원칙:
옵저버 패턴과 상태관리는 상태만 관리하고,
UI 업데이트는 별도 렌더링 모듈에 맡기는 게 좋습니다.
3. 잘 구현된 부분
requestAnimationFrame이용해 옵저버 콜백 디바운싱 처리Set자료구조를 활용해 중복 옵저버 등록 방지- Vuex 스타일 commit/dispatch 구조 구축
- 명확하게 분리된 상태, 뮤테이션, 액션 역할 구분
4. 학습 추천
-
ES6 프록시(Proxy)와 Reflect
- 객체, 배열 반응성 구현 기술 이해를 위해 Proxy 학습 추천
-
리액티브 프레임워크 설계 원리
- Vue 3 Composition API, MobX, Recoil 등의 상태관리 아키텍처 이해
-
이벤트 위임과 DOM 이벤트
- 이벤트 버블링, 위임, 객체지향 이벤트 관리 학습
-
비동기 제어와 디바운싱/스로틀
- 애니메이션 프레임 기반 최적화 원리 학습
-
SPA 라우팅과 URL 관리
- History API, 쿼리파라미터 동기화 등 SPA 핵심 원리들
종합하면, 지금처럼 기본적인 옵저버 패턴 구현에서 시작해, 점차 깊은 중첩 상태 대응, 배열 감지, 컴포넌트 기반 렌더링, 라우팅 분리 등 고급 주제로 확장하는 학습 방향을 추천드립니다.
궁금한 점 있으면 언제든 질문해 주세요! 함께 공부해 나가봅시다. 🚀
| currentCallback = requestAnimationFrame(callback); | ||
| }; | ||
| }; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1. 옵저버 관리와 재사용성 문제
문제 상황:
현재 옵저버 패턴 구현은 currentObserver 전역 변수를 한 개만 사용하여 단일 함수의 관찰자만 등록할 수 있습니다. 만약 2개 이상의 관찰 함수가 동시에 관찰 중일 경우, currentObserver가 덮어써질 수 있고 이는 잘못된 의존성 등록으로 이어질 수 있습니다.
현재 코드 한계:
- 전역
currentObserver변수에 하나의 함수만 저장 가능 - 재귀 혹은 중첩된
observe호출시 정확한 추적 불가능 - 복수의 관찰자 등록 관리에 한계
개선 제안:
currentObserver를 스택(stack) 구조로 변경해 중첩 관찰도 지원observe호출 시 현재 콜백을 스택에 추가하고, 완료 후 제거- 이 프로세스가 공식 Vue나 MobX와 같은 리액티브 라이브러리의 기본 원리
// 개선 방향 예시
const observerStack = [];
export const observe = (fn) => {
const debouncedFn = debounceFrame(fn);
observerStack.push(debouncedFn);
fn();
observerStack.pop();
};
export const observable = (obj) => {
Object.keys(obj).forEach((key) => {
let _value = obj[key];
const observers = new Set();
Object.defineProperty(obj, key, {
get() {
if (observerStack.length > 0) {
observers.add(observerStack[observerStack.length - 1]);
}
return _value;
},
set(value) {
if (_value === value) return;
_value = value;
observers.forEach((fn) => fn());
},
});
});
return obj;
};| let _value = obj[key]; | ||
| const observers = new Set(); | ||
|
|
||
| Object.defineProperty(obj, key, { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
2. 깊은 객체 및 배열 반응성 부재
현재 observable 함수는 객체의 1-depth 속성에만 적용되고, 배열 내 변경 감지를 지원하지 않습니다.
한계점:
- 중첩 객체 변경 시 옵저버가 동작하지 않음
- 배열 메서드(push, pop 등)에 반응하지 않음
개선 방법:
- 재귀적으로 nested 객체를
observable처리 - 배열 메서드를 오버라이드해 변경 발생 시 옵저버 실행
예시:
if (typeof _value === "object" && _value !== null) {
_value = observable(_value);
if (Array.isArray(_value)) {
const arrayMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
arrayMethods.forEach(method => {
const original = _value[method];
_value[method] = function (...args) {
const result = original.apply(this, args);
observers.forEach(fn => fn());
return result;
};
});
}
}| console.error("초기화 실패:", error); | ||
| renderError("데이터를 불러오는데 실패했습니다."); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
3. 상태 변경 후 명시적 렌더 호출과 옵저버 패턴 혼용
지금 코드는 상태가 바뀌면 옵저버가 등록된 일부 UI를 자동으로 업데이트하지만, 렌더링 함수(예: renderMainPage())를 명시적으로 호출하는 부분도 많습니다.
문제:
- 중복된 렌더링 호출 가능성
- 옵저버 패턴의 장점인 자동 UI 갱신 효과가 충분히 활용 안 됨
개선 방향:
- 컴포넌트별로 세분화된 observer 함수 등록을 해서 상태 변경 시 자동으로 필요한 부분만 렌더링
- 전체 화면을 수동으로 다시 그리지 말고, 각 영역을 옵저버 내에서 독립적으로 관리
- 기존 렌더 호출은 제거하거나 최소화
|
|
||
| // 카테고리 필터 업데이트 관찰 | ||
| observe(() => { | ||
| const targetDiv = document.getElementById("category-filters"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
4. 이벤트 핸들링 시 이벤트 중복 등록 가능
addEventListeners() 함수가 여러 번 호출되면 이벤트 리스너가 중복 등록되어 동일 이벤트가 여러번 발생할 수 있습니다.
현재 코드:
- 플래그
eventListenersInitialized를 사용하여 1회 등록 보장 - 그러나 초기화 흐름 복잡해지면 누락이나 중복 리스크 존재
개선 제안:
- 이벤트 위임과 전역 한번 등록을 통해 안정적으로 관리
- 특정 위임 대상만 명확히 필터링하는 방식 권장
|
|
||
| const newSort = event.target.value; | ||
| store.commit("SET_FILTERS", { sort: newSort }); | ||
| store.commit("SET_CURRENT_PAGE", 1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
5. 장바구니 모달 관리: DOM 직접 조작과 상태 불일치
장바구니 모달 내에서 수량 변경, 선택 삭제시 DOM과 상태관리를 둘 다 직접 하면서 관리하고 있어 복잡성을 가중시키고 있습니다.
문제점:
- 상태와 UI가 강하게 분리되지 않아, 업데이트 버그 발생 가능성
- 상태 변경 흐름 추적이 어렵고, 유지보수 어려움
개선 방안:
- 상태를 단일 소스로 관리하고, 옵저버 패턴을 이용해 모달 UI를 갱신하는 편이 간결함
- UI 업데이트는 상태 변경에 의한 자동화로 통해 일관성 유지
|
|
||
| APPEND_PRODUCTS(state, products) { | ||
| state.products = [...state.products, ...products]; | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
6. 중복 코드 및 불필요한 state 설정
ADD_TO_CART mutation에서 state.cart에 새 아이템을 추가할 때는 배열 복사(state.cart = [...state.cart, item])로 불변성을 유지하지만,
UPDATE_CART_ITEM 등은 직접 map을 사용해 반환하여 대체하는 방법으로 일관성은 있으나
전체 상태 변경 방식을 좀 더 명확하게 통일하면 좋겠습니다.
| } catch (error) { | ||
| console.error("상품 상세를 불러오는데 실패했습니다:", error); | ||
| } finally { | ||
| commit("SET_IS_LOADING", false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
7. addToCart 액션: 중복 아이템 확인 로직 개선 가능
addToCart 액션이 같은 상품이 수량이 동일하면 추가하지 않는 로직인데, 만약 수량이 달라지면 기존 아이템 수량만 변경합니다.
개선 사항:
- 수량 변경 시 새로 추가하는 것보다, 선택적으로 기존 수량에 더하거나 교체하는 정책 선택 필요
- 비즈니스 로직에 따라 구현 세부 조정 가능
|
|
||
| // 상품 정보 찾기 | ||
| let product = store.state.products.find((p) => p.productId === productId); | ||
| if (!product && store.state.detailProduct.productId === productId) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
8. URL 관리 및 탐색 함수 분리 및 명확화
URL 관련 유틸 함수들이 util/urlUtils.js에 분리되어 있어 깔끔하지만,
navigate() 함수내페이지 분기 로직이 너무 크고, 유지보수를 어렵게 할 수 있습니다.
개선 사항:
- 각 페이지별 렌더링, 데이터 로딩 로직을 별도 모듈로 분리
Router클래스를 도입해 경로 기반 라우팅 관리- SPA 네비게이션 체계 강화
| <input type="text" id="search-input" placeholder="상품명을 검색해보세요..." value="" class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg | ||
| focus:ring-2 focus:ring-blue-500 focus:border-blue-500"> | ||
| <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> | ||
| <svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
9. 이벤트 위임 방식 통일성 유지 필요
document.body.addEventListener로 여러 이벤트를 위임하고 있으나,
stopPropagation() 호출 위치가 일관적이지 않고, 이벤트가 불필요하게 차단될 위험 있음
개선 제안:
- 반드시 필요한 경우에만
stopPropagation()호출 - 위임 전략, 이벤트 핸들러 조건 명확하게 설계
| <option value="50"> | ||
| 50개 | ||
| </option> | ||
| <option value="100"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
10. 직관적인 변수명과 주석 추가로 가독성 개선 가능
현재 변수명, 함수명이 대체로 명확하지만,
몇몇 함수(예: updateRemoveSelectedButton)의 역할이 직관적으로 이해되기 어렵거나,
복잡한 로직 내 중간 변수 이름이 불분명한 부분이 있습니다.
제안:
- 주요 함수, 복잡한 부분에 주석 보강
- 변수명에 역할 명시
- 함수 분리로 블록 길이 축소
과제 체크포인트
배포 링크
-- 배포링크 : https://amorpaty.github.io/front_7th_chapter2-1/
기본과제
상품목록
상품 목록 로딩
상품 목록 조회
한 페이지에 보여질 상품 수 선택
상품 정렬 기능
무한 스크롤 페이지네이션
상품을 장바구니에 담기
상품 검색
카테고리 선택
카테고리 네비게이션
현재 상품 수 표시
장바구니
장바구니 모달
장바구니 수량 조절
장바구니 삭제
장바구니 선택 삭제
장바구니 전체 선택
장바구니 비우기
상품 상세
상품 클릭시 상세 페이지 이동
/product/{productId}형태로 변경된다상품 상세 페이지 기능
상품 상세 - 장바구니 담기
관련 상품 기능
상품 상세 페이지 내 네비게이션
사용자 피드백 시스템
토스트 메시지
심화과제
SPA 네비게이션 및 URL 관리
페이지 이동
상품 목록 - URL 쿼리 반영
상품 목록 - 새로고침 시 상태 유지
장바구니 - 새로고침 시 데이터 유지
상품 상세 - URL에 ID 반영
/product/{productId})상품 상세 - 새로고침시 유지
404 페이지
AI로 한 번 더 구현하기
과제 셀프회고
리액트에서 상태관리를 간단히 해보기는 했으나 자바스크립트만으로 개발해오던 저에겐 이번 과제는 신세계이지 않나 싶습니다.
구현해야할 코드 양이 많고 또 상태관리를 어떻게 해야하는가에 대해 고민이 되서 처음에는 전역변수로 state를 두고 시작을했습니다.
어떻게 구현을 해야할지 모르다가 코치님의 블로그를 보고 옵저버 패턴 형식으로 진행하면 되겠다고 생각해서 진행했는데요.
라우팅과 상태관리는 아직도 솔직히 잘 모르겠습니다. 이론을 좀 더 코치님께서 알려주시면 좋을 것 같다는 생각이 들었던 과정이었던 것 같습니다. 왜냐하면 저같은 경우 찾아보는데에도 시간이 걸려서 만약 찾아보기가지했다면 과제는 못하지 않았을까... 하는 생각이 듭니다. ㅠㅠ 물론 과제 제출보다도 이해하는 과정이 중요하겠지만 이해하고 코드를 짜보지 못한다면 그것 또한 의미가 없지 않았을까... 하는 생각입니다. ㅠㅠ
개발자로서 아직 부족한 면이 많이 보인 한 주라 조금 위축된 것 같습니다. ㅎㅎ
기술적 성장
개선이 필요하다고 생각하는 코드
는 store.commit과 dispatch를 구현했는데 dispatch를 사용해야만 dom이 바뀌는 방식이라 수동 처리였습니다...
그래서 useState를 구현했다고 보기 어렵고... 아직도 어떻게 구현을 해야하는지 잘 모르겠어서... 전체적으로 개선이 필요할 것 같습니다.
AI 활용 경험 공유하기
코파일럿 요물인 것 같네요 ... ㅎㅎ 일단 테스트 코드가 짜여져 있다보니 코파일럿이 알아서 테스트 코드에 맞추어 짜주는 것도 있더라구요 디테일한 요구가 필요할 때는 최소 기능 단위로 물어보도록 하는 것이 제일 좋긴 하지만 거의 큰 틀에 대해서는 코파일럿이 알아서 인지하고 있어서 개발하는데 많은 도움을 받았습니다. ㅎㅎ
리뷰 받고 싶은 내용
옵저버 패턴을 사용해 상태 관리 로직을 구현해 보려 했습니다. 제가 구현한 코드가 옵저버 패턴에 맞게 잘 구성되었는지 검토해 주시고, 보완할 부분을 제안해 주실 수 있을까요?
혹 제 코드를 검토하시면서 그래도 이건 잘 구현한 것 같다라고 생각되는 부분이 있다면 하나 꼽아주실 수 있을까요?
(코드에 자신이 없음)
코드는 동작하지만 아직 완전히 이해하지 못한 부분이 전반적이라 어떤 학습이 필요할지 추천해주시면 좋겠습니다.