본문 바로가기
인턴IN메타/응용SW개발

[인턴IN메타] React 실전 프로젝트: 요구사항 분석부터 무한 스크롤, 다크 모드 구현까지

by me_in_sk 2025. 7. 19.
반응형

이번 '인턴IN메타' 2주차에서는 React를 기반으로 실제 고객사의 요구사항이 담긴 상품 목록 페이지를 개발하는 과제를 수행했습니다. 단순한 기능 구현을 넘어, 사용자 경험(UX)과 코드의 안정성까지 고려하는 실전적인 프로젝트였습니다.

본 포스팅에서는 주어진 요구사항을 어떻게 분석하고 기술적으로 해결했는지, 나아가 Intersection Observer를 활용한 무한 스크롤Context API를 이용한 다크/라이트 모드 전환 등 추가 기능을 왜, 그리고 어떻게 구현했는지 상세히 다뤄보겠습니다. 마지막으로 React Testing Library를 활용한 테스트와 GitHub Pages 배포, 그리고 프로젝트를 통해 얻은 교훈을 담은 KPT 회고까지 공유합니다.

1. 프로젝트의 시작: 고객 요구사항 분석 및 개발 전략

모든 프로젝트의 시작은 고객의 목소리를 듣는 것에서부터 출발합니다. 이번 프로젝트의 요구사항은 명확했습니다.

  • 모바일 환경 최적화: 젊은 세대를 고려한 모바일 우선 설계.
  • 상품 사진 강조: 디자인 시안처럼 신발 사진을 명확하고 크게 표시.
  • 실시간 장바구니: 페이지 새로고침 없는 실시간 장바구니 개수 업데이트.
  • 결과물 확인: 배포된 테스트 URL 제공.

고객사 요구사항 문서화

고객 요구사항 문서화
이번 프로젝트에서 해결해야 할 핵심 요구사항 목록

 

이 요구사항들을 해결하기 위해 다음과 같은 기술 스택과 전략을 수립했습니다.

  • UI 구현: React와 styled-components를 사용하여 컴포넌트 기반의 재사용 가능한 UI를 구축하고, 동적 스타일링을 구현합니다.
  • 전역 상태 관리: 여러 컴포넌트에서 공유되어야 하는 '장바구니'와 '테마' 상태는 Context API를 활용하여 관리의 효율성을 높입니다.
  • 사용자 경험 개선: 무한 스크롤은 Intersection Observer API를 활용하여 효율적으로 구현합니다.

 

2. 핵심 기능 구현 ①: Context API를 활용한 전역 상태 관리

'실시간 장바구니'와 '다크 모드' 기능의 핵심은 어디서든 동일한 상태에 접근하고, 변경을 전파하는 것입니다. props drilling (속성 파고들기) 문제를 피하고 깔끔한 코드 구조를 유지하기 위해 Context API를 도입했습니다.

 

다크/라이트 모드 테마 관리

먼저 사용자가 선택한 테마(light 또는 dark)를 전역적으로 관리하기 위한 ThemeContext를 생성했습니다.

 
// ThemeContext.js
import React, { createContext, useState, useContext } from "react";

const ThemeContext = createContext();

export const useTheme = () => useContext(ThemeContext);

export const CustomThemeProvider = ({ children }) => {
  const [themeMode, setThemeMode] = useState("light");

  const toggleTheme = () => {
    setThemeMode((prevMode) => (prevMode === "light" ? "dark" : "light"));
  };

  const value = { themeMode, toggleTheme };

  return (
    <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
  );
};

 

이렇게 생성된 toggleTheme 함수는 Header 컴포넌트의 아이콘 버튼에 연결되고, themeMode 상태는 styled-components의 ThemeProvider와 결합하여 앱 전체의 스타일에 동적으로 반영됩니다.

 

라이트 모드와 다크 모드 화면 비교

라이트 모드와 다크 모드 화면 비교 이미지
Context API와 styled-components를 연동하여 구현한 테마 전환 기능

 

