BBAK-jun 님의 상세페이지[3팀 박준형] Chapter 1-1. 프레임워크 없이 SPA 만들기

과제 체크포인트

배포 링크

기본과제

상품목록

상품 목록 로딩
  • 페이지 접속 시 로딩 상태가 표시된다
  • 데이터 로드 완료 후 상품 목록이 렌더링된다
  • 로딩 실패 시 에러 상태가 표시된다
  • 에러 발생 시 재시도 버튼이 제공된다
상품 목록 조회
  • 각 상품의 기본 정보(이미지, 상품명, 가격)가 카드 형태로 표시된다
  • 한 페이지에 보여질 상품 수 선택
  • 드롭다운에서 10, 20, 50, 100개 중 선택할 수 있으며 기본 값은 20개 이다.
  • 선택 변경 시 즉시 목록에 반영된다
상품 정렬 기능
  • 상품을 가격순/인기순으로 오름차순/내림차순 정렬을 할 수 있다.
  • 드롭다운을 통해 정렬 기준을 선택할 수 있다
  • 정렬 변경 시 즉시 목록에 반영된다
무한 스크롤 페이지네이션
  • 페이지 하단 근처 도달 시 다음 페이지 데이터가 자동 로드된다
  • 스크롤에 따라 계속해서 새로운 상품들이 목록에 추가된다
  • 새 데이터 로드 중일 때 로딩 인디케이터와 스켈레톤 UI가 표시된다
  • 홈 페이지에서만 무한 스크롤이 활성화된다
상품을 장바구니에 담기
  • 각 상품에 장바구니 추가 버튼이 있다
  • 버튼 클릭 시 해당 상품이 장바구니에 추가된다
  • 추가 완료 시 사용자에게 알림이 표시된다
상품 검색
  • 상품명 기반 검색을 위한 텍스트 입력 필드가 있다
  • 검색 버튼 클릭으로 검색이 수행된다
  • Enter 키로 검색이 수행된다
  • 검색어와 일치하는 상품들만 목록에 표시된다
카테고리 선택
  • 사용 가능한 카테고리들을 선택할 수 있는 UI가 제공된다
  • 선택된 카테고리에 해당하는 상품들만 표시된다
  • 전체 상품 보기로 돌아갈 수 있다
  • 2단계 카테고리 구조를 지원한다 (1depth, 2depth)
카테고리 네비게이션
  • 현재 선택된 카테고리 경로가 브레드크럼으로 표시된다
  • 브레드크럼의 각 단계를 클릭하여 상위 카테고리로 이동할 수 있다
  • "전체" > "1depth 카테고리" > "2depth 카테고리" 형태로 표시된다
현재 상품 수 표시
  • 현재 조건에서 조회된 총 상품 수가 화면에 표시된다
  • 검색이나 필터 적용 시 상품 수가 실시간으로 업데이트된다
  • [x]장바구니
장바구니 모달
  • 장바구니 아이콘 클릭 시 모달 형태로 장바구니가 열린다
  • 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로 다시 구현한다.
  • 이 과정에서 직접 가공하는 것은 최대한 지양한다.

기술적 성장

새로 학습한 개념

  • Pub-Sub 패턴을 활용한 상태 관리 구현
    • Store 클래스를 구현하여 전역 상태 관리
    • 구독자 패턴으로 상태 변경 시 자동 업데이트
  • 브라우저 스토리지 추상화
    • StorageAdapter 인터페이스 구현
    • LocalStorage와 IndexedDB 어댑터 구현으로 확장성 확보
  • SPA 라우팅 구현
    • History API를 활용한 클라이언트 사이드 라우팅
    • 동적 라우트 파라미터 처리
    • 404 페이지 처리

기존 지식의 재발견/심화

  • 컴포넌트 기반 아키텍처
    • BaseComponent 클래스를 통한 컴포넌트 생명주기 관리
    • 상태와 props의 분리
    • 템플릿 메서드 패턴 활용

과제 셀프회고

이번 과제를 통해 프레임워크 없이 순수 자바스크립트로 모던 웹 애플리케이션을 구현하면서, 프레임워크가 해결하고자 하는 문제들을 깊이 이해할 수 있었습니다.

1. React 클래스 컴포넌트 스타일 구현

가장 큰 고민은 React의 선언적 컴포넌트 시스템을 명령형으로 구현하는 것이었습니다. 특히 다음 부분들에 집중했습니다:

