Skip to content

Conversation

@jy0813
Copy link

@jy0813 jy0813 commented Nov 10, 2025

과제 체크포인트

배포 링크

https://jy0813.github.io/front_7th_chapter2-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로 다시 구현한다.
  • 이 과정에서 직접 가공하는 것은 최대한 지양한다.

과제 셀프회고

사실 이번 과제를 하면서 가장 크게 느낀 건, 우리가 일상처럼 쓰고 있는 SPA라는 기술이 얼마나 거대한 구조물 위에서 돌아가고 있는지였습니다.

평소 아무렇지 않게 쓰던 useState와 useEffect 내부 동작을 직접 구현해보면서, React에서는 setState를 호출하면 화면이 알아서 좋은 타이밍에 갱신되고, useEffect는 적당히 알아서 부수효과를 수행해주니까 그냥 편하게 쓰기만 하면 됐는데, 막상 비슷한 기능을 스스로 만들어보려고 하니 너무나도 복잡하게 느껴졌습니다.

상태를 바꾸기만 하면 되는 게 아니라, 어떤 컴포넌트가 이 상태를 구독하는지, 언제 렌더링을 트리거해야 하는지, 이전 상태와 비교해서 실제로 갱신할 필요가 있는지, 렌더링 우선순위는 어떤지, 렌더 순서가 뒤집히면 어떻게 안정성을 보장할지 이런 모든 문제들을 직접 마주하였습니다.

React는 이런 것들을 Fiber 아키텍처라는 괴물 같은 구조로 해결하고 있습니다. Fiber는 UI를 작은 단위의 작업 으로 쪼개서 스케줄링하고, 우선순위를 부여하고, 필요할 때는 렌더링을 미루기도 하고, 중단했다 다시 이어서 처리하기도 합니다.

Fiber는 모든 컴포넌트에 작업 단위를 부여하고, 그 단위들을 마치 운영체제의 프로세스처럼 관리합니다.
이 구조가 있으니까 useEffect가 mount → update → cleanup → unmount 로 나뉘어 실행될 수 있는 거고, setState가 여러 번 호출돼도 적절히 묶어서 한 번에 처리될 수 있던 걸 아마 이번 과제를 통해서 useState 와 useEffect 의 구현 시도를 해보셨다면 다들 느끼셨을 거 같습니다.

또 React는 DOM을 매번 갈아엎는 게 아니라, Reconciliation이라는 비교 알고리즘을 통해 딱 필요한 부분만 업데이트합니다.
이 알고리즘을 직접 구현해보면 좋았겠지만 우선은 과제 통과에 시간을 많이들여서 시도해보지는 못했습니다.
그래도 Reconciliation으로 가상 DOM이 실제 DOM보다 빠르다게 이래서 그랬구나 하고 간접적으로 느껴봤습니다.

SPA를 직접 구현해보니, 라우팅, 상태 관리, DOM 업데이트, 비동기 처리, 최적화 등..
React와 Next가 그동안 조용히 대신해주고 있던 작업들이 얼마나 많았는지 체감하였습니다.

그러다 보니 이번 과제는 단순히 기능을 만드는 시간이 아니라, 우리가 당연하게 쓰는 기술이 실제로 어떤 원리를 품고 있는지 직접 들여다보는 시간을 준거 같아서 매우 만족하고있습니다. 기능 구현과 과제 통과에 많은 시간을 들였지만 이번 과제가 아니였다면 평소에 얻지 못했을 깨달음이어서 더욱 값진 시간이었습니다.

기술적 성장

React Fiber의 필요성

  • 컴포넌트별 state 격리
  • 동시 렌더링 가능
  • 우선순위 처리

Hooks 규칙의 이유

  • 호출 순서로 state 구분
  • 순서가 바뀌면 states 배열 꼬임
  • Fiber로도 순서는 필요

Store vs Component State

  • Store: 의도적 공유
  • Component State: 독립적 관리
  • 구현: useState의 기능을 구현했다 생각했지만 Store로 작용하고있던 어처구니 없는 구현(?)

렌더링의 복잡성

  • Batching 필요
  • Virtual DOM 유용
  • 부분 렌더링 어려움

import.meta.env.MODE 와 process.env.NODE_ENV 의 차이

자랑하고 싶은 코드

개선이 필요하다고 생각하는 코드

전체적으로 추상화가 부족하고 main에 몰려있는 상태여서 리팩토링을 전체적으로 진행해야합니다.

학습 효과 분석

가장 큰 배움이 있었던 부분