실시간 장바구니 관리

장바구니 기능 역시 동일한 구조입니다. CartContext를 만들어 장바구니에 담긴 상품 배열(cartItems)과 상품을 추가하는 함수(addToCart)를 전역적으로 제공합니다.

ProductCard 컴포넌트의 '담기' 버튼은 addToCart 함수를 호출하여 상품을 장바구니에 추가하고, Header 컴포넌트는 cartItems 배열의 길이를 실시간으로 감지하여 뱃지에 수량을 표시합니다. 이 모든 과정은 페이지 새로고침 없이 즉각적으로 일어납니다.

 

3. 핵심 기능 구현 ②: Intersection Observer로 완성한 무한 스크롤

사용자가 상품을 탐색할 때 '다음' 버튼을 계속 누르게 하는 것은 좋은 경험이 아닙니다. 끊김 없는 탐색 경험을 제공하기 위해, 스크롤이 페이지 하단에 닿았을 때 다음 상품 목록을 자동으로 불러오는 무한 스크롤 기능을 구현했습니다.

핵심은 Intersection Observer API입니다. 이 API는 특정 요소(Element)가 뷰포트(Viewport)에 들어오거나 나가는 것을 비동기적으로 감지합니다.

 
// ProductListPage.js 일부
// ...
const observerRef = useRef();

const loadMoreProducts = useCallback(async () => {
  if (isLoading) return;
  setIsLoading(true);
  const { products: newProducts, hasMore: newHasMore } = await fetchProducts(page);
  setProducts((prev) => [...prev, ...newProducts]);
  setHasMore(newHasMore);
  setPage((prev) => prev + 1);
  setIsLoading(false);
}, [isLoading, page]);

useEffect(() => {
  const currentObserverRef = observerRef.current;
  const observer = new IntersectionObserver(
    (entries) => {
      if (entries[0].isIntersecting && hasMore) {
        loadMoreProducts();
      }
    },
    { threshold: 1.0 }
  );

  if (currentObserverRef) {
    observer.observe(currentObserverRef);
  }

  return () => {
    if (currentObserverRef) {
      observer.unobserve(currentObserverRef);
    }
  };
}, [hasMore, loadMoreProducts]);

상품 목록 맨 마지막에 보이지 않는 <div>를 두고 observerRef로 참조합니다. 이 div가 화면에 보이면(isIntersecting), loadMoreProducts 함수를 호출하여 다음 페이지의 데이터를 불러와 기존 products 상태에 추가합니다. useCallback을 사용해 함수가 불필요하게 재생성되는 것을 방지하여 최적화도 챙겼습니다.

 

스크롤을 내릴 때 새로운 상품이 로딩

스크롤을 내릴 때 새로운 상품이 로딩 되는 이미지
Intersection Observer API를 활용하여 부드럽게 동작하는 무한 스크롤

 

4. 코드의 완성도: 테스트와 배포

"동작하는 코드"를 넘어 "신뢰할 수 있는 코드"를 만드는 것은 프로의 기본입니다. Jest와 React Testing Library를 사용하여 주요 기능에 대한 테스트 코드를 작성했습니다.

특히 ProductCard와 같은 핵심 컴포넌트는 독립적으로 정상 동작하는지 검증하는 단위 테스트가 필수입니다.

 

테스트 코드 작성

// ProductCard.test.js
// ...
describe("ProductCard", () => {
  const mockProduct = {
    id: 1,
    brand: "테스트 브랜드",
    description: "이것은 테스트용 신발입니다.",
    price: 50000,
    image: "test-image.jpg",
  };

  test("상품 정보(브랜드, 설명, 가격)가 올바르게 렌더링된다.", () => {
    renderWithProviders(<ProductCard product={mockProduct} />);

    expect(screen.getByText("테스트 브랜드")).toBeInTheDocument();
    expect(screen.getByText("이것은 테스트용 신발입니다.")).toBeInTheDocument();
    expect(screen.getByText("50,000원")).toBeInTheDocument();
  });

  test("담기 버튼을 클릭하면 addToCart 함수가 호출된다.", async () => {
    renderWithProviders(<ProductCard product={mockProduct} />);
    const addButton = screen.getByRole("button", { name: "담기" });
    await userEvent.click(addButton);
    expect(mockAddToCart).toHaveBeenCalledTimes(1);
  });
});

