👩‍💻
Megaptera Frontend
  • 주차별 학습
    • megaptera-front
    • 1. 개발 환경
      • 1. 개발 환경
      • 2. TypeScript
      • 3. React
      • 4. Testing Library
      • 5. Parcel & ESLint
      • 한 주를 마치며
    • 2. JSX
      • 1. JSX
      • 한 주를 마치며
    • 3. React로 사고하기
      • 1. React Component
      • 2. React State
      • 한 주를 마치며
    • 4. React Hooks
      • 1. Express
      • 2. Fetch API & CORS
      • 3. React의 Hook
      • 4. useRef & Custom Hook
      • 5. usehooks-ts
      • 한 주를 마치며
    • 5. 테스트
      • 1. TDD
      • 2. React Testing Library
      • 3. MSW
      • 4. Playwright
      • 한 주를 마치며
    • 6. External Store
      • 1. External Store
      • 2. TSyringe
      • 3. Redux 따라하기
      • 4. usestore-ts
      • 한 주를 마치며
    • 7. React Router
      • 1. Routing
      • 2. Routes
      • 3. Router
      • 4. Navigation
      • 한 주를 마치며
    • 8. CSS in JS
      • 1. Design System
      • 2. Style Basics
      • 3. CSS in JS
      • 4. styled-components
      • 5. props와 attrs
      • 6. Global Style & Theme
      • 한 주를 마치며
    • 9. 쇼핑몰 목록, 상품 페이지
      • 1. 개발하기 전 준비
      • 2. 목록 보기
      • 3. 상품 상세 보기
      • 4. 장바구니 보기
      • 5. 장바구니에 상품 담기
      • 한 주를 마치며
    • 10. 사용자 인증, 인가
      • 1. 로그인
      • 2. 로그아웃
      • 3. 회원가입
      • 4. 주문 목록 & 주문 상세
      • 한 주를 마치며
    • 11. 주문, 결제
      • 1. 배송 정보 입력
      • 2. 포트원 결제 요청
      • 3. 배송 및 결제 정보 전달
      • 한 주를 마치며
    • 12. 어드민
      • 1. 관리자 웹 사이트 개발 시작
      • 2. 로그인, 사용자 목록
      • 3. 카테고리 관리
      • 4. 주문 관리
      • 5. 상품 관리
      • 한 주를 마치며
Powered by GitBook
On this page
  • 1. 상품 목록
  • ProductListPage 구현
  • useFetchProducts hook 생성
  • Products 컴포넌트 구현
  • numberFormat 유틸리티 함수 구현
  • 상품 목록을 Store로 관리
  • useFetchProducts hook 변경
  • 2. 카테고리 목록
  • Header에 카테고리 목록 보여주기
  • useFetchCategories hook 생성
  • Store 구현
  • ApiService 파일 분리
  • 3. 카테고리별 상품 목록
  • useSearchParams
  • ProductListPage에서 categoryId 얻어오기
  • 카테고리 ID를 쓰도록 hook 변경
  • Store 변경
  • API Service 변경
  1. 주차별 학습
  2. 9. 쇼핑몰 목록, 상품 페이지

2. 목록 보기

Previous1. 개발하기 전 준비Next3. 상품 상세 보기

Last updated 2 years ago

1. 상품 목록

🎯 상품 목록을 얻어서 표시하는 화면을 만들기

  1. 상품 목록 얻기 - API 서버에서

  2. 상품 목록 보여주기 - React로

전자는 useFetchProducts 훅으로, 후자는 Products 컴포넌트로 구현하고, ProductListPage에서 이 둘을 조합

  • 단일 책임 원칙

  • 테스트 용이

ProductListPage 구현

// ProductListPage.tsx

import Products from '../components/product-list/Products';

import useFetchProducts from '../hooks/useFetchProducts';

export default function ProductListPage() {
    // Todo 1. 상품 목록 얻기
    const { products } = useFetchProducts();

    // Todo 2. 화면에 보여주기
    return (
        <div>
            <h2>Products</h2>
            <Products products={products} />
        </div>
    );
}

useFetchProducts hook 생성

// hooks/useFetchProducts.ts

const apiBaseUrl = 'https://shop-demo-api-01.fly.dev';

export default function useFetchProducts() {
    type Data = { 
        products: ProductSummary[]; 
    };

    const { data } = useFetch<Data>(`${apiBaseUrl}/products`);

    return {
        products: data?.products ?? [], 
        loading: !data, 
        error,
  };
}
  • BaseUrl은 추후 환경 변수로 분리 : 재활용과 유지보수가 쉬워짐

  • 필요한 경우 loading과 error를 같이 내보낼 수 있음

Products 컴포넌트 구현

Product 컴포넌트 분리

type ProductsProps = {
  products: ProductSummary[];
}

export default function Products({ products }: ProductsProps) {
  if (!products.length) {
    return null;
  }

  return (
    <Container>
      <ul>
        {products.map((product) => (
          <li key={product.id}>
            <Link to={`/products/${product.id}`}>
              <Product product={product} />
            </Link>
          </li>
        ))}
      </ul>
    </Container>
  );
}

numberFormat 유틸리티 함수 구현

언어에 맞는 숫자 서식을 지원하는 객체의 생성자

숫자를 읽기 좋게 보여주도록 numberFormat 유틸리티 함수를 준비 간단한 유틸 함수이기 때문에 테스트도 같이 작성

export default function numberFormat(value: number) {
  return new Intl.NumberFormat().format(value);
}

상품 목록을 Store로 관리

// stores/ProductsStore.ts