사실 완벽히 이해했다기보다는 이제는 그냥 사용하기보다는 이해하려 시도하는 첫발을 내디뎠다 생각합니다.
"직접 만들어보니 React가 왜 이렇게 설계되었는지 엿볼 수 있었다."

  • React의 제약사항 = 내부 구현의 필연적 결과
  • Hooks 규칙 = index 기반 설계의 부작용
  • Fiber = 전역 배열 한계의 해결책
  • Virtual DOM = 효율적 렌더링의 열쇠

코드를 사용하는 것과 코드를 이해하는 것의 차이를 체감했습니다.

추가 학습이 필요한 영역

  • Fiber 구조 공부

  • Reconciliation 알고리즘

  • Priority Queue

  • Time Slicing

  • Virtual DOM 구현

  • diff 알고리즘

  • patch 최적화

과제 피드백

이번 과제가 좋았던 이유는 단순히 동작하는 SPA를 만들었다는 성취감보다도, 그 과정에서 라우팅, 상태 관리, 렌더링 사이클이라는 근본적인 영역들을 직접 만져볼 수 있었다는 점입니다.

AI 활용 경험 공유하기

AI로 "직접 만들어보면 이해할 수 있지 않을까?" 라는 생각으로 시작했습니다.
개인적으로 프로젝트를 생성하여 같은 환경을 구성하여 작업하였습니다.
아쉬운건..직접 만들고 경험하면서 아 잘못만들었다~하고 히스토리를 제대로 남겨놓지 않아서
PR에 경험 공유에 대한 내용이 많이 유실된거같아 아쉬운 마음입니다.

React를 사용하면서 늘 궁금했던 것들이 있는데,

  • useState는 어떻게 상태를 기억하는가?
  • useEffect는 언제, 어떻게 실행되는가?

1단계

const states = [];
let index = 0;

const useState = (initialValue) => {
  const currentIndex = index;  // 클로저로 캡처

  if (states[currentIndex] === undefined) {
    states[currentIndex] = initialValue;
  }

  const setState = (newValue) => {
    states[currentIndex] = newValue;
    index = 0;  // 리렌더링 시 인덱스 초기화
    render();
  };

  index++;
  return [states[currentIndex], setState];
};
  • 클로저: currentIndex가 각 useState마다 고유하게 캡처됨
  • 배열: 여러 상태를 순서대로 저장
  • 인덱스: 렌더링마다 0부터 시작해서 순서 보장

2단계

const setState = (newValue) => {
  // 함수형 업데이트 지원
  const nextValue = typeof newValue === 'function'
    ? newValue(states[currentIndex])
    : newValue;

  // Object.is()로 비교 
  if (Object.is(states[currentIndex], nextValue)) {
    return;  // 같으면 리렌더링 스킵
  }

  states[currentIndex] = nextValue;
  index = 0;
  render();
};
  • 함수형 업데이트로 최신 상태 보장
  • 성능 최적화: 같은 값이면 리렌더링 스킵
const effects = [];

const useEffect = (effect, deps) => {
  const currentIndex = index;
  const prevEffect = effects[currentIndex];

  // deps 변경 감지
  const hasChanged =
    !prevEffect ||
    !deps ||
    deps.some((dep, i) => !Object.is(dep, prevEffect.deps?.[i]));

  if (hasChanged) {
    // 이전 cleanup 실행
    if (prevEffect?.cleanup) {
      prevEffect.cleanup();
    }

    // effect 실행
    const cleanup = effect();

    // 저장
    effects[currentIndex] = { deps, cleanup };
  }

  index++;
};
  • Mount: deps가 없거나 처음 실행 → effect 실행
  • Update: deps 배열의 값이 변경 → cleanup → effect 실행
  • Unmount: cleanup 함수 실행

cleanup은 다음 effect 실행 전에 실행됨!

Observer 패턴 Router 구현을 구현하면서 멘토링 시간과 평일 QA에 본걸 기반으로 만들어보려했는데 이해를 못했습니다..
사실 notify를 제안한 클로드한테 아래 처럼 물어봤는데도..아니 구독자들한테 알림을 주면 뭐가 좋은데? 하면서
물어봤지만 그래도..명확히 해결을 못했습니다.

Q: subscribe/notify가 뭐야?

A: 유튜브 구독 개념!

  • subscribe = 구독 신청
  • notify = 새 영상 알림
  • callback = 내가 보는 행동