export default class BaseComponent {
  constructor($el, props = {}) {
    this.target = $el;
    this.props = props;
    this.state = {};
    this._handlers = new Map(); // 이벤트 핸들러 관리
    this.preserveSelectors = props.preserveSelectors || [];
  }

  setState(newState) {
    const prevState = { ...this.state };
    const nextState = { ...this.state, ...newState };
    
    if (this.shouldComponentUpdate(nextState)) {
      this.state = nextState;
      this.render();
      this.componentDidUpdate(prevState);
    }
  }

  // 최적화를 위한 업데이트 조건 체크
  shouldComponentUpdate(nextState) {
    return JSON.stringify(this.state) !== JSON.stringify(nextState);
  }
}
  • 생명주기 메서드 구현: React의 생명주기를 참고하여 componentDidMount, componentWillUnmount, shouldComponentUpdate 등을 구현
  • 상태 관리 최적화: 불필요한 리렌더링을 방지하기 위한 shouldComponentUpdate 구현
  • 메모리 관리: 컴포넌트 언마운트 시 이벤트 리스너와 구독 정리

2. 이벤트 시스템 개선

이벤트 관리는 성능과 메모리 누수 방지를 위해 특히 신경 썼습니다:

class BaseComponent {
  addEventDelegate(eventType, selector, callback) {
    const handler = (e) => {
      const target = e.target.closest(selector);
      if (!target) return;
      
      if (this.target.contains(target)) {
        callback.call(this, e, target);
      }
    };
    
    this.target.addEventListener(eventType, handler);
    
    // 이벤트 핸들러 추적
    if (!this._handlers.has(eventType)) {
      this._handlers.set(eventType, new Set());
    }
    this._handlers.get(eventType).add(handler);
    
    return () => this.removeEventHandler(eventType, handler);
  }

  removeEventHandler(eventType, handler) {
    this.target.removeEventListener(eventType, handler);
    this._handlers.get(eventType)?.delete(handler);
  }

  componentWillUnmount() {
    // 모든 이벤트 핸들러 정리
    this._handlers.forEach((handlers, eventType) => {
      handlers.forEach(handler => {
        this.removeEventHandler(eventType, handler);
      });
    });
    this._handlers.clear();
  }
}
  • 이벤트 위임: 동적으로 추가되는 요소들의 이벤트 처리
  • 메모리 누수 방지: 컴포넌트 제거 시 모든 이벤트 리스너 정리
  • 중복 등록 방지: 이벤트 핸들러 추적 및 관리

3. 전역 상태 관리 시스템

Pub/Sub 패턴을 기반으로 한 상태 관리 시스템을 구현했습니다:

class Store {
  constructor(initialState = {}) {
    this.state = { ...initialState };
    this.subscribers = new Map();
    this.middlewares = [];
  }

  subscribe(key, callback) {
    if (!this.subscribers.has(key)) {
      this.subscribers.set(key, new Set());
    }
    this.subscribers.get(key).add(callback);
    
    // 구독 해제 함수 반환
    return () => {
      this.subscribers.get(key)?.delete(callback);
    };
  }

  // 미들웨어 시스템
  addMiddleware(middleware) {
    this.middlewares.push(middleware);
  }

  setState(key, value, action = "UPDATE") {
    const prevValue = this.state[key];
    
    // 미들웨어 실행 (상태 변경 전)
    this.runMiddlewares("BEFORE_SET", { key, value, prevValue, action });
    
    this.state[key] = value;
    
    // 미들웨어 실행 (상태 변경 후)
    this.runMiddlewares("AFTER_SET", { key, value, prevValue, action });
    
    // 구독자들에게 알림
    this.notify(key, value, prevValue, action);
  }
}
  • 미들웨어 시스템: 상태 변경의 전후 처리를 위한 미들웨어 구현
  • 디버깅 지원: 상태 변경 로깅을 위한 미들웨어 추가
  • 메모리 관리: 구독 해제 기능 구현

