-
Notifications
You must be signed in to change notification settings - Fork 50
[1팀 박용태] Chapter2-1. 프레임워크 없이 SPA 만들기 #32
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
31103ae to
ea17754
Compare
c1cdc92 to
9b09aa3
Compare
c645a3d to
45b75f2
Compare
0eb74af to
6813d00
Compare
6813d00 to
10947c0
Compare
10947c0 to
c1f8eee
Compare
c401f55 to
41835fc
Compare
41835fc to
76b29d1
Compare
a283dd4 to
c432713
Compare
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)를 활용하여 자동으로 생성된 내용입니다.
전체 리뷰 요약
이번 PR은 vanilla JS 컴포넌트 기반 SPA 구현으로, 요구사항인 상품 목록, 필터, 정렬, 무한 스크롤, 장바구니, 상품 상세, 토스트, 라우팅 등 주요 기능을 모두 구현하였습니다. 타입스크립트 설정과 DTO로 API 데이터 타입 안정성 보장, 상태 관리(store) 구축, 라우터 구성, 뷰모델로 UI 상태 분리 등 설계가 좋은 기반을 갖추고 있습니다.
주요 구조
createComponent유틸리티로 선언적 컴포넌트 생성 및 라이프사이클, 이벤트 위임 관리appStore전역 상태 관리 및 로컬 스토리지 연동- Router 모듈로 SPA 라우팅 및 주소 관리
- API함수를 DTO 매핑하여 API 변경에 유연하게 대응
- ViewModel 계층으로 복잡한 UI 상태를 분리
- 페이지별 컴포넌트(
HomePage,ProductDetailPage)로 비즈니스 로직 캡슐화
개선 방향
- 상태와 URL 쿼리를 단일 책임 원칙에 맞게 분리, 상태 관리 체계와 URL 동기화 로직 일원화 권장
- 전역 상태 관리 활용 시 중복된 로컬 상태 관리를 지양하여 상태 불일치 방지
- 이벤트 핸들러 내 DOM 조작과 이벤트 타겟 탐색을 엄격히 하여 예외 케이스 대비
- 컴포넌트 간 이벤트 핸들러 전달 및 래핑 구조 간결화
- URL 조합과 라우터 경로 처리 로직에서 경로 합성 방식 통일화
이 구조는 유지보수성과 확장성 기반 마련에 적합하지만, 더 큰 규모에서는 상태 관리 라이브러리 도입, 모듈화 강화, 이벤트 위임 명확화 등이 필요할 것입니다.### 1) 작업 및 PR 작성 방식 관련 피드백
학습 단계에서 여러 방향을 고민하고 다양한 시도를 해보는 것은 매우 중요한 성장 과정입니다. 최종 결과 완성도도 중요하지만, 그 과정에서 고민과 선택, 경험이 쌓이는 것이 본질적 가치입니다. 다만 실제 업무에서는 명확한 목표 설정과 효율적 방향 전환이 중요하니, 상황에 따라 집중 대상과 적절한 타협점을 찾는 연습도 필요합니다.
코치의 경험을 토대로 몇 가지 제언을 드립니다:
- 작업 중간 중간 작게라도 자주 커밋하고 목적별, 기능별로 분리해두면 나중에 리뷰하고 읽기 좋습니다.
- PR 작성 시 현재 구현 목적, 문제점, 고민한 대안, 의뢰하고 싶은 포인트를 명확히 정리하면 리뷰어가 구체적으로 도움을 줄 수 있습니다.
- 중요한 변경 사항과 의도, 코드 패턴에 대해 간단한 설명을 주석이나 PR 코멘트에 포함하면 이해가 더욱 쉽습니다.
- 코드 스타일, 네이밍, 구조 등 통일성을 신경 쓰되 너무 완벽함에 집착하지 마세요.
2) 개인 PR 작성에서 고려할 점
- PR 제목과 본문은 분명하게 작성하기
- 변경한 파일과 대표적인 변경 사항을 정리하기
- 코드 리뷰에서 특히 보고 싶은 부분이나 애매한 부분 콕 집어 질문하기
- 테스트 수행 결과나 실행 방법 간단 기술
결론적으로, 자신의 학습 스타일과 업무 스타일을 점검하며, 현재 PR에 대해 다음 단계 목표를 세워 보시면 좋겠습니다. 질문해주셔서 감사드리며, 앞으로도 꾸준한 학습 응원하겠습니다! 🙏
| console.error("[HomePage] onMount error", error); | ||
| } finally { | ||
| setState("isLoading", 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.
1. 문제상황 제시
실제 상품 검색, 필터, 정렬 조건들이 URL 쿼리 파라미터에 반영되어 사용자 상태 공유와 새로고침 후 상태 유지가 요구될 때 현 구조에서는 URL 반영과 상태 동기화가 일부 중복 관리되며, 개선 없이 확장하면 관리하기 어렵습니다.
- URL 파라미터를 직접 업데이트하는 로직이 컴포넌트 내부 효과(effects)와 핸들러에 산재함
- 여러 상태 조작 함수 내에서 Router.updateQueryParams 호출이 분산됨
- 무한 스크롤과 초기 필터 로드 등 복잡한 상태 변화가 섞여 있어 유지보수가 어려움
2. 근본 원인
렌더링 함수 내에서 직접 URL 업데이트, 로컬 상태관리, API 호출 로직이 함께 다뤄지면서 관심사 분리가 약해지고, 상태 관리와 사이드 이펙트가 혼재되어 있습니다.
3. 개선 구조
- 현 구조: HomePage 컴포넌트 내 상태 관리 + 렌더링 + URL 쿼리 업데이트 + API 호출 (한 곳에서 복합적으로)
- 개선 구조: 상태 관리(Store 또는 전역 상태)와 URL 동기화를 별도의 모듈로 분리, HomePage는 상태 변경 알림 수신 + 단순 렌더링만 담당
개선 사항:
- URL 쿼리 상태와 내부 상태를 1:1 대응시키는 커스텀 훅/모듈 분리
- 상태 변경 시 URL 업데이트 로직을 일원화하여 중복 호출을 방지
- API 호출은 상태 변화에 따른 단일 Effect로 처리하여 의존성 명확화
// ❌ 현 상태 변경 핸들러 예
handleSetSort: (value) => {
setState("listResponse", (curr) => ({
...curr,
filters: { ...curr.filters, sort: value },
}));
Router.updateQueryParams({ sort: value });
},
// ✅ 개선안 (예)
import { syncFiltersWithUrl } from './url-sync';
function handleSetSort(value) {
const newFilters = { ...filters, sort: value };
setFilters(newFilters);
syncFiltersWithUrl(newFilters); // 중앙화된 함수
}추가적인 전역 상태 사용 및 이벤트 위임 관리 고려
현재 컴포넌트 생성방식은 효과적이나, 전역 상태 및 이벤트 위임이 가능한 구조 도입 시 확장과 유지보수가 더 용이해질 수 있습니다.
| stroke-linecap="round" | ||
| stroke-linejoin="round" | ||
| stroke-width="2" | ||
| d="M3 3h2l.4 2M7 13h10l4-8H5.4m2.6 8L6 2H3m4 11v6a1 1 0 001 1h1a1 1 0 001-1v-6M13 13v6a1 1 0 001 1h1a1 1 0 001-1v-6" |
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. 불필요한 중복 상태 변경 문제
handleIncreaseQuantity, handleDecreaseQuantity 등 함수 내부에서 setState와 appStore.setCart를 연속 호출하며 두 상태값을 별도로 관리하고 있는데, 상태 불일치 및 불필요한 렌더링 유발 가능성이 있습니다.
2. 근본 원인
로컬 컴포넌트 상태와 전역 상태(appStore)가 모두 동일한 데이터를 관리하며 동기화 책임이 분산되어 있음
3. 개선 구조
- 전역 상태(appStore)를 단일 출처로 하여 로컬 state 제거하거나, 전역 상태를 구독하여 로컬 상태와 혼용하지 않도록 개선
- setState는 appStore 변경에 따른 구독 콜백에서 처리하게 하여 data flow를 단방향으로 유지
// ❌ 현재 방식
const handleIncreaseQuantity = (productId) => {
setState("cart", (cur) => /* ... */);
appStore.setCart(cart.map(/* ... */));
};
// ✅ 개선 예시
const handleIncreaseQuantity = (productId) => {
appStore.addCartItemCountByProductId(productId);
};
// 컴포넌트는 appStore 구독으로만 cart 상태를 받음| if (isRendering) return; | ||
| handler(currentProps, getState, setState, event); | ||
| }, | ||
| ]), |
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. 이벤트 위임 시 일관성 및 성능 이슈
문서 전체에 이벤트 리스너를 부착할 때 모든 이벤트에서 조건 검사 후 핸들러를 실행하게 구현되어, 이벤트 빈도 높은 요소(예: input, scroll 등)에서 성능 저하 우려
2. 근본 원인
한 문서 전체의 이벤트를 capture phase 또는 bubble phase에서 전부 처리하면서 이벤트 필터링이 불분명하고, 잘못 처리될 여지가 있음
3. 개선
- 이벤트 타입별로 보다 명확하고 구체적인 위임 방식 적용
- 이벤트 위임 원칙에 충실히, 반드시 필요한 엘리먼트에 한정
- 버블링 단계 활용을 명확히 하여 이벤트 중복 처리 최소화
- Propagation 제어를 엄격히 하여 중복 호출 방지
| lprice: 0, | ||
| }, | ||
| eventHandlers: { | ||
| "navigate-to-detail": (props, getter, setter, event) => { |
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. eventHandlers에 직접 DOM 이벤트 의존
navigate-to-detail 핸들러가 이벤트 타겟 확인 로직이 불완전해, 제품 이미지 외 다른 영역 클릭 시도시 버그가 발생할 위험
2. 근본 원인
DOM 이벤트 타겟 검사 구현이 단순히 event.target에 의존해서 정확한 최상위 타겟을 확인하지 않음
3. 개선
- 이벤트 타겟 대신 이벤트 위임 시 이벤트가 발생한 컴포넌트 루트 엘리먼트 혹은 적절한 상위 엘리먼트에서 데이터 속성을 활용해 정확한 상품 ID 획득
event.target.closest()등 안전한 탐색 함수 사용
// ❌ 현재
const productId = event.target.dataset.productId;
// ✅ 개선
const productId = event.target.closest('[data-product-id]')?.dataset.productId;| <div class="flex items-center mb-3"> | ||
| <div class="flex items-center"> | ||
| <svg class="w-4 h-4 text-yellow-400" fill="currentColor" viewBox="0 0 20 20"> | ||
| <path |
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. 상품 수량 범위 하드코딩 문제
quantity-decrease와 quantity-increase 핸들러에서 수량 제한을 하드코딩(최대 107)하고 있으나, 재고(stock) 등의 외부 데이터와 동기화 되어 있지 않아 재고 변경 시 수량 제한이 일치하지 않을 위험 있음
2. 근본 원인
상품 상세 정보 내 stock 필드와 수량 input 제약 사이 연결고리가 없음
3. 개선
stock값을 맥스 값으로 동적으로 활용하도록 수정- 동적 범위 체크 및 UI 반영으로 재고와 수량 제약 일관성 유지
const stock = productDetailResponse?.stock || 999;
// 수량 증가 제한
if (getter("count") >= stock) return;| AFTER: appState.cart.find((item) => item.id === productId)?.count, | ||
| }); | ||
| appStore.setCart( | ||
| appState.cart.map((item) => (item.id === productId ? { ...item, count: Math.min(item.count + 1, 999) } : item)), |
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. 선택 삭제 함수 문제
removeSelectedCartItems 메서드 내부에 체크된 상품을 삭제하는 것이 아니라 선택 상태를 모두 false로 리셋하는 버그 있음
2. 근본 원인
함수 내부에서 선택한 상품 제거 로직이 아닌, 선택 여부를 모두 false로 변경하는 코드가 사용됨
3. 개선
- 선택된 상품만 필터링하여 cart에서 제거하도록 변경
// ❌ 현재
removeSelectedCartItems: () => {
appStore.setCart(appState.cart.map(item => ({ ...item, selected: false })));
},
// ✅ 개선
removeSelectedCartItems: () => {
appStore.setCart(appState.cart.filter(item => !item.selected));
},| const fullUrl = queryString ? `${url}?${queryString}` : url; | ||
|
|
||
| history.replaceState(null, "", fullUrl); | ||
|
|
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. URL 조작 시 경로 생성 로직 문제
push 메서드 내 경로 생성에서 basePath와 경로 연결 시 슬래시 처리 로직이 불명확하여 URL 중복 또는 누락 가능성 있음
2. 근본 원인
basePath 변수에 따라 슬래시가 중복될 위험이나, 일관적인 경로 합성 처리 부족
3. 개선
- URL 합성 시 URL 객체 또는 명확한 분리+조합 로직 사용 권장
// 기존
const fullUrl = queryString
? `${Router.basePath}${pathOnly.replace("/", "")}?${queryString}`
: `${Router.basePath}${pathOnly.replace("/", "")}`;
// 개선
const normalizedBase = Router.basePath.endsWith('/') ? Router.basePath.slice(0, -1) : Router.basePath;
const normalizedPath = pathOnly.startsWith('/') ? pathOnly : `/${pathOnly}`;
const fullUrl = queryString
? `${normalizedBase}${normalizedPath}?${queryString}`
: `${normalizedBase}${normalizedPath}`;| ${(!props.selectedCategory2 && !props.selectedCategory1 | ||
| ? firstDepthOptions.map( | ||
| (option) => /* HTML */ ` | ||
| <button |
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. 카테고리 선택 이벤트 핸들링 개선점
핸들러에서 event.target.dataset으로 직접 접근하는데, event.target이 예상과 다를 경우 오류 여지 있음
2. 근본 원인
이벤트 전파 과정에서 event.target이 버튼 외 다른 요소인 경우 처리 누락 가능성
3. 개선
event.target.closest를 활용해 명확한 버튼 요소를 찾아 처리
const button = event.target.closest('button[data-category1]');
if (!button) return;
const value = button.dataset.category1;
props.handleSetSelectedCategory1(value);이 같은 방식을 category2-filter 핸들러 등에도 적용 권장
| value="${props.search}" | ||
| 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" | ||
| /> |
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. prop 전달 불필요 중복 및 코드 정돈
FilterToolbox에서 핸들러를 래핑하여 다시 같은 인자를 넘기는 형태가 반복적임
2. 근본 원인
필요 이상으로 핸들러를 재정의 및 다시 전달하여 코드 양 증가
3. 개선
- 핸들러를 직접
props.handleXXX로 사용하는 것이 가독성/간결성에 도움
// ❌ 현재
const handleSetSort = (value) => {
props.handleSetSort(value);
};
// ✅ 개선
// templateFn 내부 바로 사용
// props.handleSetSort
과제 체크포인트
배포 링크
배포 사이트 링크
기본과제
상품목록
상품 목록 로딩
상품 목록 조회
한 페이지에 보여질 상품 수 선택
상품 정렬 기능
무한 스크롤 페이지네이션
상품을 장바구니에 담기
상품 검색
카테고리 선택
카테고리 네비게이션
현재 상품 수 표시
장바구니
장바구니 모달
장바구니 수량 조절
장바구니 삭제
장바구니 선택 삭제
장바구니 전체 선택
장바구니 비우기
상품 상세
상품 클릭시 상세 페이지 이동
/product/{productId}형태로 변경된다상품 상세 페이지 기능
상품 상세 - 장바구니 담기
관련 상품 기능
상품 상세 페이지 내 네비게이션
사용자 피드백 시스템
토스트 메시지
심화과제
SPA 네비게이션 및 URL 관리
페이지 이동
상품 목록 - URL 쿼리 반영
상품 목록 - 새로고침 시 상태 유지
장바구니 - 새로고침 시 데이터 유지
상품 상세 - URL에 ID 반영
/product/{productId})상품 상세 - 새로고침시 유지
404 페이지
AI로 한 번 더 구현하기
과제 셀프회고
할..말이... 너무 많은 프로젝트입니다.. (할말 정리 중..⏳..⌛️..)
전체적인 주요 작업의 흐름은,
입니다.
TL;DR
1) csr 라우트 이동을 구현하며, 중앙 집중식 구조가 필요함을 느껴
src/route.js의 선언적인 라우트 레지스트리를 구현1.5) Route 레벨의 데이터 loader 패턴 도입,
src/route.js의loader함수.2) 전역에서 접근 가능한 싱글톤 패턴의 정적 Router,
src/core/router/index.js3) 디테일한 상태 기반의 UI 업데이트를 위한 반응형 컴포넌트 시스템 도입,
src/core/component/create-component.js4.5) 라이프 사이클의 도입...
src/core/component/create-component.js- effect와 onMount5) 그 외..
전반적인 감상
위에서도 말했다시피 실제 현업이나 프로젝트를 진행할 때는 해보지 않아도 되는 고민들을 해보는 시간이었습니다. 지난 주차에서 현실에서 할법한 고민들을 밀도있게 고민해봤다면, 이번주에는 평소에 사용하는 도구를 깊게 까보고 도구 빌더의 입장에서 고민해볼 수 있는 경험이었다고 생각합니다.
과제 시작 -> 문제 발견 -> 문제 정의 -> (떠오르는 선례들.. 아~ 이래서 이게 필요했던 거구나..) -> 해결 방안 모색 -> 설계 -> 구현 -> 적용 -> 개선 -> 적용의 작업 흐름이 정말 재미있었습니다. 설계할 때는 머리가 쥐가 났지만, 많은 고민 끝에 만들어낸 결과물이 매끄럽게 워킹할 때는 큰 효능감을 느끼기도 했습니다.그 과정에서
특히, 대 AI시대에 부족한 기본기가 발목을 잡는 경험을 많이 했는데, 고런 부분을 밀도있게 다시 챙겨볼 수 있는 기회였다고 생각합니다.
추가로, 미리 gh page 배포를 하고 간단 배포 가이드를 팀원들에게 공유했다가 학습메이트들의 말에 떠밀려 잡담방에 다시 올리게 되었습니다. 뭐 문서의 퀄리티가 좋은 것은 아니지만, 누군가에게 공유해봐야만 느낄 수 있는 경험들이 재미있었습니다.
등.. 이를 계기로 앞으로 더 많은 공유를 시도해보려고 합니다.
아쉬운 점
테스트가 몇 개 실패한다.... 😱😱😱 ... 로컬에서는 성공하는데, 이런저런 이유로 실패하네요...
여전히.. 과제하는데 시간이 급급해 팀원들과 과제와 관려한 더 많은 대화를 나누지 못했다는 점이 아쉽습니다. 그래도 중간에 모각코도 하고 작업에 대한 공유도 했지만, 뭔가 아직은 아쉽습니다. 차주에는 또 다른 시도를 해봐야겠어요
어디 내세울만한 코드 퀄리티는 아닌점.. 이번주에는 온전히 나를 위한 코드 작성이었다고 생각합니다. 내가 시스템을 만들고 내가 보기좋게, 나만 이해하면 되는 구조였던 것 같습니다. 평소에 DX를 위한 도구 인터페이스 설계를 하는 것을 좋아하는 데, 사실 이번주는 많이 내려놨습니다.. 그 덕에 조금 더 온전히 경험해볼 수 있는 것도 있었던 것 같습니다.
아키텍쳐에 대한 지식 부족.. 이를 계기로 구조 공부에 시간을 더 써볼 예정입니다... 그동안은 알면서도 억지로 시간을 할애하지 못했지만, 제가 흥미를 크게 느끼는 영역이기도 하고 기대가됩니다.
AI 활용 경험 공유하기
AI활용을 해보지 않은 경험을 했습니다. 필요한 시간이었습니다.
리뷰 받고 싶은 내용
1) PR에 남긴 제 작업과정에서 개선할 부분이 있을까요?
저는 가끔 제가 너무 비효율적으로 작업하는 습관을 가지고 있다고 생각합니다. 결과를 내는 것 보다, 그 과정에서 다양한 방향을 고민해보는 것에 집중하게 되는 것 같아요. 아마도 작업 시 목표지향적으로 움직이지 않아서인 것 같습니다. 물론 실제 업무와 이런 학습성 작업의 접근 방식은 다르긴 합니다만, 자꾸 이런 접근이 반복되고 사서 고생을 하는 것 같습니다. 무튼 정답이 어느정도 있는 질문이라서, 쓰고나니 애매한 질문이라는 느낌이네요.
그렇담 추가로, 코치님은 과제를 검토하시면서 어떤 PR이 읽기 좋으신가요? 4주동안 과제 제출 / 회고성 PR을 작성하면서 '어떤 방식으로 작성해야겠다' 등의 기준이 없었다보니, '내 PR이 잘 읽힐까?', '상황에 맞는 적절한 포멧/문체 일까?'라는 생각이 문득 들었습니다. 제 PR에 대한 피드백 부탁드립니다...🙏
2)