const createRouter = () => {
  let currentPath = window.location.pathname;
  const subscribers = [];

  const subscribe = (callback) => {
    subscribers.push(callback);
  };

  const notify = () => {
    subscribers.forEach(callback => callback());
  };

  const push = (path) => {
    currentPath = path;
    window.history.pushState({}, '', path);
    notify();  // 구독자들에게 알림!
  };

  // 브라우저 뒤로가기 감지
  window.addEventListener('popstate', () => {
    currentPath = window.location.pathname;
    notify();
  });

  return { subscribe, push, path: currentPath };
};

Router 에 Quuer 파라미터 구현도 같이 하기

const getQuery = () => {
  const params = new URLSearchParams(window.location.search);
  const query = {};
  for (const [key, value] of params) {
    query[key] = value;
  }
  return query;
};

const setQuery = (newQuery) => {
  const currentQuery = getQuery();
  const updatedQuery = { ...currentQuery, ...newQuery };

  // 빈 값 제거
  Object.keys(updatedQuery).forEach(key => {
    if (!updatedQuery[key]) delete updatedQuery[key];
  });

  const params = new URLSearchParams(updatedQuery);
  const queryString = params.toString();
  const newUrl = `${currentPath}${queryString ? '?' + queryString : ''}`;

  push(newUrl);
};

그리고 다시 api 요청을 하고 확인하던 중 문제를 발견해서 고쳐보기도하고..

// ❌ 잘못된 코드
const push = (path) => {
  currentPath = path;  // '/products?search=laptop'
  // → pathname과 query가 섞임!
};

// ✅ 수정
const push = (path) => {
  const url = new URL(path, window.location.origin);
  currentPath = url.pathname;  // '/products'만 저장
  window.history.pushState({}, '', path);
  notify();
};

useEffect가 실행 안 됨 (부분 렌더링 시도)

시도한 것

// 숫자만 업데이트하려고 시도
const countDisplay = document.getElementById('count-display');
countDisplay.textContent = count;

문제: useEffect hooks이 재실행되지 않음 (Counter 함수를 다시 안 불러서)

  • React의 Virtual DOM 필요성 이해
  • 부분 렌더링은 생각보다 복잡함

문제: Query 변경 시 UI 업데이트 안 됨

증상: 검색, 카테고리 변경해도 화면 그대로

// router.push() 내부
currentPath = '/products?search=laptop';  // ❌ query 포함

해결: currentPath에 query까지 저장되어 라우팅 로직 망가지는거같아 URL 파싱해서 pathname만 추출하여 사용했습니다.

그리고 진짜 크리티컬한 문제들은..렌더링이 3번 일어나는데..useState 에서 문제를 찾았습니다.

1번: loading: true, products: []
2번: loading: true, products: [12]    ?
3: loading: false, products: [12]

클로드와 찾아본 문제의 원인은 아래 코드라는데 음..

.then((data) => {
  setProducts(data.products);  // 1번 렌더링
  setLoading(false);            // 2번 렌더링
})
  • React는 각 setState마다 렌더링 트리거
  • React 18의 Automatic Batching이 해결
  • 현재 구현은 batching 없음 → 2번 렌더링 정상

라는 깨달음을 얻고..

전역 Hooks 인스턴스 = Store? 라는 문제가 일어났습니다..useState가

// hooks.js
const states = [];  // ← 모든 컴포넌트가 공유

// ProductList
const [products] = useState([]);  // states[0]
const [loading] = useState(true);  // states[1]

// OtherComponent (만약 동시 렌더링)
const [data] = useState(null);  // states[2] ← 섞임!
  • 구현: Store처럼 작동 (전역 배열)
  • 의도: Component State (격리)
  • 제약: 한 번에 하나의 컴포넌트만 렌더링

라는 문제로 useState 구현을 완전이 잘못했다...라고 생각하고 useEffect와 useState 방식으로 본 과제의 리팩토링은 못했습니다.

그래도 이러한 문제들도,

Fiber 아키텍처를 공부해야하고. Fiber 아키텍처가

  • 각 컴포넌트마다 독립된 Fiber 노드
  • 각 Fiber에 독립된 states 배열

가능하다는걸 깨달았습니다. Fiber 에 대해 항상 들어보긴 했지만 스윽 읽고 넘어가고 했었는데 결국 이렇게 공부하는 날이 왔습니다.

이번에는 그래도 실패했지만,,다음주에 다룬다고 들었던거같은데 다시 한번 츄라이 해보겠습니다.

그리고 아...잘못만들었네 하고 클로드한테 하소연 하니까

React 역사의 필연성