위 코드는 ProductCard에 상품 데이터가 주어졌을 때, 브랜드, 설명, 가격 정보가 화면에 올바르게 표시되는지와 '담기' 버튼을 클릭했을 때 addToCart 함수가 정상적으로 호출되는지를 검증합니다. 이런 테스트들은 기능 변경이나 리팩토링 시 발생할 수 있는 잠재적 버그를 사전에 방지하는 안전망 역할을 합니다.

테스트를 마친 결과물은 GitHub Pages를 통해 배포하여 고객사가 직접 확인할 수 있도록 테스트 URL을 전달했습니다.

 

Jest 테스트가 성공적으로 통과

Jest 테스트가 성공적으로 통과된 터미널 화면
간단한 단위/통합 테스트 통과로 코드의 안정성을 확보

 

5. 프로젝트를 통한 성장: KPT 회고

이번 프로젝트를 통해 배운 점을 KPT(Keep, Problem, Try) 형식으로 회고하며 마무리하고자 합니다.

  • Keep (유지할 점 / 긍정적 경험)
    • 선제적인 기능 구현: 요구사항에 명시된 기능뿐만 아니라, 사용자 경험에 큰 영향을 미치는 무한 스크롤 같은 기능을 스스로 고민하고 추가로 구현한 점은 매우 긍정적인 경험이었습니다.
    • Context API의 성공적 적용: 전역 상태 관리를 Context API로 해결하며 컴포넌트 간의 결합도를 낮추고 코드 구조를 깔끔하게 유지할 수 있었습니다.
  • Problem (개선할 점)
    • useEffect 의존성 관리: Intersection Observer를 useEffect와 함께 사용하면서, 초기에는 의존성 배열 관리에 미숙하여 불필요한 재실행이 일어나는 문제를 겪었습니다. useCallback으로 함수를 메모이제이션하고 의존성을 명확히 하여 해결했지만, React 훅의 동작 원리에 대한 더 깊은 이해가 필요함을 느꼈습니다.
  • Try (다음에 시도할 점)
    • 고도화된 상태 관리: 이번 프로젝트에서는 Context API만으로 충분했지만, 애플리케이션 규모가 더 커진다면 Redux Toolkit이나 Zustand 같은 전문 상태 관리 라이브러리를 도입하여 더욱 체계적으로 상태를 관리해보고 싶습니다.
    • 성능 최적화: React.memo 등을 활용한 렌더링 최적화를 적용하여 대규모 데이터 상황에서도 빠른 성능을 유지하는 방법을 더 깊게 학습하고 적용해 볼 계획입니다.

결론: 요구사항 너머를 보는 개발자

이번 2주차 프로젝트는 단순히 주어진 과제를 수행하는 것을 넘어, 실제 사용자 입장에서 더 나은 경험을 고민하고, 코드의 품질과 안정성까지 책임지는 경험이었다는 점에서 의미가 깊습니다. 고객의 요구사항을 완벽히 소화하는 것은 기본이며, 그 너머의 가치를 제안하고 만들어낼 수 있을 때 비로소 한 단계 더 성장할 수 있음을 깨달았습니다. 이번 포스팅은 이번 주차에 작성한 주간 회고 기록을 남기며 글을 마칩니다.

주간 회고 기록
2주차 주간 회고 기록

 

여러분의 React 프로젝트 경험은 어떠셨나요? 댓글로 자유롭게 공유해주세요.

 

 


◆ 관련 링크

 

인턴IN메타

인턴IN메타(인턴인메타)는 인턴체험서비스를 제공하는 메타버스 직업체험관입니다.

www.interninmeta.or.kr

인턴IN메타

반응형

댓글