과제 체크포인트
배포 링크
https://eveneul.github.io/front_6th_chapter1-1/
기본과제
상품목록
상품 목록 로딩
- 페이지 접속 시 로딩 상태가 표시된다
- 데이터 로드 완료 후 상품 목록이 렌더링된다
- 로딩 실패 시 에러 상태가 표시된다
- 에러 발생 시 재시도 버튼이 제공된다
상품 목록 조회
- 각 상품의 기본 정보(이미지, 상품명, 가격)가 카드 형태로 표시된다
한 페이지에 보여질 상품 수 선택
- 드롭다운에서 10, 20, 50, 100개 중 선택할 수 있으며 기본 값은 20개 이다.
- 선택 변경 시 즉시 목록에 반영된다
상품 정렬 기능
- 상품을 가격순/인기순으로 오름차순/내림차순 정렬을 할 수 있다.
- 드롭다운을 통해 정렬 기준을 선택할 수 있다
- 정렬 변경 시 즉시 목록에 반영된다
무한 스크롤 페이지네이션
- 페이지 하단 근처 도달 시 다음 페이지 데이터가 자동 로드된다
- 스크롤에 따라 계속해서 새로운 상품들이 목록에 추가된다
- 새 데이터 로드 중일 때 로딩 인디케이터와 스켈레톤 UI가 표시된다
- 홈 페이지에서만 무한 스크롤이 활성화된다
상품을 장바구니에 담기
- 각 상품에 장바구니 추가 버튼이 있다
- 버튼 클릭 시 해당 상품이 장바구니에 추가된다
- 추가 완료 시 사용자에게 알림이 표시된다
상품 검색
- 상품명 기반 검색을 위한 텍스트 입력 필드가 있다
- 검색 버튼 클릭으로 검색이 수행된다
- Enter 키로 검색이 수행된다
- 검색어와 일치하는 상품들만 목록에 표시된다
카테고리 선택
- 사용 가능한 카테고리들을 선택할 수 있는 UI가 제공된다
- 선택된 카테고리에 해당하는 상품들만 표시된다
- 전체 상품 보기로 돌아갈 수 있다
- 2단계 카테고리 구조를 지원한다 (1depth, 2depth)
카테고리 네비게이션
- 현재 선택된 카테고리 경로가 브레드크럼으로 표시된다
- 브레드크럼의 각 단계를 클릭하여 상위 카테고리로 이동할 수 있다
- "전체" > "1depth 카테고리" > "2depth 카테고리" 형태로 표시된다
현재 상품 수 표시
- 현재 조건에서 조회된 총 상품 수가 화면에 표시된다
- 검색이나 필터 적용 시 상품 수가 실시간으로 업데이트된다
장바구니
장바구니 모달
- 장바구니 아이콘 클릭 시 모달 형태로 장바구니가 열린다
- X 버튼이나 배경 클릭으로 모달을 닫을 수 있다
- ESC 키로 모달을 닫을 수 있다
- 모달에서 장바구니의 모든 기능을 사용할 수 있다
장바구니 수량 조절
- 각 장바구니 상품의 수량을 증가할 수 있다
- 각 장바구니 상품의 수량을 감소할 수 있다
- 수량 변경 시 총 금액이 실시간으로 업데이트된다
장바구니 삭제
- 각 상품에 삭제 버튼이 배치되어 있다
- 삭제 버튼 클릭 시 해당 상품이 장바구니에서 제거된다
장바구니 선택 삭제
- 각 상품에 선택을 위한 체크박스가 제공된다
- 선택 삭제 버튼이 있다
- 체크된 상품들만 일괄 삭제된다
장바구니 전체 선택
- 모든 상품을 한 번에 선택할 수 있는 마스터 체크박스가 있다
- 전체 선택 시 모든 상품의 체크박스가 선택된다
- 전체 해제 시 모든 상품의 체크박스가 해제된다
장바구니 비우기
- 장바구니에 있는 모든 상품을 한 번에 삭제할 수 있다
상품 상세
상품 클릭시 상세 페이지 이동
- 상품 목록에서 상품 이미지나 상품 정보 클릭 시 상세 페이지로 이동한다
- URL이
/product/{productId}
형태로 변경된다 - 상품의 자세한 정보가 전용 페이지에서 표시된다
상품 상세 페이지 기능
- 상품 이미지, 설명, 가격 등의 상세 정보가 표시된다
- 전체 화면을 활용한 상세 정보 레이아웃이 제공된다
상품 상세 - 장바구니 담기
- 상품 상세 페이지에서 해당 상품을 장바구니에 추가할 수 있다
- 페이지 내에서 수량을 선택하여 장바구니에 추가할 수 있다
- 수량 증가/감소 버튼이 제공된다
관련 상품 기능
- 상품 상세 페이지에서 관련 상품들이 표시된다
- 같은 카테고리(category2)의 다른 상품들이 관련 상품으로 표시된다
- 관련 상품 클릭 시 해당 상품의 상세 페이지로 이동한다
- 현재 보고 있는 상품은 관련 상품에서 제외된다
상품 상세 페이지 내 네비게이션
- 상품 상세에서 상품 목록으로 돌아가는 버튼이 제공된다
- 브레드크럼을 통해 카테고리별 상품 목록으로 이동할 수 있다
- SPA 방식으로 페이지 간 이동이 부드럽게 처리된다
사용자 피드백 시스템
토스트 메시지
- 장바구니 추가 시 성공 메시지가 토스트로 표시된다
- 장바구니 삭제, 선택 삭제, 전체 삭제 시 알림 메시지가 표시된다
- 토스트는 3초 후 자동으로 사라진다
- 토스트에 닫기 버튼이 제공된다
- 토스트 타입별로 다른 스타일이 적용된다 (success, info, error)
심화과제
SPA 네비게이션 및 URL 관리
페이지 이동
- 어플리케이션 내의 모든 페이지 이동(뒤로가기/앞으로가기를 포함)은 하여 새로고침이 발생하지 않아야 한다.
상품 목록 - URL 쿼리 반영
- 검색어가 URL 쿼리 파라미터에 저장된다
- 카테고리 선택이 URL 쿼리 파라미터에 저장된다
- 상품 옵션이 URL 쿼리 파라미터에 저장된다
- 정렬 조건이 URL 쿼리 파라미터에 저장된다
- 조건 변경 시 URL이 자동으로 업데이트된다
- URL을 통해 현재 검색/필터 상태를 공유할 수 있다
상품 목록 - 새로고침 시 상태 유지
- 새로고침 후 URL 쿼리에서 검색어가 복원된다
- 새로고침 후 URL 쿼리에서 카테고리가 복원된다
- 새로고침 후 URL 쿼리에서 옵션 설정이 복원된다
- 새로고침 후 URL 쿼리에서 정렬 조건이 복원된다
- 복원된 조건에 맞는 상품 데이터가 다시 로드된다
장바구니 - 새로고침 시 데이터 유지
- 장바구니 내용이 브라우저에 저장된다
- 새로고침 후에도 이전 장바구니 내용이 유지된다
- 장바구니의 선택 상태도 함께 유지된다
상품 상세 - URL에 ID 반영
- 상품 상세 페이지 이동 시 상품 ID가 URL 경로에 포함된다 (
/product/{productId}
) - URL로 직접 접근 시 해당 상품의 상세 페이지가 자동으로 로드된다
상품 상세 - 새로고침시 유지
- 새로고침 후에도 URL의 상품 ID를 읽어서 해당 상품 상세 페이지가 유지된다
404 페이지
- 존재하지 않는 경로 접근 시 404 에러 페이지가 표시된다
- 홈으로 돌아가기 버튼이 제공된다
AI로 한 번 더 구현하기
- 기존에 구현한 기능을 AI로 다시 구현한다.
- 이 과정에서 직접 가공하는 것은 최대한 지양한다.
과제 셀프회고
기술적 성장
- 폴더 구조 · 설계에 대한 고찰
- 처음부터 구조를 잘 잡는 사람이 개발을 잘한다는 생각이 있었다. 지금도 그 생각은 변하지 않았지만! 실제로 과제를 받고 나서 머릿속으로 구조화를 하는 게 내 생각보다 너무 힘들었다.
- 금요일, 김성호 코치님의 멘토링을 앞두고 나 포함 팀원분들이 다 같은 생각을 하고 있었다. 처음부터 잘 짜여진 코드를 작성하고 싶은데, 개발을 하다 보면 이것도 필요하고 저것도 필요하다는 생각이 많아지는 게 걱정이라고.. (나만 그런 생각 한 게 아니어서 내심 다행이었다.)
- 코치님은 주니어 때는 그런 생각이 당연히 드는 게 당연하고, 처음부터 잘하는 사람은 없다고 하셨다. 많이 만들어 가면서 익히는 거라고.. 일단 큰 덩어리로 만들고 코드가 신호를 보내 주면 그때서야 나누면 된다는 말씀을 듣고, 앞으로의 구현 방식은 그렇게 해야겠다는 생각이 들었다.
- Class VS Fuction
- 1차 시도 - 함수형 + 커스텀 훅 느낌
- 처음에는 리액트처럼
onClick=”myFunction”
식 바인딩을 꿈꾸며 전역 함수 & 커스텀 훅 스타일로 시작 - 그렇게 되면 코드가 예쁘지 않게(?) 작성될 것 같아서 저장소를 삭제하고(!) 다시 Fork 받아서 진행
- 처음에는 리액트처럼
- 2차 시도 - 클래스로 재구성
- BaseComponent, BasePage를 만들고 상속 구조로 재편하려고 했으나………
- 잦은 override와 불필요한 추상화가 오히려 가독성을 해치고, 재미도 없어져서 다시 함수형으로 변경했다.
- 3차 시도 - Function으로 복귀
MyComponent.mount = () ⇒ { … }
패턴 도입하여 “DOM 생성 후 실행”을 보장- 결과적으로 컴포넌트를 잘게 나누어도 예측 가능해서 잘 맞았고,
mount
뿐만 아닌init
,unmount
등 다양한 메서드들을 추가로 도입할 수 있을 것 같음.
- 1차 시도 - 함수형 + 커스텀 훅 느낌
- 다른 분과 이야기하면서 **“저 지금 국비 이제 막 졸업한 사람처럼 짜고 있어요”**라고 했다. 솔직히 지금도 그런 것 같다. 그래서 코치님들께 수정하고, 지웠던 코드를 보여 주기 싫어서 저장소를 삭제하고 fork를 받았다. 하지만 현빈 매니저님께서 실패한 것도 경험이니 저장소 삭제하지 말고 실패한 과정이든, 성공한 과정이든 모두 commit에 남겨 놓으라고 하셨다. 기록의 중요성을 체감하는 요즈음, 커밋도 하나의 기록이니까 앞으로는 지울 코드라도 커밋에 남겨 놓아야겠다. (갑자기 감성적.. 🖋️)
자랑하고 싶은 코드
core/useStore.js로 전역에 상태하는 훅을 만들었다.
Recoil, Zustand가 어떻게 돌아가는지도 모르고 생각없이 막 사용하던 지난 날….. 처음 전역 상태 관리를 도입해야겠다고 생각하고, Zustand 코드를 참고했다.
const listener = [];
const get = (key) => {
if (!key) {
return globalState;
} else {
return globalState[key];
}
};
// 전역 상태를 만드는 함수
const set = (key, value) => {
setNestedValue(globalState, key, value);
listener.forEach(({ callback, targetKey }) => {
if (!targetKey || key === targetKey || key.startsWith(`${targetKey}.`)) {
const valueToReturn = getNestedValue(globalState, targetKey);
callback(valueToReturn, globalState);
}
});
};
// 전역 상태가 변경됐음을 감지해 주는 함수
const watch = (callback, targetKey = null) => {
listener.push({ callback, targetKey });
return () => {
const index = listener.findIndex((l) => l.callback === callback && l.targetKey === targetKey);
if (index !== -1) listener.splice(index, 1);
};
};
return () => ({ get, set, watch });
get
으로 미리 선언해둔 globalState에 있는 객체 중 해당하는 key만 정보를 추출하여 리턴한다.- 처음에는 1뎁스로만 되어 있는 (params 정보만 있는)
globalState
를 구성했으나, 카트라든지 카테고리라든지 다른 상태도 관리할 일이 생겨서 2뎁스까지 들어가서 value를 바꾸는 함수를 구성했다. (여담으로 저때 좀 신 들렸나 보다. AI 도움 안 받고 갑자기 생각나는 대로 작성했다. 지금 하라고 하면 못 할 듯) watch
는set
으로 객체를 변경하면 그것을 감지하고, 새롭게 변경한 state를 알려 주고, 콜백을 제공한다. 나 같은 경우, 하위 컴포넌트 또는 다른 컴포넌트에서 store.set으로 객체를 변경하면, 상위 컴포넌트에서 변경을 감지해 다시 dom에 그려주는render
를 진행하는 식으로 구현했다.
개선이 필요하다고 생각하는 코드
- return 안에 있는 template에 넣을 생각으로 각 컴포넌트마다 state 객체를 만들었다.
const state = {
isLoading: true
};
- 그런데 구현하다 보면 isLoading 같은 것도, 전역 상태에서 관리할 만한 것도 state에 작성되거나, 전역 상태에서 관리할 것들을 state에 작성하는 경우도 있었고,
- 부모 컴포넌트에서 인자로 넘겨 줘서 바인딩 해 줄 것도 괜히(?) state에 작성했다.
- 이런 중복된 코드를 줄이고, 명확한 역할을 부여하고 싶다. 그런데 괜히 건드렸다가 빵꾸 터질까 봐 겁남.
학습 효과 분석
- 처음부터 모든 것을 다 해내려고 하는 것이 좋은 건 아니구나 생각이 많이 들었다. 앞서 이야기했듯이 더러운 내 코드 (ㅠㅠ) 코치님들이 보시기에 정색할 만한 코드 같으면 과감없이 지웠다. (실제로도 개인 공부 할 때 그랬음)
- 멘토링을 받고 다음부터는 큰 덩어리부터 시작해, 점차 분리해 나가는 과정으로 다음 과제를 맞이할 것 같다.
- 왜인지 Intersection Observer를 쓸 수가 없었다. 그러면 테스트에 통과를 못 해서, 어쩔 수 없이 scroll event를 작성했다.
- 현재 스크롤 값에 + view (현재 모니터에 보이는 화면) height와, body height - trigger height로 비교를 해서 하단에 스크롤이 도달하면 데이터 패칭을 이루어지는 아이디어로 작성했는데, 이게 맞는 건지 모르겠다.
- 하지만 observer를 쓰지 못하니 스크롤로 데이터를 패칭하는 아이디어를 생각한 내가 참 기특한 것 같다. ^^
과제 피드백
- 처음에 과제 타이틀만 보고 쉬운 거 아닌가? 했던 나를 후려치고 싶을 정도로 생각보다 너무 어려웠다. 할 게 너무 많았고, 어떤 식으로 구현을 해야 잘 굴러갈까 생각이 많았다. (날개가 아니라 목으로 돌아가는 비둘기마냥..)
- 그런데 하면 할수록 리액트에게 너무 감사함을 느낀다. 그런데 지금은 이런 과제를 내 준 코치님께 더 충성충성하게 된다. 이번 기수에서 과제가 바뀌어서 코치님도 수정하시느라 바쁘셨는데, 결론적으로 선택 과제로 요구사항들이 많이 빠져서 좋다. ^^
- 리액트 같은 프레임워크에서 제공하는 Hook 없이 생짜로 구현해 볼 수 있어서 했던 고민들이 좋았던 것 같다.
AI 활용 경험 공유하기
- Cursor AI 썼다. 처음 개강날에 팀원분들께 AI는줘패야말을들어요 라고 했지만, 정말 줘패야 말을 듣는다는 건 오히려 인공지능이 아니라 나일 수도?
- 이거 해 줘. 이거 구현할 거야가 아니라 내 아이디어를 먼저 제시했다. 가장 많이 한 말은 “나는 이렇게 생각했는데 너는 어때?” 였다. 그러면 인공지능에게 내가 구현해야 할 방식을 설명하면서도 거기에서 힌트를 얻어 오히려 AI 없이 코드를 작성한 적이 꽤 많았다.
- 그리고 커밋 메시지를 Cursor AI 시켰다. 생각보다 많이 괜찮다. 가끔 오글거리는 말은 지우는 둥 한 번 정도 검수가 필요하지만, 커밋 메시지와 깃모지로 고민했던 지난 날은 안녕..
리뷰 받고 싶은 내용
- 아무리 생각해도 잘 짜여진 Route를 모르겠습니다. 제 코드에서는 미리 나눠 놓은 routes를 순회 돌면서 현재 주소와 매치한 뒤, init을 한 뒤, mount를 실행시킵니다.
const view = async () => {
for (const route of routes) {
const match = getAppPath().match(route.path);
if (!match) continue;
const Page = route.component;
Page.init?.(match?.[1]);
draw("main", Page({}));
await Page.mount?.();
return; // 매치된 첫 번째 라우트만 실행하고 멈추게
}
};
- 지금은 init과 mount밖에 없지만 vue에서는 이런 기능들이 훨 많은데, vue처럼 나중에 unmount라든지 다른 메서드들을 추가하면 이런 로직은 계속 아래로 작성하는 것밖에 답이 없나? 라는 생각이 듭니다. 이를 테면 아래처럼요…
const view = async () => {
for (const route of routes) {
...
if(Page.unmount) await Page.unmount?.();
return; // 매치된 첫 번째 라우트만 실행하고 멈추게
}
};
- 제가 생각한 건, Page를 순회하면서 해당 메서드가 있으면 실행시키고, 아니면 실행시키지 않는 것이었는데 그럴 경우 if 문 지옥에 빠질 것 같습니다.
- 회고가 넘 길어서 죄송합니다. 🙇♀️