시행착오:

  1. 2013: Class Component → setState 하나로 관리
  2. 2015: 성능 문제 → Fiber 아키텍처 구상
  3. 2017: Fiber 완성 → 컴포넌트별 격리
  4. 2019: Hooks 출시 → Fiber 덕분에 가능

우리의 여정 = React의 역사를 경험했다. 라고 위로해주던데 이번 과제 덕분에 이런거도 해보고 조금 더 고민해보고 생각하는 법을 배운거같습니다. 근데 이게 AI 활용경험 공유가 맞나..?우선 실패한 경험입니다. 🤣

리뷰 받고 싶은 내용

Copy link
Contributor

@JunilHwang JunilHwang left a 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은 요구사항에 맞춰 상품 목록, 상세, 장바구니, 검색 및 필터, 무한 스크롤 등 핵심 기능을 충실히 구현하였으며, Mock API 서버와 MSW를 사용해 테스트 가능하도록 구성했습니다. 컴포넌트를 잘 분리하여 역할 분담이 이루어져 있고, SPA 라우팅 관리도 적절히 수행되고 있습니다.

다만, 단일 파일(main.js) 내에 렌더링, 상태 관리, URL parsing/동기화, 이벤트 핸들링이 복합적으로 얽혀 있어 향후 기능 추가나 유지보수 시 복잡도가 증가할 수 있는 점은 개선이 필요합니다.

현재 코드 구조

  • main.js가 렌더링과 이벤트 핸들링, 상태 관리(로컬스토리지 및 URL Parsing) 대부분을 담당
  • API 통신과 뷰 렌더링, 컴포넌트가 명확히 분리되어 비교적 이해하기 쉬움
  • 무한 스크롤, 장바구니 상태 관리 로직이 utils 내 별도 파일로 분리됨
  • UI 컴포넌트들은 태그 스트링 템플릿 기반으로 최소한의 리턴 단위로 구현됨

설계 및 확장성 피드백

  • 상태 관리 및 URL 동기화 로직을 별도 모듈이나 상태 컨테이너로 분리하면 유지보수 및 확장성이 크게 향상됩니다.
  • 렌더링과 로직 결합도를 줄이고 컴포넌트 리렌더링 최적화 기법을 도입할 여지가 있습니다.
  • 이벤트 핸들러를 좀 더 작고 역할별로 분리하여 가독성을 높이는 방안을 권장합니다.
  • 상품 상세 페이지, 장바구니 등 UI 컴포넌트 내부 SNS나 SVG 등을 컴포넌트 단위로 더 분리하면 재사용성과 테스트 편리성이 좋아집니다.

전반적으로 요구사항에 잘 부합하며, 다음 단계에서는 상태 관리, 컴포넌트 설계 패턴 및 효율적인 DOM 업데이트 방법에 집중해보시면 좋겠습니다.PR에 문의 주신 추가 질문이 없어, 전체적인 코드 이해를 기반으로 심도있게 답변 드립니다.

상태 관리와 URL 동기화

현 코드에서는 URL 쿼리 파라미터를 직접 읽어 렌더링 시 상태로 활용하는 방식을 차용하고 있습니다. 이런 방식은 절대적인 단일 진실 소스로서 URL을 사용한다는 장점이 있지만, 앱 상태가 복잡해질수록 URL과 내부 메모리 상태의 불일치 가능성, 이벤트 중복 처리, 로직 분산 등 유지보수 비용 증가 요인도 함께 발생합니다.

실제 확장 가능성을 고려할 때는 중간 상태 객체(State Store)를 두고, URL 파라미터->상태 변환, 상태->URL 업데이트를 엄격히 컨트롤하는 구조를 도입하는 것이 좋습니다. 이를 통해 앱 내 모든 상태 변화를 중앙 집중식으로 추적 및 관리할 수 있고, 변경되는 부분만 렌더링해 성능 향상도 가능합니다.

DOM 이벤트 처리

본 코드의 이벤트 위임 방식은 매우 효율적인데, 이벤트 핸들러가 점점 길어지는 것을 여러 작은 함수로 분할하면 가독성과 유지보수성이 크게 올라갑니다. 예를 들어 "장바구니 수량 증가" 관련 이벤트 핸들러를 별도 함수로 분리한 후, 이벤트 내에서는 해당 함수 호출만 수행하는 식입니다.

UI 컴포넌트 디자인

컴포넌트들이 순수 템플릿 문자열로 구성되어 있어 프레임워크 없이도 동작하지만, 관리 포인트가 많아지면 컴포넌트별로 독립된 상태와 인터랙션을 갖기 어려울 수 있습니다. 따라서 컴포넌트 단위로 이벤트 바인딩, 상태 관리 책임 분리를 차차 도입해 나가는 것도 고려해보세요.

