-
Notifications
You must be signed in to change notification settings - Fork 50
[6팀 김현우] Chapter2-1. 프레임워크 없이 SPA 만들기 #33
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
c1cdc92 to
9b09aa3
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에서는 기본적인 SPA 쇼핑몰 기능의 구현 요구사항을 충실히 충족하는 구조로 잘 작성되었으며,
상품 목록부터 상세 페이지, 장바구니, 검색, 필터, 무한 스크롤, URL 동기화 및 SPA 라우팅까지 폭넓은 영역을 다루었습니다.
코드는 기능별로 컴포넌트와 유틸리티 모듈로 적절히 분리되어 있고, 이벤트 위임을 적절히 사용하여 동적 요소도 잘 처리하고 있습니다.
하지만 향후 "확장성"을 고려할 때 다음과 같은 개선 포인트가 있습니다.
- 상태 관리와 URL 관리의 분리: 필터 상태를 전역 객체 하나로 관리하면서 동시에 URL 갱신까지 담당하고 있는데, 상태와 URL 관리를 분리해 이벤트나 옵저버 패턴으로 상태 영향도를 낮출 필요가 있습니다.
- 무한 스크롤 로딩 로직 분리: API 호출, 상태 갱신, UI 업데이트가 한 함수에 몰려 있어, 앞으로 오류 처리나 다른 UI 대응을 확장하는 데 어려울 수 있습니다.
- 장바구니 모달 이벤트 관리 개선: 개별 DOM 노드에 이벤트를 붙이는 부분을 이벤트 위임과 상태 중심 설계로 개선하면 더 간결하고 효율적으로 관리할 수 있습니다.
- 상품 상세 페이지 이벤트 위임: 이벤트 처리 방식을 DOM 특정 엘리먼트 직접 바인딩 대신 위임으로 전환하면 생산성과 확장성 향상에 도움이 됩니다.
- SPA 네비게이션 이벤트 위임 범위 최적화: 링크 클릭 인터셉터의 이벤트 위임 범위를 너무 넓게 하지 않고, 특정 영역으로 제한하는 것이 성능과 유지보수에 유익합니다.
- 컴포넌트 함수 내부의 역할 분리: 특히 필터 및 토스트 컴포넌트는 렌더링과 로직 분리를 명확히 하여 재사용성과 테스트 용이성을 개선할 수 있습니다.
- 브레드크럼 URL 인코딩 및 SPA 연동: 브레드크럼 네비게이션에 SPA 라우팅 기능과 URL 인코딩 적용이 필요합니다.
- 토스트 메시지 컴포넌트 설계: 단일 메시지 처리 컴포넌트와 별도의 토스트 매니저 분리로 유연성을 확보할 수 있습니다.
이처럼 기본 기능 구현은 탄탄하지만, 향후 요구사항 증가 및 유지보수성을 고려하여 상태 관리와 이벤트 처리 구조를 한 단계 업그레이드하는 방향을 추천드립니다.
| * @returns {Object} 현재 필터 상태 | ||
| */ | ||
| export const getCurrentFilters = () => { | ||
| return { ...currentFilters }; |
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. 문제상황 제시
구체적인 문제 상황:
필터 상태 관리를 전역 객체 하나로 관리하고, UI와 URL 업데이트 로직을 결합하면서 확장성 측면에서 부담이 될 수 있습니다.
현재 코드의 한계:
- 현재 구조는 필터 상태가 한 곳에 집중되어 한 페이지에서는 문제 없지만, 다른 페이지나 기능 추가 시 관리가 어려워집니다.
- URL 동기화와 상태 업데이트가 함께 처리되어 테스트 및 유지보수가 어렵습니다.
- 복잡한 필터 추가 시 모든 곳에 영향을 미칠 가능성이 높습니다.
2. 근본 원인
핵심 문제:
"단일 전역 상태 객체에 UI와 URL 조작 로직이 결합되어 있어 상태 관리와 뷰 업데이트가 분리되지 않음"
왜 문제인가:
상태와 UI/URL 관리가 밀접하게 결합되면 새로운 필터 조건 추가, 분기 처리, 또는 비동기 업데이트 시 복잡해지고 오류 발생 가능성이 증가합니다.
3. 개선 구조
현재 구조:
- state: currentFilters 전역 객체
- updateFilters: 상태 및 URL 동시 업데이트
개선된 구조:
- 상태 관리와 URL 관리 분리
- 상태 변경은 이벤트나 옵저버 패턴으로 처리
- URL 업데이트는 별도의 모듈에서 상태 구독 후 처리
개선 사항:
- 상태 변경 알림을 받는 옵저버 패턴 혹은 이벤트 시스템 도입
- URL 반영은 별도 함수에서 상태 구독 후 처리
- 컴포넌트들이 상태를 구독하여 필요한 UI만 업데이트
코드 비교:
// ❌ 현재 방식
export const updateFilters = (newFilters) => {
currentFilters = {
...currentFilters,
...newFilters,
};
// URL도 함께 업데이트
updateURL(currentFilters);
return { ...currentFilters };
};
// ✅ 개선 방식 예시 (상태와 URL 업데이트 분리)
const listeners = [];
export const subscribeFilters = (listener) => {
listeners.push(listener);
};
export const updateFilters = (newFilters) => {
currentFilters = {
...currentFilters,
...newFilters,
};
listeners.forEach((l) => l(currentFilters));
return { ...currentFilters };
};
// 별도 모듈이나 위치에서
subscribeFilters((filters) => {
updateURL(filters);
});| // 다음 페이지 번호 | ||
| const nextPage = state.currentPage + 1; | ||
|
|
||
| // API 호출 |
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. 문제상황 제시
구체적인 문제 상황:
무한 스크롤 로딩과 상품 추가 로직이 하나의 단일 함수에 모두 구현되어 있어, 앞으로 새로운 요구사항(예: 필터 조건 변경 시 무한 스크롤 초기화, 스크롤 위치 제어 등)이 더해질 때 확장성이 떨어집니다.
현재 코드의 한계:
- 로딩 중 UI 표시, 오류 핸들링, 상품 추가 로직이 섞여 있어 유지보수가 복잡합니다.
- 무한 스크롤 활성화/비활성화 상태를 내부에서 관리하지만, 외부 상태 변경에 대응하는 구조가 미흡합니다.
- 추후 다른 위치에 더 많은 기능이 추가되면 코드가 장황해질 가능성이 큽니다.
2. 근본 원인
핵심 문제:
"무한 스크롤 데이터 로딩과 UI 조작 로직이 단일 함수에 결합되어 있어 기능 확장 및 분리 관리를 어렵게 만듦"
왜 문제인가:
이 구조는 테스트가 어려워지고, 오류 발생 시 트러블슈팅이 힘들며 재사용성이 낮아 다양한 상황에 대응하기 어렵습니다.
3. 개선 구조
현재 구조:
- loadMoreProducts() 함수 안에 API 호출, 로딩 UI 처리, DOM 조작이 통합되어 있음
개선된 구조:
- API 호출과 데이터 처리 로직 분리
- UI 상태 관리(로딩 표시/숨기기, 완료 메시지 등) 별도 함수 또는 컴포넌트에 위임
- 상태 변화에 따른 사이드 이펙트는 옵저버나 상태 관리 모듈에서 처리
개선 사항:
- API 요청 함수와 UI 업데이트 함수를 분리하여 재사용성을 늘림
- 에러처리 로직 별도 함수 분리
- 무한 스크롤 상태를 외부에서 구독 및 제어하도록 설계
코드 비교:
// ❌ 기존 loadMoreProducts 내 로딩 UI 직접 제어
try {
const response = await getProducts(...);
appendProductsToGrid(response.products);
updateInfiniteScrollState(...);
showCompletionMessage();
} catch (error) {
hideLoadingIndicator();
}
// ✅ 개선: API 호출은 별도 함수
async function fetchNextProducts(page, filters) {
const response = await getProducts({ ...filters, page });
return response.products;
}
// UI 업데이트와 상태 변경은 콜백 혹은 이벤트로 분리
async function loadMoreProducts() {
setLoading(true);
try {
const products = await fetchNextProducts(currentPage + 1, currentFilters);
addProductsUI(products);
updateStateAfterLoad(...);
} catch(err) {
handleLoadError(err);
} finally {
setLoading(false);
}
}| checkbox.addEventListener("change", handleItemCheckboxChange); | ||
| }); | ||
|
|
||
| // 수량 증가 버튼 |
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. 문제상황 제시
구체적인 문제 상황:
장바구니 모달의 이벤트 리스너 등록과 DOM 조작 로직이 매우 길고 복잡하게 단일 파일에 모여 있어, 유지보수 시 혼란스러울 수 있고, 확장성에도 제약이 있습니다.
현재 코드의 한계:
- 이벤트 위임 대신 개별 노드에 일일이 이벤트를 붙이는 방식이라 동적 요소 관리에 효율성이 떨어집니다.
- 체크박스 상태 복원을 로컬스토리지에서 직접 DOM 조작으로 구현하여 재사용성과 테스트가 어렵습니다.
- 전반적으로 책임 분리가 모호하며, 함수들이 너무 많아 단일 책임 원칙 위반 가능성
2. 근본 원인
핵심 문제:
"모달 UI 상태 관리, 이벤트 처리, 상태 동기화 로직이 한 파일 내에서 꼬여 있어 기능 단위 분리가 부족함"
왜 문제인가:
이런 구조는 수정 시 영향 범위 파악이 힘들고, 새로운 기능 추가나 리팩토링에 비용을 많이 요구합니다.
3. 개선 구조
현재 구조:
- DOM 삽입 후 모든 이벤트를 구체적으로 개별 노드에 부착
- 상태 저장/복원도 직접 DOM 쿼리 및 조작으로 처리
개선된 구조:
- 이벤트 위임 기법 활용해 상위 컨테이너에 이벤트를 한 번만 붙임
- 상태 관리는 별도의 훅 혹은 상태 매니저로 분리
- 체크박스 상태 저장/복원과 UI 업데이트를 명확한 함수로 분리
개선 사항:
- 모달 DOM 이벤트 리스너를 하나 또는 적은 수의 핸들러로 처리
- 상태 변경 시 UI 업데이트 분리 함수 호출
- 공유 가능한 체크박스 상태 관리 모듈 도입
코드 예시:
// ❌ 개별 노드 이벤트 등록
itemCheckboxes.forEach(cb => cb.addEventListener('change', onCheckboxChange));
// ✅ 이벤트 위임
modalElement.addEventListener('change', (e) => {
if(e.target.matches('.cart-item-checkbox')) {
onCheckboxChange(e.target);
}
});| /** | ||
| * 장바구니 추가 버튼 이벤트 핸들러 초기화 | ||
| * 이벤트 위임을 사용하여 동적으로 추가되는 상품에도 대응 | ||
| */ |
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. 문제상황 제시
구체적인 문제 상황:
장바구니 핸들러가 단일 플래그 isInitialized를 통해 한번만 초기화하지만, 만약 페이지 내에서 재렌더링 되는 경우나 SPA에서 상태를 잃을 가능성에 대비한 재초기화 로직이 부족합니다.
현재 코드의 한계:
- 상태 또는 DOM의 변화가 있을 때 대응할 재초기화 메커니즘 미탑재
- 이벤트 위임을 문서(document)에 붙이지만, 이벤트 핸들러 정리 기능은 없어서 메모리 누수 가능성 낮지 않음
- 에러 발생 시 복구 기능이 부족
2. 근본 원인
핵심 문제:
"SPA 환경에서 동적 렌더링과 상태 관리 시 이벤트 핸들러 등록과 해제가 체계적으로 관리되지 않음"
왜 문제인가:
이로 인해 여러 번 이벤트가 등록되거나 누수 발생시 앱 성능 저하, 예상치 못한 동작 발생 가능성이 있습니다.
3. 개선 구조
개선 사항:
- 이벤트 핸들러 등록 시 해제 로직 추가
- SPA 내에서 경로 변경 등으로 필요 시 재초기화 가능하도록 개선
- 이벤트 위임 범위를 필요 최소 범위로 줄이고, doctype과 scope 고려
예시:
// 이벤트 핸들러 등록
const onClick = event => { /* ... */ };
document.addEventListener('click', onClick);
// 해제 함수
const cleanup = () => {
document.removeEventListener('click', onClick);
isInitialized = false;
};
// SPA 라우팅 시 cleanup 호출 후 initCartHandler 재호출| }); | ||
| } | ||
|
|
||
| if (quantityDecreaseBtn && quantityInput) { |
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. 문제상황 제시
구체적인 문제 상황:
상품 상세 페이지 내 수량 증감 버튼에 대한 이벤트 핸들링이 DOM 요소 이벤트에 직접 붙여져 있어, 장기적인 관리와 확장성에 부담이 있습니다.
현재 코드의 한계:
- 이벤트 핸들러가 요소마다 직접 붙어 있어, 신규 동적 컴포넌트나 변경에 대응하기 어렵습니다.
- 수량 검증 로직이 입력 변경 이벤트에서만 처리되어 수량 입력 비정상 변경에 취약
- 상품 상세 페이지 내 관련 상품 클릭도 개별 이벤트 리스너 부착으로 비용 발생
2. 근본 원인
핵심 문제:
"구체적 DOM 요소에 이벤트 핸들러가 직접 결합되어 있어 동적 컨텐츠에 유연하게 대응하기 어려움"
왜 문제인가:
서로 다수의 이벤트 핸들러 중복 등록 및 삭제가 복잡해지고, 테스트 및 유지보수가 어려워집니다.
3. 개선 구조
개선 사항:
- 이벤트 위임 방식을 적용하여 상위 컨테이너에 핸들러를 한 번만 등록
- 수량 입력 검증을 중앙집중화하여 중복 방지 및 정확성 향상
- 관련 상품 영역에 한 번만 이벤트 리스너 등록하여 위임 방식 적용
코드 예시:
// 이벤트 위임
const container = document.querySelector('.product-detail-container');
container.addEventListener('click', (e) => {
if(e.target.matches('#quantity-increase')) { /* 증가 로직 */ }
else if(e.target.matches('#quantity-decrease')) { /* 감소 로직 */ }
else if(e.target.closest('.related-product-card')) { /* 관련 상품 이동 */ }
});
// 입력 검증은 input 이벤트에 위임
container.addEventListener('input', (e) => {
if(e.target.matches('#quantity-input')) {
// 유효성 검사
}
});| * data-link 속성이 있는 a 태그 클릭 시 SPA 방식으로 처리 | ||
| * @param {Event} e - 클릭 이벤트 | ||
| */ | ||
| export const handleLinkClick = (e) => { |
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. 문제상황 제시
구체적인 문제 상황:
SPA 네비게이션을 위해 링크 클릭 인터셉터를 설정하였으나, 현재 사용자 클릭 이벤트를 전체 문서에 리스닝하고, data-link 속성 있는 링크만 처리하는 방식입니다.
현재 코드의 한계:
- 대규모 DOM에서 모든 클릭을 듣기 때문에 성능 이슈 가능성 존재
- 정적 링크로만 동작하며, 동적 생성 엘리먼트에 대한 처리 방법이 명확하지 않음
- 중첩된 a 태그나 버튼 등 클릭 이벤트 예외 처리 코드가 없음
2. 근본 원인
핵심 문제:
"이벤트 위임 범위가 지나치게 광범위하고, 동적 엘리먼트 및 예외 상황 대응이 부족함"
왜 문제인가:
비효율적인 이벤트 처리로 인한 퍼포먼스 저하와 클릭 이벤트 예외 상황 발생 가능성이 있습니다.
3. 개선 구조
개선 사항:
- 이벤트 위임 범위를 라우팅이 필요한 특정 영역으로 제한
- 동적 엘리먼트 추가 시 필터링 강화
- 중첩된 링크 등의 예외 처리 고려
예시 코드:
// 특정 컨테이너에만 이벤트 위임
const container = document.getElementById('root');
container.addEventListener('click', e => {
const link = e.target.closest('a[data-link]');
if (!link) return;
// 중첩 a 태그 예외 처리 등
if (link.contains(e.target)) {
e.preventDefault();
navigateTo(link.getAttribute('href'));
}
});| <!-- 기존 필터들 --> | ||
| <div class="flex gap-2 items-center justify-between"> | ||
| <!-- 페이지당 상품 수 --> | ||
| <div class="flex items-center gap-2"> |
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. 문제상황 제시
구체적인 문제 상황:
필터 UI와 관련 이벤트 핸들러가 긴 함수 안에 모두 구현되어 있어 코드 가독성과 유지보수가 어렵습니다.
현재 코드의 한계:
- 로직이 너무 긴 함수에 몰려있어 역할 분리 부족
- 이벤트 핸들러도 복잡하게 중첩되어 있어 추후 기능 확장 시 어려움
- DOM 엘리먼트 탐색이 반복적으로 이루어짐
2. 근본 원인
핵심 문제:
"컴포넌트의 렌더링과 이벤트 처리가 하나의 함수에 집중되어 역할이 명확히 분리되어 있지 않음"
왜 문제인가:
복잡도가 증가하면 실수하거나 버그가 생기기 쉽고, 유지보수 및 테스트가 어려워지며, 재사용이 힘듭니다.
3. 개선 구조
개선 사항:
- 렌더링과 이벤트 바인딩을 명확히 분리하여 모듈화
- 카테고리 렌더링, 이벤트 처리, 브레드크럼 업데이트 등을 개별 함수나 클래스로 분리
- 이벤트 위임을 적극 활용해 핸들러 수 최소화
- DOM 접근을 줄이고 상태 기반 UI 업데이트 적용
예시:
// 렌더링 함수들
function renderCategory1() { ... }
function renderCategory2() { ... }
// 이벤트 핸들러 분리
function onCategory1Click() { ... }
function onCategory2Click() { ... }
function onBreadcrumbClick() { ... }
// 초기화 함수
function init() {
renderCategory1();
bindEvents();
}| </a> | ||
| `); | ||
| } | ||
|
|
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 인코딩 처리와 SPA 전환 이벤트 위임이 부족하여, URL 직접 입력 또는 링크 클릭 시 SPA 네비게이션 흐름이 올바르게 작동하지 않을 수 있습니다.
현재 코드의 한계:
- 브레드크럼 링크 생성 시 하드코딩된 href만 존재, SPA router에 의한 비동기 페이지 전환 미지원
- 인코딩 처리 부족으로 카테고리명에 따라 URI 충돌 우려
- 데이터 링크 어트리뷰트 미사용으로 클릭 시 새로고침 발생 가능
2. 근본 원인
핵심 문제:
"브레드크럼 네비게이션에 SPA 이벤트 전파 및 URL 인코딩 미처리가 결합되지 않아 사용자 경험 저하"
왜 문제인가:
SPA 전환 경험이 매끄럽지 않고, 잘못된 URL을 통한 네비게이션 시 에러나 페이지 로딩 오류가 발생합니다.
3. 개선 구조
개선 사항:
- 브레드크럼 링크에
data-link속성을 추가하여 SPA 라우터 인터셉터에 의해 네비게이션되도록 변경 encodeURIComponent를 사용해 카테고리명 인코딩 처리- SPA router의 navigateTo 함수를 사용한 네비게이션 연동
코드 비교:
// ❌ 현재 브레드크럼 링크
<a href="${category1Query}" data-link>${category1}</a>
// ✅ 개선 후
<a href="${category1Query}" data-link>${encodeURIComponent(category1)}</a>
// 더 나아가 클릭 이벤트 핸들링을 router에서 처리| @@ -0,0 +1,47 @@ | |||
| export const Toast = () => { | |||
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. 문제상황 제시
구체적인 문제 상황:
Toast 컴포넌트가 모든 토스트 메시지를 한번에 렌더링해 두고 있습니다. 이로 인해, 메시지 타입에 따라 보여주는 메시지와 스타일을 매번 변경하기 불편하고, 토스트 메시지 관리가 어렵습니다.
현재 코드의 한계:
- 하나의 컴포넌트 내에 모든 타입의 메시지가 포함되어 불필요한 DOM이 생성됨
- 동적으로 생성 및 제거하지 않고 항상 렌더링되어 있어 가독성과 유지보수가 어려움
- 토스트 메시지 상태 관리 및 애니메이션 구현이 독립적이지 못함
2. 근본 원인
핵심 문제:
"하나의 컴포넌트가 여러 토스트 메시지를 모두 렌더링하는 구조로, 단일 메시지 관리에 부적합함"
왜 문제인가:
동적 토스트 메시지 생성과 제거를 효율적으로 지원하지 않아, 사용자 경험과 유지 보수성이 떨어집니다.
3. 개선 구조
개선 사항:
- Toast 컴포넌트는 단일 메시지 렌더링을 수행하고, 생성 시 메시지 타입과 내용을 동적으로 받아서 렌더링
- 스타일과 애니메이션은 토스트 생성 시 적용하고, 메시지별 생성과 제거를 담당하는 매니저를 별도로 구현
- 토스트 메시지들의 동적 생성/소멸로 리소스 관리 개선
코드 예시:
export const Toast = (message, type) => {
const styles = {
success: 'bg-green-600',
error: 'bg-red-600',
info: 'bg-blue-600',
};
return `<div class="toast ${styles[type]}">${message}</div>`;
};
// 별도 ToastManager에서 생성, 삭제 담당
과제 체크포인트
배포 링크
https://lecto17.github.io/front_7th_chapter2-1/
기본과제
상품목록
상품 목록 로딩
상품 목록 조회
한 페이지에 보여질 상품 수 선택
상품 정렬 기능
무한 스크롤 페이지네이션
상품을 장바구니에 담기
상품 검색
카테고리 선택
카테고리 네비게이션
현재 상품 수 표시
장바구니
장바구니 모달
장바구니 수량 조절
장바구니 삭제
장바구니 선택 삭제
장바구니 전체 선택
장바구니 비우기
상품 상세
상품 클릭시 상세 페이지 이동
/product/{productId}형태로 변경된다상품 상세 페이지 기능
상품 상세 - 장바구니 담기
관련 상품 기능
상품 상세 페이지 내 네비게이션
사용자 피드백 시스템
토스트 메시지
심화과제
SPA 네비게이션 및 URL 관리
페이지 이동
상품 목록 - URL 쿼리 반영
상품 목록 - 새로고침 시 상태 유지
장바구니 - 새로고침 시 데이터 유지
상품 상세 - URL에 ID 반영
/product/{productId})상품 상세 - 새로고침시 유지
404 페이지
AI로 한 번 더 구현하기
과제 셀프회고
기술적 성장
React가 vanila js에서 어떤 부분들을 개선해줬는지 조금이나마 알게 되고, React로 편하게 개발할 수 있던 것을 역체감
라우터
react-router도 DOM 바꾸는 거니까, 결국 이것도 innerHTML 쓸 것 같은데'라는 생각이 있었다.)react-router는 “어떤 React 컴포넌트 트리를 렌더링할지”만 결정하고 DOM 변경은 React 렌더러(react-dom)가 Virtual DOM diff를 통해 처리한다는 것을 알게 되었다. 그리고 React는 대부분document.createElement,setAttribute,appendChild같은 방식으로 패치하지 페이지 전체innerHTML을 갈아엎는 식으로 동작하지 않는다는 것을 알게 되었다.window.history에 대한 내용(popstate Event), SPA에서 pushState를 통한 라우트 처리에 대해 학습자랑하고 싶은 코드
개선이 필요하다고 생각하는 코드
학습 효과 분석
과제 피드백
AI 활용 경험 공유하기
리뷰 받고 싶은 내용