주요 기술적 도전과 해결

  1. 컴포넌트 재사용성

    • 문제: 컴포넌트 간 의존성이 높아지면서 재사용이 어려워짐
    • 해결: Props를 통한 의존성 주입과 이벤트 버스 패턴 도입
  2. 상태 동기화

    • 문제: 여러 컴포넌트에서 동일한 상태 참조 시 동기화 문제
    • 해결: 중앙 집중식 Store와 구독 시스템으로 해결
  3. 라우팅과 컴포넌트 생명주기

    • 문제: SPA 라우팅 시 컴포넌트 정리가 제대로 되지 않는 문제
    • 해결: 명확한 생명주기 정의와 자동 정리 시스템 구현

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

  1. 상태 관리 개선점
  • 상태 변경 히스토리 추적 기능 추가
  • 상태 롤백 기능 구현
  • 디버깅을 위한 로깅 시스템 강화
  1. 라우터 개선점
  • 중첩 라우팅 지원
  • 라우트 가드 구현
  • 라우트 트랜지션 효과 추가
  1. 컴포넌트 시스템
  • 더 세밀한 생명주기 메서드 추가
  • 성능 최적화를 위한 메모이제이션
  • 이벤트 시스템 개선

학습 효과 분석

가장 큰 배움

  1. 상태 관리 시스템 설계
  • Pub-Sub 패턴의 실제 구현 경험
  • 미들웨어 시스템의 유용성 이해
  • 상태 지속성 관리의 중요성
  1. 컴포넌트 기반 설계
  • 재사용성과 확장성을 고려한 설계
  • 생명주기 관리의 중요성
  • 상태와 뷰의 분리
  1. 라우팅 시스템 구현
  • SPA에서의 라우팅 처리 방법
  • History API 활용
  • 동적 라우트 처리
  1. 의미 있는 테스트 코드 작성
  • 테스트는 순수 함수처럼 독립적이어야 함을 깨달음
    • 테스트 간의 의존성이나 순서 의존성이 있으면 안됨
    • 각 테스트는 자체적으로 완결성을 가져야 함
  • 테스트와 구현 코드 간의 강한 커플링 방지의 중요성
    • 구현 변경이 테스트 코드 전체를 무너뜨리면 안됨
    • 테스트는 행위(behavior)를 검증해야지, 구현(implementation)을 검증하면 안됨
  • 실제 사용자 시나리오에 기반한 테스트의 가치
    • 장바구니 추가/삭제/수정 등 실제 사용자 행동 기반 테스트
    • 카테고리 필터링, 검색 등 실제 사용 패턴 검증
  • 테스트 가능한 코드 설계의 중요성
    • 순수 함수 분리를 통한 테스트 용이성 확보
    • 의존성 주입을 통한 테스트 격리성 확보
    • 사이드 이펙트 분리를 통한 예측 가능성 향상

추가 학습 필요 영역

  1. 테스트 자동화
  • E2E 테스트 강화
  • 단위 테스트 커버리지 향상
  • 테스트 시나리오 다양화
  1. 성능 최적화
  • 컴포넌트 세분화를 통한 렌더링 최적화
    • 현재: 페이지, 헤더, 푸터 단위의 큰 컴포넌트 구조
    • 개선 방향: 더 작은 단위의 독립적 컴포넌트로 분리
      • 상품 카드, 카테고리 필터, 검색바 등을 독립 컴포넌트화
      • 상태 변경 시 필요한 부분만 리렌더링 가능하도록 구조 개선
    • 라이브러리와 웹앱 개발의 균형
      • 현재는 라이브러리 코어와 웹앱 구현이 혼재된 상태
      • 컴포넌트 세분화는 두 영역 모두에서 이점 제공
        • 라이브러리: 더 유연한 조합과 재사용성 확보
        • 웹앱: 렌더링 성능 최적화와 상태 관리 효율화
      • 결론: 컴포넌트 세분화는 양쪽 모두에게 필요한 개선 방향
  • 번들 사이즈 최적화
  • 캐싱 전략 수립

과제 피드백

개선하면 좋을 것 같은 부분

  1. 테스트 가독성 향상 - Given-When-Then 패턴 적용

❌ 현재 테스트 코드:

describe('장바구니 테스트', () => {
  it('상품 수량을 변경한다', () => {
    const product = { id: 1, name: 'Phone', price: 1000 };
    addToCart(product);
    updateQuantity(1, 3);
    expect(getCartItem(1).quantity).toBe(3);
  });
});

✅ Given-When-Then 패턴 적용:

describe('장바구니 테스트', () => {
  it('상품 수량을 변경할 수 있다', () => {
    // Given (준비): 테스트를 위한 상태 세팅
    const initialProduct = createTestProduct({
      id: 1,
      name: 'Phone',
      price: 1000
    });
    const cart = new Cart();
    cart.addItem(initialProduct);
    
    // When (실행): 테스트할 동작 수행
    cart.updateItemQuantity(initialProduct.id, 3);
    
    // Then (검증): 기대하는 결과 확인
    const updatedItem = cart.getItem(initialProduct.id);
    expect(updatedItem.quantity).toBe(3);
    expect(updatedItem.totalPrice).toBe(3000);
  });

  it('장바구니에서 선택된 상품들을 삭제할 수 있다', () => {
    // Given
    const cart = new Cart();
    const products = [
      createTestProduct({ id: 1, name: 'Phone' }),
      createTestProduct({ id: 2, name: 'Tablet' }),
      createTestProduct({ id: 3, name: 'Laptop' })
    ];
    products.forEach(product => cart.addItem(product));
    cart.selectItems([1, 2]); // 1, 2번 상품 선택
    
    // When
    cart.removeSelectedItems();
    
    // Then
    expect(cart.getAllItems()).toHaveLength(1);
    expect(cart.getItem(3)).toBeDefined();
    expect(cart.getSelectedItems()).toHaveLength(0);
  });

  it('장바구니 상품 가격이 변경되면 총액이 자동으로 계산된다', () => {
    // Given
    const cart = new Cart();
    const product = createTestProduct({
      id: 1,
      name: 'Phone',
      price: 1000
    });
    cart.addItem(product);
    
    // When
    cart.updateItemPrice(product.id, 1500);
    
    // Then
    const updatedCart = cart.getCartSummary();
    expect(updatedCart.totalPrice).toBe(1500);
    expect(updatedCart.itemCount).toBe(1);
  });
});

✅ 복잡한 시나리오에서의 Given-When-Then:

describe('상품 필터링 및 정렬', () => {
  it('카테고리로 필터링 후 가격순으로 정렬된다', () => {
    // Given
    const products = [
      createTestProduct({
        category: 'electronics',
        name: 'Expensive Phone',
        price: 2000
      }),
      createTestProduct({
        category: 'clothing',
        name: 'T-Shirt',
        price: 500
      }),
      createTestProduct({
        category: 'electronics',
        name: 'Cheap Phone',
        price: 1000
      })
    ];
    const productList = new ProductList(products);
    
    // When
    const result = productList
      .filterByCategory('electronics')
      .sortByPrice('asc')
      .getItems();
    
    // Then
    expect(result).toHaveLength(2);
    expect(result[0].name).toBe('Cheap Phone');
    expect(result[1].name).toBe('Expensive Phone');
  });
});

describe('장바구니 할인 적용', () => {
  it('총액이 50000원 이상일 때 10% 할인이 적용된다', () => {
    // Given
    const cart = new Cart();
    const products = [
      createTestProduct({
        id: 1,
        name: 'Premium Phone',
        price: 40000
      }),
      createTestProduct({
        id: 2,
        name: 'Phone Case',
        price: 15000
      })
    ];
    products.forEach(product => cart.addItem(product));
    
    // When
    const summary = cart.applyDiscount();
    
    // Then
    expect(summary.originalPrice).toBe(55000);
    expect(summary.discountRate).toBe(0.1);
    expect(summary.finalPrice).toBe(49500);
    expect(summary.discountApplied).toBe(true);
  });
});

이런 방식의 장점:

  1. 테스트 시나리오가 명확하게 구분되어 가독성이 향상됨
  2. 테스트 실패 시 어느 단계에서 문제가 발생했는지 파악이 쉬움
  3. 테스트 코드가 문서화 역할을 함
  4. 새로운 개발자가 코드를 이해하기 쉬움
  5. 테스트 유지보수가 용이함

이렇게 Given-When-Then 패턴을 적용하면 테스트의 목적과 시나리오가 더욱 명확해지고, 테스트 코드 자체가 좋은 문서화 역할을 할 수 있습니다.

좋았던 부분

  1. 명확한 요구사항 명세
  • 체크리스트 형태의 구체적인 요구사항
  • 기본/심화 과제의 명확한 구분
  • 실제 프로젝트와 유사한 환경
  1. 실무적인 기술 스택
  • Vite를 통한 모던 개발 환경
  • TailwindCSS를 활용한 스타일링
  • MSW를 통한 API 모킹
  1. 컴포넌트 시스템 구현 경험
  • React 컴포넌트 라이프사이클 클론 구현
    • 마운트, 업데이트, 언마운트 등 핵심 라이프사이클 직접 구현
    • 상태 변화에 따른 리렌더링 메커니즘 이해
  • Virtual DOM 없이 컴포넌트 동작 원리 이해
    • 순수 JavaScript로 컴포넌트 상태 관리 구현
    • 템플릿 리터럴을 활용한 선언적 렌더링 구현
  • 실제 프레임워크의 작동 방식 깊이 이해
    • 컴포넌트 기반 설계의 장단점 체감
    • 상태 관리와 렌더링 최적화의 중요성 인식
class BaseComponent {
  setState(nextState) {
    const prevState = { ...this.state };
    this.state = { ...this.state, ...nextState };
    
    // 상태 변경 후 리렌더링 및 라이프사이클 메서드 호출
    this.reRender();
    this.componentDidUpdate(this.props, prevState, this.state);
  }

  reRender() {
    if (this.target) {
      // 보존할 요소들이 있는 경우 보존 로직 적용
      if (this.preserveSelectors.length > 0) {
        this.reRenderWithPreservation();
      } else {
        this.target.innerHTML = this.template();
      }
    }
  }
}

모호했던 부분

  1. 상태 관리 범위
  • 전역/지역 상태의 구분 기준
  • 상태 지속성 요구사항
  • 상태 동기화 전략
  1. 에러 처리 범위
  • 어떤 에러를 처리해야 하는지
  • 에러 UI 요구사항
  • 재시도 로직 구현 범위
  1. 테스트 코드의 방향성
  • AI 생성 테스트 코드의 한계
    • 테스트의 실제 목적과 의도 파악 어려움
    • 구현에 맞춘 테스트가 아닌, 테스트에 맞춘 구현의 위험성
    • 테스트 시나리오의 실제 가치 검증 필요성
  • 테스트 독립성의 중요성 깨달음
    • 단위 테스트 간 상호 의존성이 없어야 함
    • 테스트 실행 순서에 영향받지 않아야 함
    • 다른 테스트의 상태나 사이드 이펙트에 영향받지 않아야 함
  • 이상적인 테스트 방향성 정립
    • 테스트는 구현이 아닌 동작을 검증해야 함
    • 각 테스트는 독립적이고 자체적으로 완결성을 가져야 함
    • 테스트는 코드의 품질 향상을 위한 도구로 활용되어야 함

모호했던 부분 - 테스트 코드 예시

❌ 잘못된 테스트 예시 (의존성 있는 테스트)

describe('장바구니 테스트', () => {
  it('상품을 장바구니에 추가한다', () => {
    addToCart(product);
    expect(getCartItems()).toContain(product);
  });

  // 이전 테스트의 상태에 의존적인 테스트
  it('장바구니에서 상품을 제거한다', () => {
    removeFromCart(product.id);
    expect(getCartItems()).not.toContain(product);
  });
});

✅ 개선된 테스트 예시 (독립적인 테스트)

describe('장바구니 테스트', () => {
  // 각 테스트마다 독립적인 상태로 시작
  beforeEach(() => {
    clearCart();
  });

  it('상품을 장바구니에 추가한다', () => {
    const product = createTestProduct();
    addToCart(product);
    expect(getCartItems()).toContain(product);
  });

  it('장바구니에서 상품을 제거한다', () => {
    const product = createTestProduct();
    addToCart(product);  // 테스트에 필요한 상태를 직접 설정
    removeFromCart(product.id);
    expect(getCartItems()).not.toContain(product);
  });

  it('장바구니 상품 수량을 변경한다', () => {
    const product = createTestProduct();
    addToCart(product);
    updateQuantity(product.id, 3);
    const cartItem = getCartItem(product.id);
    expect(cartItem.quantity).toBe(3);
  });
});

❌ 구현에 의존적인 테스트:

describe('상품 필터링', () => {
  it('카테고리로 상품을 필터링한다', () => {
    const products = filterProducts('electronics');
    // 구현 세부사항에 의존적인 테스트
    expect(products.every(p => p._category === 'electronics')).toBe(true);
  });
});

✅ 행위 중심의 테스트:

describe('상품 필터링', () => {
  it('전자제품 카테고리 선택 시 전자제품만 표시된다', () => {
    const products = [
      createTestProduct({ category: 'electronics', name: 'Phone' }),
      createTestProduct({ category: 'clothing', name: 'Shirt' })
    ];
    
    const filtered = filterProducts(products, 'electronics');
    
    expect(filtered).toHaveLength(1);
    expect(filtered[0].name).toBe('Phone');
  });

  it('존재하지 않는 카테고리 선택 시 빈 목록을 반환한다', () => {
    const products = [
      createTestProduct({ category: 'electronics', name: 'Phone' })
    ];
    
    const filtered = filterProducts(products, 'non-existent');
    
    expect(filtered).toHaveLength(0);
  });
});

AI를 활용한 방법(바이브코딩)

컴포넌트 라이프사이클 구현

바닐라 자바스크립트로 리액트 클래스컴포넌트의 라이프사이클을 구현하는것이 가장 큰 재미었습니다. 이전에 사용햇었던 클래스 컴포넌트 라이프사이클 메서드들을 머릿속에서 다시 뇌새김하며 AI에게 componentDidMount componentDidUpdate componentDidUnmount 등 프로시저를 작성해달라하며, 부수효과가 일어났을때 언제 메서드들이 호출되어야하는지를 치열하게 토론을 해가며 작성했습니다.

전역상태관리와 바이브코딩 경험

라이프사이클도 재밌었지만, 리덕스처럼 과하지는 않고 심플한 pub/sub 패턴을 활용한 전역스토어를 만들어보자고했습니다. 옵저버함수를 만들어 컴포넌트레벨에서 구독이 가능하게했으며 로컬상태와 전역상태를 동기화시키는 작업을 할수있도록 설계했었습니다.

더 나아가, 이런 상태 스토어는 외부 구현체와도 함께 사용이 가능해야하기떄문에 로컬스토리 인터페이스와 인덱스드디비 인터페이스도 함꼐 만들어달라고했었습니다. 재밌던점은 커서가 디버깅이 어려울테니 로깅 미들웨어도 만들어줄테니 이것도 사용해보라하며 함께 던져준점이 가장 흥미로웠습니다. 실제로 제 배포된 링크를 보면 로깅 미들웨어가 함꼐 동작하고있습니다 :)

커밋 자동화

제 커밋을 천천히 살펴보시면 한글로된것과 영어로된것이 구분되어있는데 영어로되어있는것은 실제 과제를 구현해나가며 AI에게 논리적인 단위로 커밋과 푸시까지 요청했던것입니다. 배포시점부터는 코드레벨에 갇혀있는 AI가 컨트롤 할수없기에 제가 직접 관여를 했습니다.

부끄럽지만 제 커서 커밋 잡부 프롬프트를 공유하겠습니다..

프롬프트


## ✅ Pull Request 잡부 Rule

### 1. PR 제목 규칙

**형식:**

[scope] [Jira Ticket ID] Summary of changes


**설명:**

* `scope`: 변경된 기능, 모듈, 또는 디렉토리 이름. 예: `auth`, `ui`, `api`, `core`
* `Jira Ticket ID`: 이슈 트래킹 ID. 예: `PO-123`. 없는 경우 생략 가능하나 가급적 작성
* `Summary`: 명령문 형태로 작성하고 마침표를 붙이지 않음

**예시:**

[auth] [PO-123] Add OAuth2 login flow [design-system] Update button component style


**검사 기준 (예시):**

* 제목이 `[]`로 시작하지 않으면 경고
* `scope`가 PascalCase면 경고 (→ 소문자 사용)
* 마침표가 끝에 붙으면 경고
* 길이가 50자 초과면 경고

---

### 2. PR 본문 템플릿 준수 여부

`PULL_REQUEST_TEMPLATE.md` 기준으로 아래 항목이 모두 존재해야 합니다:

* `## Summary`
* `## Changes`
* `## Test` (생략 가능하지만 있으면 좋음)
* `## Related Issues` (없으면 `N/A`)

**검사 기준 (예시):**

* `## Summary` 누락 → 경고
* `Summary`가 한 문장이 아니면 경고
* `Changes` 섹션에 리스트 (`-`) 형식이 없으면 경고
* `본문 전체가 영어가 아닐 경우` 경고 (언어 감지 사용 가능)
* `Resolves:` 또는 `Related to:`가 없는 경우 경고

---

### 3. 공통 스타일 가이드

* 영어로 작성합니다 (한글 혼용 금지)
* 명령형 문장 사용 (ex: `Add`, `Fix`, `Refactor`, `Remove` 등으로 시작)
* 줄바꿈, 리스트 마크다운, 구문 강조 등은 GitHub 가독성 고려
* 하나의 PR은 하나의 논리 단위 변경만 포함해야 합니다 (잡탕 PR 금지)

---

### 4. 예시 PR 본문

Summary

Add social login using OAuth2 for Google and Kakao

Changes

  • Add login buttons for Google and Kakao
  • Implement OAuth2 redirect and callback handling
  • Issue JWT token from backend after successful login
  • Update user context with authenticated session

Test

  • Tested login flow manually with both providers
  • Unit tests added for auth utility functions

Resolves: PO-123

리뷰 받고 싶은 내용

처음에는 단순히 BaseComponent를 만들고 상속으로 쭉 내려가면 될 줄 알았습니다.

class BaseComponent {
  setState() { /* ... */ }
  render() { /* ... */ }
}

class ProductList extends BaseComponent {
  render() { /* ... */ }
}

class ProductDetail extends BaseComponent {
  render() { /* ... */ }
}

그런데 직접 개발을 해보니, 비슷한 기능을 수행해야하는 컴포넌트가 존재할때 상속으로만 해결하려다보니 문제가 발생했습니다.

// 이런 식으로 중복이 발생...
class ProductList extends BaseComponent {
  handleSearch() { /* ... */ }
}

class CategoryList extends BaseComponent {
  handleSearch() { /* ... */ } // 비슷한 코드 중복
}

그래서 합성을 해볼까했는데 현재 코드에서는 도저히 합성이 불가능하더라고요. 가령 High Order Function / Component로 풀어보려했는데 쉽지않았습니다.

이럴때에는 어떻게 합성을 해야할지 궁금합니다. 이래서 meta가 클래스컴포넌트를 포기하고 함수 컴포넌트로 넘어가게 된것이 아닐까 싶더라고요

과제 피드백

안녕하세요 준형님, 수고하셨습니다. 이번 과제는 프레임워크 없이 SPA를 구현하면서 React와 같은 프레임워크가 해결하는 문제들을 직접 경험해보는 것이 목표였어요.

코드의 구현이 초창기 React의 클래스 컴포넌트의 모습을 보는 것 같아서 인상깊었습니다. 컴포넌트 베이스를 바닐라 자바스크립트로 구현하고, Pub-Sub 패턴 기반의 전역 상태 관리 시스템까지 만들어내셨네요. 특히 메모리 누수 방지를 위한 이벤트 핸들러 관리와 미들웨어 시스템 구현이 인상적입니다.

AI와의 바이브코딩 경험도 흥미롭게 읽었습니다. 단순히 코드를 생성하는 것이 아니라 아키텍처 설계부터 디버깅 미들웨어까지 함께 고민하며 개발하신 과정이 잘 드러나네요.

너무 너무 잘하셨습니다. :)


Q) 상속 대신 합성을 어떻게 활용할 수 있을까요?

=> 전통적으로 객체에서 합성을 한다는 건 아래처럼 공통 기능을 만든다음에 this를 넘겨주는 것입니다. this는 baseComponent의 공통 메소드를 가지고 있으니 로직을 분리할수가 있죠.

// 공통 기능을 별도 클래스로 분리 class SearchHandler { constructor(component) { this.component = component; // this를 통째로 받음 }

handleSearch(query) { this.component.setState({ search: query }); this.component.loadProducts(); // component의 메서드 호출 } }


그렇지만 이런 방식으로 만들면 재사용을 하기 위해 분리를 했는데 Search기능을 활용하기 위한 method들을 각자 만들어줘야 하는 문제가 발생합니다. 내부 구조를 모른채로 재사용을 하고 싶지만 객체지향은 내부 구조의 약속(=인터페이스)를 기준으로 소통하도록 되어 있으니까요.

이러한 이유가 클래스 컴포넌트를 포기하고 hook으로 넘어간 이유이죠. 합성 자체가 잘못된 방식은 아니구요. 명시적으로 좋은 구조를 유지할 수 있다는 장점과 개발자 경험이 더 복잡해진다는 단점을 포기하고 FE 특성상 견고한 구조보다는 단순하고 수정하기 좋은 코드를 만들기 위한 선택이라고 생각해주세요.

수고하셨습니다. 2주차도 화이팅입니다! :)