토스트 및 모달 관리

여러 토스트가 연속해서 나타나는 시나리오, 혹은 다양한 타입별 메시지 처리 로직이 필요하다면 토스트 생성 큐(queue) 관리, 모달 상태 관리 방식 등을 별도로 구현하는 것도 추천됩니다.

API 모킹 및 테스트

Mock Service Worker 설정과 Playwright 테스트가 잘 통합되어 있어 신뢰할 수 있는 테스트 커버리지가 확보되어 있습니다. 실제 API 환경과 차이가 있을 수 있으므로 모킹 데이터 업데이트 주기에 신경 써야 합니다.


이상입니다. 기능 구현 과정과 테스트 모두 잘 이루어졌으니, 다음 스텝으로 상태 관리 아키텍처와 UI 컴포넌트 분리 및 최적화 작업에 집중해보시면 더 좋은 코드 품질을 경험할 수 있을 것입니다. 언제든 추가 질문 주시면 상세히 도와드리겠습니다.

.then(([productsData, categoriesData]) => {
const { products, pagination } = productsData;
const { filters } = pageState;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1. 문제 상황 제시

사이트가 성장하며 상품 목록에 다양한 조건(검색어, 카테고리, 정렬, 페이지 수 등)이 증가할 경우, render 함수가 복잡한 URL 파라미터 읽기 및 비교 작업과 상품 목록 API 요청 및 렌더링까지 모두 담당하고 있음.

현재 코드의 한계:

  • render 함수가 너무 많은 책임을 가지고 있어 유지보수가 어려움
  • URL 파라미터 및 상태가 render 함수 내부에 산재되어 재사용과 확장이 어렵다
  • 무한 스크롤 페이지네이션 로직이 initInfiniteScroll 함수에 분리되어 있지만, 전체 상태 관리 및 UI 업데이트가 명확하게 분리되지 않아 확장성 저하

2. 근본 원인

핵심 문제: 컴포넌트 렌더링, 상태 관리, URL 파라미터 읽기/쓰기, API 요청이 한 함수 또는 몇몇 함수에 집중되어 있어 관심사가 분리되어 있지 않음

왜 문제인가:
이 구조는 새로운 요구사항(예: 카테고리 추가, 필터 추가, SEO 최적화, SSR 대응 등)이 발생하면 한 함수의 수정이 매우 복잡해지고, 테스트와 디버깅도 어려워짐.

3. 개선 구조

현재 구조:
render 함수 내에서 URL 파라미터 읽기 → API 호출 → 렌더링 결과를 출려 → 이벤트에 따른 URL push 및 재렌더링

개선된 구조:

  • URL 상태 관리 추상화: URL과 상태를 1:1 매핑하는 별도 모듈로 분리
  • 상태 관리와 데이터 페칭 분리: 상태 변화에 따른 API 호출과 데이터 변화를 별도의 책임으로 분리
  • 컴포넌트별 렌더링 분리: 상태 변화에 따라 필요한 컴포넌트만 재렌더링하도록 분리
  • 무한 스크롤과 상태 관리를 더 유기적으로 연결하고, 상태 변화에 따라 무한 스크롤 초기화 및 파기 관리

개선 사항:

  • URL 파라미터를 읽어 상태를 만들고, 반대로 상태 변경 시 URL을 업데이트하는 유틸리티 함수 작성
  • 상태 관리를 전역 객체 또는 상태 관리 라이브러리 도입 (간단한 경우에선 옵저버 패턴 적용 가능)
  • 렌더링 함수들은 상태를 받아 UI만 담당하도록 설계
  • 이벤트 핸들러에서는 상태를 변경하고, 상태 변경 시 렌더링 함수가 호출되는 흐름

코드 비교:

// ❌ 현재: render 함수가 모든 역할 수행
async function render() {
  const url = new URL(window.location);
  const limit = parseInt(url.searchParams.get('limit')) || 20;
  const page = parseInt(url.searchParams.get('current')) || 1;
  const search = url.searchParams.get('search') || '';
  ...
  // API 호출 및 렌더링, 무한 스크롤 초기화 등
}

// ✅ 개선된 방식 예시 (의사코드)
const state = { limit: 20, page: 1, search: '', category1: '', category2: '', sort: 'price_asc' };

function syncStateAndURL() {
  // URL -> 상태 읽기 또는 상태 -> URL 쓰기 구현
}

async function fetchProducts() {
  // API 호출만 담당
  return getProducts(state);
}

async function renderProducts() {
  const products = await fetchProducts();
  ProductList.render(products);
}

function onStateChange() {
  syncStateAndURL();
  renderProducts();
  initInfiniteScroll(state);
}

// 이벤트 핸들러에서는 상태만 변경
function onFilterChange(newFilters) {
  Object.assign(state, newFilters);
  onStateChange();
}

10가지 추가 범용 피드백

  1. 이벤트 위임 사용은 좋으나 이벤트 핸들러 로직이 너무 길어 가독성과 유지보수성 저하
  2. 여러 요소를 여러 번 쿼리하는 부분에 변수 할당 후 재사용하면 성능과 가독성 개선 가능
  3. 하드코딩된 문자열(URL path, CSS class 등)이 많으므로 enum 또는 상수로 분리하는 것이 좋음
  4. 중복 코드(특히 모달 열기/닫기, 토스트 알림 생성 등)는 함수화 또는 컴포넌트화 권장
  5. 토스트 메시지와 모달은 UI 상태 관리 시스템 도입 시 분리하여 관리하면 더 효율적
  6. 가격 등을 숫자 처리할 때 타입 안정성을 위해 parseInt/Number 변환 시 예외 처리 검토
  7. 컴포넌트가 많은데, 복잡도 때문에 파일명과 folder 구조를 더 체계적으로 관리하면 좋음
  8. CSS 클래스를 Tailwind와 함께 사용하고 있는데, utility 클래스 중복 최소화를 위해 공통 스타일 분리 고려
  9. API 호출 실패 시 상세 에러 처리가 부족하며 사용자에게 적절히 안내하는 로직 강화 필요
  10. localStorage 관련 작업은 JSON 파싱/스트링화 시 실패 케이스 핸들링과 동시성 문제 고려 필요

if ($target.closest('.breadcrumb-link')) {
e.stopPropagation();
const button = $target.closest('.breadcrumb-link');
const category1 = button.dataset.category1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2. 문제 상황 제시

현재 장바구니 추가, 수량 변경 및 삭제 등 기능들이 utils/cart.js와 main.js에 중복 연결되어 있지만,
상태 동기화 및 UI 업데이트가 일관되게 처리되지 않아 사용자 경험에 영향이 있을 수 있음.

현재 코드의 한계:

  • 장바구니에 추가/수량 변경 시 모달 새로고침과 헤더 배지 업데이트가 산발적으로 호출됨
  • UI 리렌더링과 데이터 상태 관리가 분리되어 있어 변경 시 UI가 늦게 반영되거나 누락될 가능성 존재

2. 근본 원인

비즈니스 로직(상태 변경)과 UI 갱신(렌더링)이 명확하게 분리되지 않고 서로 의존되어 있어, 상태 변경 시 자동 UI 갱신 메커니즘이 부족함.

3. 개선 구조

  • 상태 변경 함수들이 이벤트 핸들러 내에서 직접 UI를 갱신하지 않고, 상태 변경 후 별도의 UI 갱신 함수가 호출되도록 변경
  • Pub/Sub 또는 옵저버 패턴을 도입하여 상태 변경 시 자동으로 관련 UI 컴포넌트가 업데이트되도록 개선

코드 비교:

// ❌ 현재 모든 핸들러에서 updateHeader(); refreshCartModal(); showToast() 직접 호출
addToCart(product, quantity);
updateHeader();
refreshCartModal();

// ✅ 개선
function onCartChange() {
  updateHeader();
  refreshCartModal();
}

const addProductToCart = (product, quantity) => {
  addToCart(product, quantity);
  onCartChange();
};

const category1 = $target.dataset.category1;
const url = new URL(window.location);
url.searchParams.set('category1', category1);
url.searchParams.delete('category2');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3. 문제 상황 제시

이벤트 핸들러 내에서 dom 쿼리 및 데이터 추출, 상태 변경, UI 변경, URL 관리 등이 밀접하게 섞여 있어, 코드 가독성과 유지보수성이 떨어짐.

현재 코드의 한계:

  • 개별 이벤트 핸들러가 매우 크고 많은 행위를 수행, 로직 분리 필요
  • 재사용성 낮고 테스트가 어려움

3. 근본 원인

관심사의 분리가 미흡하여, 한 함수에 여러 역할이 혼재되어 있음.

3. 개선 구조

  • 이벤트 핸들러에서 DOM 조작 코드를 최소화하고, 상태 관리 함수와 UI 렌더링 함수를 각각 호출
  • 상태 변경과 URL 동기화를 별도의 모듈로 분리하여 관리
  • 이벤트 위임 시 클릭된 요소에 따른 핸들링만 수행하고, 상태 변경은 별도의 함수 호출로 외부 분리

코드 비교:

// ❌ 현재 방식
if ($target.closest('.quantity-increase-btn')) {
  const productId = $target.dataset.productId;
  const item = getCartData().items.find(item => item.id === productId);
  updateCartQuantity(productId, item.quantity + 1);
  refreshCartModal();
}

// ✅ 개선
function onQuantityIncrease(productId) {
  updateCartQuantity(productId, getCartQuantity(productId) + 1);
  refreshCartModal();
}

// 이벤트 핸들러 내
if ($target.closest('.quantity-increase-btn')) {
  const productId = $target.dataset.productId;
  onQuantityIncrease(productId);
}

`;

export const Rating = ({ rating }) => {
return /* HTML */ `
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4. 문제 상황 제시

Rating 컴포넌트가 rating 값을 소수점 포함한 실수로 받음에도 fullStar, emptyStar 반복을 정수로만 처리하고 있어 소숫점 반영이 누락되는 문제 발생 가능

현재 코드의 한계:

  • 별점이 4.5일 경우, 4개만 꽉 찬 별표가 표시되고 반쪽 별 등 세밀한 표현 불가

4. 근본 원인

단순 정수 카운트만 사용해 별점 UI가 표현되어, 다양한 평점 표현 확장성 부족

4. 개선 사항

  • 정수, 반쪽 별(half star) 처리 포함한 UI 개선
  • 별점 5개 중 몇 개가 full, half, empty인지 계산해서 출력

코드 비교:

// ❌ 현재 방식
${fullStar.repeat(rating)} ${emptyStar.repeat(5 - rating)}

// ✅ 개선된 방식 (반별점 처리)
const fullStars = Math.floor(rating);
const halfStar = rating - fullStars >= 0.5;
const emptyStars = 5 - fullStars - (halfStar ? 1 : 0);

return /* HTML */ `
  <div class="flex items-center">
    ${fullStar.repeat(fullStars)}
    ${halfStar ? halfStarSVG : ''}
    ${emptyStar.repeat(emptyStars)}
  </div>
`;

<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5. 문제 상황 제시

토스트 팝업 닫기 버튼 클릭과 자동 사라지는 타이머가 있음에도, 동시에 여러 토스트 등장 시 관리가 복잡하며,
토스트가 계속 생성될 경우 DOM 누적 우려 있음

현재 코드의 한계:

  • 토스트 생성 시 인스턴스 관리 미흡으로 여러 토스트 간의 충돌이나 메모리 누수 가능성

5. 근본 원인

토스트 요소가 DOM에 계속 쌓이며, 제거 시기 관리가 자동 타이머와 수동 닫기가 별도임

5. 개선 사항

  • 토스트 팝업을 큐로 관리하여 동시 생성 개수를 제한하거나 뒤로 밀림 현상 완화
  • 개별 토스트 구성요소에 고유 id 할당 및 상태 관리
  • 필요 시 토스트 컴포넌트 클래스로 리팩토링 권장

// 로딩 UI 숨김
const $loadingEl = document.querySelector('#infinite-scroll-loading');
if ($loadingEl) $loadingEl.style.display = 'none';
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6. 문제 상황 제시

무한 스크롤 구현 시 Intersection Observer를 사용하여 잘 동작하지만,
상태 객체의 직접 수정을 여러 곳에서 수행하여 복잡도가 오를 수 있음

현재 코드의 한계:

  • 상태 관리의 일관성이 부족하며 필요한 부분과 상태 값을 명확히 분리하는 구조가 아님
  • 예외 상황(네트워크 오류, 스크롤 중복 호출 등) 핸들링 미흡

6. 개선 사항

  • 상태 변경은 setState 함수로 제한하여 사이드 이펙트를 방지
  • 네트워크 오류, 더 이상 데이터 없을 때 UI 적절히 알림
  • intersection observer 해제와 상태 초기화를 명확히 구분하여 메모리 누수 방지

>
${cat1}
</button>
`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

7. 문제 상황 제시

카테고리 버튼 컴포넌트에서 1depth와 2depth 카테고리를 렌더링하지만,
버튼의 선택 상태 표시를 위한 스타일 조건이 복잡하게 하드코딩 되어 있음

현재 코드의 한계:

  • 조건문 inline으로 작성되어 코드 가독성 떨어짐
  • UI 변화 시 스타일 관리 복잡

7. 개선 사항

  • 선택된 카테고리 상태를 별 변수에 미리 할당해 가독성 개선
  • 조건부 CSS 적용을 함수나 라이브러리로 분리하면 유지보수 편리

viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

8. 문제 상황 제시

브레드크럼 구조가 2depth까지만 지원하는 듯 보이나,
차후 3depth 이상 확장 필요시 현재 구조는 하드코딩되어 확장성 낮음

현재 코드의 한계:

  • 신규 깊은 카테고리(depth) 추가 시 컴포넌트 전체 수정 필요

8. 개선 사항

  • 카테고리 배열 경로를 받아 map 렌더링하는 재귀적 혹은 반복적 컴포넌트 설계 권장
  • 유동적인 깊이 지원으로 UI 확장성 확보 가능

></path>
</svg>
`;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

9. 문제 상황 제시

스타 SVG 아이콘들이 컴포넌트 상단에 하드코딩되어 있어 코드 분량이 길고, 재사용성이 떨어짐

현재 코드의 한계:

  • 아이콘 변경, 스타일 조정시 SVG 코드를 직접 수정해야 함

9. 개선 사항

  • SVG 컴포넌트를 별도 파일이나 React, Vue 등 컴포넌트 형태로 분리 추천
  • CSS 색상 변경 시 클래스 조작하거나 스타일 프롭 활용 권장

category2,
category3,
category4,
sort,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

10. 문제 상황 제시

API delay 함수가 고정 값으로 되어 있어 상황에 따라 다양한 지연 시뮬레이션이 어렵고, 테스트 유연성이 제한

현재 코드의 한계:

  • 모든 API 응답에서 200ms 딜레이만 사용됨

10. 개선 사항

  • delay 함수를 매개변수로 지연 시간 설정 가능하게 개선하면 테스트 목적에 따라 활용도 증가
const delay = (ms = 200) => new Promise(resolve => setTimeout(resolve, ms));

Copy link

@eveneul eveneul left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

재윤 님! 이번 주차도 고생 많으셨습니다~ 자바스크립트 챕터라고 했지만 생각보다 어마무시한 난이도에 많이 당황하셨죠? ㅎㅎ 그래도 잘 해내셨네요!

main.js에 여러 기능들이 있는데, 이걸 분리하면 더 좋을 것 같습니다!

다음 주도 같이 파이팅해 봅시다~~!!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Category 버튼을 1뎁스, 2뎁스를 따로따로 만드는 것보다는 하나의 컴포넌트로 합치는 건 어떨까요? 저도 이번 챕터 문제를 풀 때 똑같은 고민을 했다가 재윤 님처럼 두 개로 나눈 경험이 있는데요, 지금 생각해 보면 카테고리 뎁스가 더 많아질 때는 어떻게 하지? 생각이 들어서요.

제가 생각한 코드는..

const Category = (attrs, label, isSelected) => /* HTML */ `
  <button 
    ${Object.entries(attrs)
      .map((k, v) => `data-${k}=${v}`)
      .join(" ")}
    class="px-3 py-2 text-sm rounded-md border transition-color 
    ${isSelected ? "bg-blue-100 border-blue-300 text-blue-800" : "bg-white border-gray-300 text-gray-700 hover:bg-gray-50"}
  >${label}</button>
`;

이렇게 만들고, Category 컴포넌트를 사용할 때는,

.map(product => Category({ category1: product }, product, false)
.map(product => Category({ category1: selectedCategory1, category2: product },
product,
product === selectedCategory2 
)
)

이런 식으로 해도 좋을 것 같아용

error: 'bg-red-600',
};

const icons = {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

토스트 ui가 상태별로 나뉘어지는데, 요렇게 나누신 거 좋다고 생각합니다!

requestAnimationFrame(() => {
toast.style.opacity = '1';
});
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

requestAnimation을 두 번 중첩해서 쓰는 것보다 opacity 0과 transition 설정은 기존 Toast 컴포넌트 CSS로 박고 opacity 1, 0으로 하는 건 어떨까요! requestAnimationFrame을 두 번 쓰신 이유가 궁금해용

};

// 장바구니 모달 닫기
const closeCartModal = () => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 main.js에 장바구니 관련 기능, render 관련 기능 등.. 여러 독립적인 기능들이 한꺼번에 모여 있어 한 파일 속 코드의 길이가 너무 긴 것 같아요! /core/ 같은 파일을 만들어서 각각 분리해 놓으면 어떨까요?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants