과제 체크포인트
배포 링크
기본과제
상품목록
상품 목록 로딩
- 페이지 접속 시 로딩 상태가 표시된다
- 데이터 로드 완료 후 상품 목록이 렌더링된다
- 로딩 실패 시 에러 상태가 표시된다
- 에러 발생 시 재시도 버튼이 제공된다
상품 목록 조회
- 각 상품의 기본 정보(이미지, 상품명, 가격)가 카드 형태로 표시된다
- 한 페이지에 보여질 상품 수 선택
- 드롭다운에서 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);
}
}
- 미들웨어 시스템: 상태 변경의 전후 처리를 위한 미들웨어 구현
- 디버깅 지원: 상태 변경 로깅을 위한 미들웨어 추가
- 메모리 관리: 구독 해제 기능 구현
주요 기술적 도전과 해결
-
컴포넌트 재사용성
- 문제: 컴포넌트 간 의존성이 높아지면서 재사용이 어려워짐
- 해결: Props를 통한 의존성 주입과 이벤트 버스 패턴 도입
-
상태 동기화
- 문제: 여러 컴포넌트에서 동일한 상태 참조 시 동기화 문제
- 해결: 중앙 집중식 Store와 구독 시스템으로 해결
-
라우팅과 컴포넌트 생명주기
- 문제: SPA 라우팅 시 컴포넌트 정리가 제대로 되지 않는 문제
- 해결: 명확한 생명주기 정의와 자동 정리 시스템 구현
개선이 필요하다고 생각하는 코드
- 상태 관리 개선점
- 상태 변경 히스토리 추적 기능 추가
- 상태 롤백 기능 구현
- 디버깅을 위한 로깅 시스템 강화
- 라우터 개선점
- 중첩 라우팅 지원
- 라우트 가드 구현
- 라우트 트랜지션 효과 추가
- 컴포넌트 시스템
- 더 세밀한 생명주기 메서드 추가
- 성능 최적화를 위한 메모이제이션
- 이벤트 시스템 개선
학습 효과 분석
가장 큰 배움
- 상태 관리 시스템 설계
- Pub-Sub 패턴의 실제 구현 경험
- 미들웨어 시스템의 유용성 이해
- 상태 지속성 관리의 중요성
- 컴포넌트 기반 설계
- 재사용성과 확장성을 고려한 설계
- 생명주기 관리의 중요성
- 상태와 뷰의 분리
- 라우팅 시스템 구현
- SPA에서의 라우팅 처리 방법
- History API 활용
- 동적 라우트 처리
- 의미 있는 테스트 코드 작성
- 테스트는 순수 함수처럼 독립적이어야 함을 깨달음
- 테스트 간의 의존성이나 순서 의존성이 있으면 안됨
- 각 테스트는 자체적으로 완결성을 가져야 함
- 테스트와 구현 코드 간의 강한 커플링 방지의 중요성
- 구현 변경이 테스트 코드 전체를 무너뜨리면 안됨
- 테스트는 행위(behavior)를 검증해야지, 구현(implementation)을 검증하면 안됨
- 실제 사용자 시나리오에 기반한 테스트의 가치
- 장바구니 추가/삭제/수정 등 실제 사용자 행동 기반 테스트
- 카테고리 필터링, 검색 등 실제 사용 패턴 검증
- 테스트 가능한 코드 설계의 중요성
- 순수 함수 분리를 통한 테스트 용이성 확보
- 의존성 주입을 통한 테스트 격리성 확보
- 사이드 이펙트 분리를 통한 예측 가능성 향상
추가 학습 필요 영역
- 테스트 자동화
- E2E 테스트 강화
- 단위 테스트 커버리지 향상
- 테스트 시나리오 다양화
- 성능 최적화
- 컴포넌트 세분화를 통한 렌더링 최적화
- 현재: 페이지, 헤더, 푸터 단위의 큰 컴포넌트 구조
- 개선 방향: 더 작은 단위의 독립적 컴포넌트로 분리
- 상품 카드, 카테고리 필터, 검색바 등을 독립 컴포넌트화
- 상태 변경 시 필요한 부분만 리렌더링 가능하도록 구조 개선
- 라이브러리와 웹앱 개발의 균형
- 현재는 라이브러리 코어와 웹앱 구현이 혼재된 상태
- 컴포넌트 세분화는 두 영역 모두에서 이점 제공
- 라이브러리: 더 유연한 조합과 재사용성 확보
- 웹앱: 렌더링 성능 최적화와 상태 관리 효율화
- 결론: 컴포넌트 세분화는 양쪽 모두에게 필요한 개선 방향
- 번들 사이즈 최적화
- 캐싱 전략 수립
과제 피드백
개선하면 좋을 것 같은 부분
- 테스트 가독성 향상 - 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);
});
});
이런 방식의 장점:
- 테스트 시나리오가 명확하게 구분되어 가독성이 향상됨
- 테스트 실패 시 어느 단계에서 문제가 발생했는지 파악이 쉬움
- 테스트 코드가 문서화 역할을 함
- 새로운 개발자가 코드를 이해하기 쉬움
- 테스트 유지보수가 용이함
이렇게 Given-When-Then 패턴을 적용하면 테스트의 목적과 시나리오가 더욱 명확해지고, 테스트 코드 자체가 좋은 문서화 역할을 할 수 있습니다.
좋았던 부분
- 명확한 요구사항 명세
- 체크리스트 형태의 구체적인 요구사항
- 기본/심화 과제의 명확한 구분
- 실제 프로젝트와 유사한 환경
- 실무적인 기술 스택
- Vite를 통한 모던 개발 환경
- TailwindCSS를 활용한 스타일링
- MSW를 통한 API 모킹
- 컴포넌트 시스템 구현 경험
- 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();
}
}
}
}
모호했던 부분
- 상태 관리 범위
- 전역/지역 상태의 구분 기준
- 상태 지속성 요구사항
- 상태 동기화 전략
- 에러 처리 범위
- 어떤 에러를 처리해야 하는지
- 에러 UI 요구사항
- 재시도 로직 구현 범위
- 테스트 코드의 방향성
- 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
Related Issues
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가 클래스컴포넌트를 포기하고 함수 컴포넌트로 넘어가게 된것이 아닐까 싶더라고요