const apiBaseUrl = '...';
    
@singleton()
@Store()
export default class ProductsStore {
  products: ProductSummary[] = [];

  async fetchProducts() {
    this.setProducts([]);

    const { data } = await axios.get(`${apiBaseUrl}/products`);
    const { products } = data;

    this.setProducts(products);
  }

  @Action()
  setProducts(products: ProductSummary[]) {
    this.products = products;
  }
}
  • reflect-metadata import

  • tsconfig.json 파일에 decorator 설정 주석 해제

useFetchProducts hook 변경

export default function useFetchProducts(): {
  products: ProductSummary[];
} {
  const store = container.resolve(ProductsStore);

  const [{ products }] = useStore(store);

  useEffectOnce(() => {
    store.fetchProducts();
  });	

  return { products };
}

2. 카테고리 목록

🎯 헤더에서 카테고리 목록을 표시하기

Header에 카테고리 목록 보여주기

export default function Header() {
  const { categories } = useFetchCategories();

  return (
    <Container>
      <h1>Shop</h1>
      <nav>
        <ul>
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/products">Products</Link>
            {!!categories.length && (
              <ul>
                {categories.map((category) => (
                  <li key={category.id}>
                    <Link to={`/products?categoryId=${category.id}`}>
                      {category.name}
                    </Link>
                  </li>
                ))}
              </ul>
            )}
          </li>
          <li>
            <Link to="/cart">Cart</Link>
          </li>
        </ul>
      </nav>
    </Container>
  );
}

useFetchCategories hook 생성

hooks/useFetchCategories.ts useFetchProducts 복사해서 수정하면 편리

Store 구현

// stores/CategoriesStore.ts

@singleton()
@Store()
export default class CategoriesStore {
  categories: Category[] = [];

  async fetchCategories() {
    this.setCategories([]);

    const categories = await apiService.fetchCategories();

    this.setCategories(categories);
  }

  @Action()
  setCategories(categories: Category[]) {
    this.categories = categories;
  }
}

ApiService 파일 분리

services 폴더에 API 호출을 모아주는 ApiService 파일을 생성 API의 base URL을 지정하기 위해 환경변수를 활용

const API_BASE_URL = process.env.API_BASE_URL || 'https://...';

export default class ApiService {
  private instance = axios.create({
    baseURL: API_BASE_URL,
  });

  async fetchCategories(): Promise<Category[]> {
    const { data } = await this.instance.get('/categories');
    const { categories } = data;
    return categories;
  }

  async fetchProducts(): Promise<ProductSummary[]> {
    const { data } = await this.instance.get('/products');
    const { products } = data;
    return products;
  }
}

export const apiService = new ApiService();
  • ProductsStore 에서도 코드 수정

3. 카테고리별 상품 목록

🎯 카테고리 클릭 시 해당 카테고리 상품 보여주기

처음부터 고민해서 바로 만들어도 되고, 일단 만들고 고쳐나가도 됨 테스트 코드를 작성하면서 하면 오류를 줄일 수 있음

useSearchParams

현재 위치에 대한 URL의 쿼리 문자열을 읽고 수정하는 데 사용 React의 useState와 비슷하게, 현재 위치의 검색 매개변수와 이를 업데이트하는 데 사용하는 함수의 배열을 반환

// 참고 
window.location.search

ProductListPage에서 categoryId 얻어오기

export default function ProductListPage() {
  const [params] = useSearchParams();

  const categoryId = params.get('categoryId') ?? undefined;

  const { products } = useFetchProducts({ categoryId });

  return (
    <div>
      <h2>Products</h2>
      <Products products={products} />
    </div>
  );
}
  • Id가 없으면 null을 반환하는데, 여기서는 일부러 undefined 사용(있을 수도, 없을 수도 있음)

카테고리 ID를 쓰도록 hook 변경

export default function useFetchProducts({ categoryId }: {
  categoryId: string;
}): {
  products: ProductSummary[];
} {
  const store = container.resolve(ProductsStore);

  const [{ products }] = useStore(store);

  useEffect(() => {
    store.fetchProducts({ categoryId });
  }, [store, categoryId]);

  return { products };
}
  • 카테고리 ID가 바뀔 때마다 리렌더되야 하므로 useEffect를 사용

  • useFetchCategories 도 useEffect를 사용하고 의존성 배열에 store 추가

Store 변경

async fetchProducts({ categoryId }: {
  categoryId?: string;
}) {
  this.setProducts([]);

  const products = await apiService.fetchProducts({ categoryId });

  this.setProducts(products);
}

API Service 변경

export default class ApiService {
    async fetchProducts({categoryId}: { 
        categoryId?: string; 
    } = {}): Promise<ProductSummary[]> {
        const {data} = await this.instance.get('/products', {
            params: {categoryId},
        });
        const {products} = data;
        return products;
    }
}
  • axios는 params를 넘겨줄 수 있음

🌎 각 언어에 맞는 문자비교, 숫자, 시간, 날짜비교를 제공하는, ECMAScript 국제화 API를 위한 네임 스페이스 원하는대로 커스텀이 가능

Intl.NumberFormat
Intl
🔗 참고 자료
🔗 실습 링크 : useFetchCategories hook 및 CategoriesStore 구현, ApiService 분리
🔗 실습 링크 : 카테고리별 상품 목록 보기 구현
🔗 실습 링크 : codecept 설치 및 routes, product list 테스트
React Router - useSearchParams
🔗 실습 링크 : useFetchProducts hook 구현
🔗 실습 링크 : Products UI 구현
🔗 실습 링크 : ProductsStore 적용