Skip to content

[2단계 - 상세 정보 & UI/UX 개선하기] 이스타 미션 제출합니다.#305

Open
Eastar-DS wants to merge 102 commits intowoowacourse:eastar-dsfrom
Eastar-DS:step2
Open

[2단계 - 상세 정보 & UI/UX 개선하기] 이스타 미션 제출합니다.#305
Eastar-DS wants to merge 102 commits intowoowacourse:eastar-dsfrom
Eastar-DS:step2

Conversation

@Eastar-DS
Copy link
Copy Markdown

@Eastar-DS Eastar-DS commented Apr 13, 2026

🎯 미션 소개

  • API 연동을 통한 비동기 통신 학습
  • 사용자 시나리오 기반 E2E 테스트

🕵️ 셀프 리뷰(Self-Review)

제출 전 체크 리스트

  • 기능 요구 사항을 모두 구현했고, 정상적으로 동작하는지 확인했나요?
  • 기본적인 프로그래밍 요구 사항(코드 컨벤션, 에러 핸들링 등)을 준수했나요?
  • 테스트 코드는 모두 정상적으로 실행되나요? (필요 시, API Mocking 등)
  • (해당하는 경우) 배포한 데모 페이지에 정상적으로 접근할 수 있나요?

프로젝트 의존성 그래프

1. 레이어 개요

flowchart TB
    subgraph Composition["🎯 Composition Root"]
        main["main.ts"]
    end

    subgraph UI["🖼️ UI / View Layer"]
        direction LR
        SearchForm
        MovieListView
        HeroSection
        MovieDetailModal
        StarRating
        Notifier
        InfiniteScroll
    end

    subgraph Orchestration["🎛️ Orchestration"]
        MovieListController
    end

    subgraph Domain["📦 Domain / State"]
        direction LR
        MovieListStore
    end

    subgraph Ports["🔌 Ports (Interface)"]
        MovieRatingRepo["MovieRatingRepo (interface)"]
    end

    subgraph Adapters["⚡ Adapters / Infrastructure"]
        direction LR
        TmdbClient
        LocalStorageRatingRepo
        movieResponseMapper
    end

    subgraph Shared["🧱 Shared / Types"]
        direction LR
        types["types/movie"]
        DomainErrors
        constants
        AppShell["dom/AppShell"]
    end

    main --> UI
    main --> Orchestration
    main --> Domain
    main --> Adapters
    main --> AppShell

    Orchestration --> Domain
    Orchestration --> UI
    Orchestration --> Ports
    Orchestration --> Adapters

    Domain --> Adapters
    Adapters --> Shared
    UI --> Shared
    Ports -.implements.-> LocalStorageRatingRepo

    classDef root fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#000
    classDef ui fill:#dbeafe,stroke:#2563eb,color:#000
    classDef orch fill:#fce7f3,stroke:#db2777,color:#000
    classDef dom fill:#dcfce7,stroke:#16a34a,color:#000
    classDef port fill:#f3e8ff,stroke:#9333ea,stroke-dasharray:5 5,color:#000
    classDef adapter fill:#ffedd5,stroke:#ea580c,color:#000
    classDef shared fill:#f1f5f9,stroke:#64748b,color:#000

    class main root
    class SearchForm,MovieListView,HeroSection,MovieDetailModal,StarRating,Notifier,InfiniteScroll ui
    class MovieListController orch
    class MovieListStore dom
    class MovieRatingRepo port
    class TmdbClient,LocalStorageRatingRepo,movieResponseMapper adapter
    class types,DomainErrors,constants,AppShell shared
Loading

2. 파일 단위 상세 그래프

flowchart LR
    main["main.ts"]

    %% Composition에서 뿌려지는 모든 것
    main --> SearchForm
    main --> TmdbClient
    main --> MovieListStore
    main --> MovieListView
    main --> MovieListController
    main --> HeroSection
    main --> AppShell["dom/AppShell"]
    main --> Notifier
    main --> LocalStorageRatingRepo
    main --> MovieDetailModal
    main --> InfiniteScroll

    %% Controller 의존
    MovieListController --> MovieListStore
    MovieListController --> MovieListView
    MovieListController --> HeroSection
    MovieListController --> MovieDetailModal
    MovieListController --> TmdbClient
    MovieListController -. depends on interface .-> MovieRatingRepo

    %% Store
    MovieListStore --> TmdbClient

    %% View
    MovieListView --> movieListMarkup
    MovieListView --> types

    %% Hero
    HeroSection --> movieListMarkup
    HeroSection --> types

    %% Modal
    MovieDetailModal --> StarRating
    MovieDetailModal --> types

    %% Rating
    LocalStorageRatingRepo -. implements .-> MovieRatingRepo

    %% API
    TmdbClient --> movieResponseMapper
    TmdbClient --> apiTypes
    TmdbClient --> DomainErrors
    TmdbClient --> types
    movieResponseMapper --> apiTypes
    movieResponseMapper --> types
    movieResponseMapper --> DomainErrors
    apiTypes --> types

    %% Notifier
    Notifier --> errorToUserMessage
    errorToUserMessage --> DomainErrors

    %% AppShell
    AppShell --> selector["dom/selector"]

    %% Markup util
    movieListMarkup --> types
    movieListMarkup --> constants

    classDef root fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#000
    classDef ui fill:#dbeafe,stroke:#2563eb,color:#000
    classDef orch fill:#fce7f3,stroke:#db2777,color:#000
    classDef dom fill:#dcfce7,stroke:#16a34a,color:#000
    classDef port fill:#f3e8ff,stroke:#9333ea,stroke-dasharray:5 5,color:#000
    classDef adapter fill:#ffedd5,stroke:#ea580c,color:#000
    classDef shared fill:#f1f5f9,stroke:#64748b,color:#000

    class main root
    class SearchForm,MovieListView,HeroSection,MovieDetailModal,StarRating,Notifier,InfiniteScroll,movieListMarkup ui
    class MovieListController orch
    class MovieListStore dom
    class MovieRatingRepo port
    class TmdbClient,LocalStorageRatingRepo,movieResponseMapper,apiTypes adapter
    class types,DomainErrors,constants,AppShell,selector,errorToUserMessage shared
Loading

3. 별점 저장소의 "교체 가능한 구조" (Port & Adapter)

flowchart LR
    Controller["MovieListController"] -->|"의존 방향"| Interface["MovieRatingRepo<br/><i>interface</i>"]
    Interface -. implements .- Impl1["LocalStorageRatingRepo<br/>(현재)"]
    Interface -. implements .- Impl2["ServerRatingRepo<br/>(미래)"]

    main["main.ts<br/>(Composition Root)"] -.주입.-> Controller
    main -->|"new"| Impl1

    classDef interface fill:#f3e8ff,stroke:#9333ea,stroke-dasharray:5 5,color:#000
    classDef impl fill:#ffedd5,stroke:#ea580c,color:#000
    classDef other fill:#fef3c7,stroke:#d97706,color:#000

    class Interface interface
    class Impl1,Impl2 impl
    class main,Controller other
Loading

4. 에러 흐름 (Domain Error → User Message)

TMDB 응답 실패를 "사용자가 읽을 수 있는 토스트"로 바꾸는 경로.

flowchart LR
    fetch["fetch()"] --> TmdbClient
    TmdbClient -->|"throw"| Errors["DomainErrors<br/>NetworkError / ApiError /<br/>ApiParseError / ConfigError"]
    Errors --> Controller["MovieListController"]
    Controller -->|"notifier.error(e)"| Notifier
    Notifier --> Translator["errorToUserMessage"]
    Translator --> Toast["사용자 토스트"]

    classDef boundary fill:#ffedd5,stroke:#ea580c,color:#000
    classDef domain fill:#fee2e2,stroke:#dc2626,color:#000
    classDef ui fill:#dbeafe,stroke:#2563eb,color:#000

    class TmdbClient,fetch boundary
    class Errors domain
    class Controller,Notifier,Translator,Toast ui
Loading

5. 런타임 흐름 — 영화 상세 모달 열기 (시퀀스)

sequenceDiagram
    actor User
    participant View as MovieListView
    participant Ctrl as MovieListController
    participant Api as TmdbClient
    participant Repo as LocalStorageRatingRepo
    participant Modal as MovieDetailModal

    User->>View: 카드 클릭
    View->>Ctrl: onMovieClick(movieId)
    Ctrl->>Ctrl: token = ++_detailToken
    Ctrl->>Api: fetchMovieDetail(id)
    Api-->>Ctrl: MovieDetail
    Ctrl->>Ctrl: token 유효성 체크 (race guard)
    Ctrl->>Repo: getRating(id)
    Repo-->>Ctrl: number | null
    Ctrl->>Modal: open(detail, currentRating)
    Modal->>User: 모달 표시
Loading

리뷰 요청 & 논의하고 싶은 내용

1) 이번 단계에서 가장 많이 고민했던 문제와 해결 과정에서 배운 점

저번 미션 마지막부터 AI의 도움을 많이 받고있는데요, 저번에 느꼈던 기술부채를 줄이기위해 이번 미션과 관련된 html과 css를 집중하여 먼저 머리속에 틀을 잡고 step2 미션을 시작했습니다. 틀을 잡고 진행하니 AI가 제안해준 코드가 이해되기 시작해서 저번보단 훨씬 기분좋게 미션을 수행했습니다!

(1) 별점 저장소를 어떻게 추상화할 것인가

미션 설명에 "지금은 localStorage로 구현하지만 다음 배포에 서버 API로 교체할 예정" 이라는 제약이 명시되어 있어서, 구현체를 바꿀 때 Controller/Modal 같은 상위 레이어가 흔들리지 않는 구조를 먼저 고민해야한다고 AI를 통해 알게되었습니다. 이 과정에서 레포지토리 패턴이라는 용어를 처음 알게 되었고, 적용해보았습니다.

  • MovieRatingRepo 인터페이스를 먼저 정의하고 LocalStorageRatingRepo가 이를 구현하도록 분리했습니다.
  • 모달과 Controller는 인터페이스에만 의존시켜, 서버 교체 시 main.ts의 조립 한 줄(new LocalStorageRatingRepo() → new ServerRatingRepo(client))만 바꾸어 주면 되도록 구현했습니다.
  • 배운 점: "인터페이스를 먼저 설계하고 구현체를 끼운다"는 흐름이, 단순히 클래스를 쪼개는 것과 다르게 변경 지점이 한 곳으로 모이는 경험을 처음으로 체감했습니다. 마치 레고로 같은 모양의 구멍에 다른 블럭을 끼워 넣는 느낌을 받았습니다.

(2) 브레이크포인트를 어디에 둘 것인가

처음엔 520px / 1120px로 잡았는데, 태블릿에서 3열(3×200px = 600px)을 그리는 순간 그리드가 부모 폭을 넘는 문제가 생겼습니다.

  • Figma 시안은 모바일(390) / 태블릿(800) / 데스크톱(1120/1440)으로 나뉘어 있어서 800px / 1120px로 통일하고, mobile-first 베이스에 min-width 쿼리를 쌓아 올리는 방식으로 바꿨습니다.
  • clamp()와 min()으로 폰트/컨테이너를 유동화하고, 모달은 모바일에서 bottom-sheet(align-items: flex-end), 데스크톱에서 중앙(center)으로 전환했습니다.

배운 점: 브레이크포인트는 "그 지점에서 레이아웃이 실제로 깨지는가" 를 먼저 고려해야 한다는 것. 숫자를 먼저 박지 않고 콘텐츠(카드 크기 × 열 개수 + gap)부터 계산해야 함을 배웠습니다.

(3) 무한 스크롤 — IntersectionObserver

더보기 버튼을 제거하고 무한 스크롤로 바꿀 때, 도저히 어떻게 해야하는건지 모르겠어서 AI의 도움을 많이 받았습니다.

  • sentinel 엘리먼트 하나를 감시하고 rootMargin: "200px"로 뷰포트 도달 전에 미리 트리거.
  • IntersectionObserver를 이용함으로써 스크롤 이벤트 throttle/debounce를 직접 관리하지 않아도 되고, 레이아웃 스래싱을 유발하는 동기 스타일 읽기가 없어 성능상 안전.

배운 점: 브라우저가 이미 최적화해 둔 관찰자 API가 있을 때는 직접 이벤트를 쌓지 말자. 관찰 대상(센티널)을 명시적으로 DOM에 두니 "언제 더 불러올지" 의 결정 지점이 한 곳으로 모여 디버깅도 쉬워진다.

2) 이번 리뷰를 통해 논의하고 싶은 부분

(1) MovieListController의 책임이 커지고 있는 것 같습니다.

showPopular, search, loadMore, openDetail까지 한 클래스가 다 들고 있고, 생성자 인자도 store, view, hero, notifier, tmdb, modal, ratingRepo 7개입니다. 현재 규모에서는 "한 곳만 보면 전체 흐름이 보인다"는 장점이 있지만, 기능이 더 늘어나면 분리해야 할 것 같습니다.

  • 질문 1: MovieDetailController(모달/별점) vs MovieListController(목록/검색/스크롤) 같은 식으로 쪼개는 것이 맞을까요? 아니면 현재 규모에선 유지하는 게 낫다고 보실까요?

  • 질문 2: 생성자 인자가 7개인데, AI는 Builder/Factory를 도입하거나 DI 컨테이너 비슷한 패턴을 고려할 수도 있다고 하더라구요. 둘다 뭔지 저는 잘 모르지만, 지금 고려해보거나 공부헤볼만한 시점일까요? 아니면 main.ts에서 조립하는 현재 방식이면 충분할까요?

(2) any로 받는 API 응답 매퍼의 타입 안전성

mapMovieListResponse(data: any), mapMovieDetailResponse(data: any)로 받고 내부에서 필드를 꺼내 Movie/MovieDetail로 매핑하고 있습니다. mapMovieListResponse에서 Array.isArray(data.results) 검증은 추가했지만, 각 필드(id, title, vote_average 등)의 존재/타입은 검증하지 않습니다.

  • 질문 1: Zod 같은 런타임 스키마 검증 도구의 존재를 AI를 통해 알게되었는데요, 공부해보고 도입해볼 타이밍일까요, 아니면 지금은 TMDB 응답 스펙을 신뢰하고 최소 방어만 유지하는 게 맞을까요?

  • 질문 2: any 대신 TMDB 응답 타입(TmdbMovieListResponse 같은)을 명시해 unknown에서 좁혀가는 방식이 더 나을까요?

(3) innerHTML 사용으로 인한 잠재적 XSS

movieListMarkup.ts에서 movie.title을 template literal로 보간해 innerHTML에 주입하고 있습니다. TMDB 응답이라 실제 공격 가능성은 낮지만, "외부에서 받은 데이터를 그대로 innerHTML에 넣지 않는다"는 원칙에는 어긋납니다.

AI에게 방법들을 제안받았는데요, createElement를 이용하는 방식,
+ cloneNode + textContent 조합으로 교체하는 방식, DOMPurify 같은 sanitizer를 쓰는 방식, Trusted Types API를 쓰는 방식 등 다양한 제안이었는데요, 이 것들 중 어느 것이 이 프로젝트에 가장 맞을까요? 저는 거의 처음들어보는 개념들인데요, 혹시 다 공부해두는 것이 어느정도 필요한 지식들일까요?
(혹은 TMDB 같은 신뢰 가능한 소스라면 그냥 두는 것도 실용적 선택일까요?)

(4) e2e 테스트 코드 작성

미션을 수행하면서 TDD방식이 아니기도 했고, 피곤하다는 이유로 대부분의 테스트코드를 AI에게 맡겨버렸습니다. 단위테스트는 로또미션까지 수행하면서 테스트코드에 익숙해지긴 한거같은데, e2e테스트의 경우 cypress를 전혀 모르는 상태에서 작성된 코드를 보다보니 확실히 익숙하지 않습니다.

질문: cypress와 jest를 한번 깊게 파보는게 필요할까요? 테스트 코드 작성이 아직은 귀찮게 느껴지기도 하고, 테스트 코드를 잘 작성한다고 서비스가 완성되는게 아니다 보니 열정이 샘솟지는 않는거 같아요. 차라리 한번 열을 올려서 공부해두는걸 추천할만하다고 생각하시는지 궁금합니다!


✅ 리뷰어 체크 포인트

  • 비동기 통신 과정에서 발생할 수 있는 예외(네트워크, 서버 오류 등)를 고려했는가?
  • 비동기 로직에서 콜백 지옥 없이, 적절히 async/await 또는 Promise를 활용했는가?
  • 역할과 책임에 따라 파일/모듈을 분리했는가? (UI, 비즈니스 로직, API 호출 등)

JetProc and others added 30 commits March 31, 2026 16:55
Co-authored-by: Eastar-DS <Eastar-DS@users.noreply.github.com>
Co-authored-by: Eastar-DS <Eastar-DS@users.noreply.github.com>
Co-authored-by: Eastar-DS <Eastar-DS@users.noreply.github.com>
Co-authored-by: Eastar-DS <Eastar-DS@users.noreply.github.com>
…sponse interface 정의

Co-authored-by: Eastar-DS <Eastar-DS@users.noreply.github.com>
Co-authored-by: Eastar-DS <Eastar-DS@users.noreply.github.com>
Co-authored-by: Eastar-DS <Eastar-DS@users.noreply.github.com>
Co-authored-by: Eastar-DS <Eastar-DS@users.noreply.github.com>
Eastar-DS added 27 commits April 9, 2026 14:06
- 최대 4열까지만 표시, 1,2,4열만 사용
- 로고와 서치바 위치, Hero 크기
- Hero 타이틀 등 텍스트 크기
- 모달 작은화면에서는 바텀시트로 변경
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 13, 2026

Warning

Rate limit exceeded

@Eastar-DS has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 19 minutes and 58 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 19 minutes and 58 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cfaf5309-46ba-4517-ac88-524c6e9ee4de

📥 Commits

Reviewing files that changed from the base of the PR and between 900a152 and 37886d8.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (27)
  • cypress/e2e/spec.cy.ts
  • index.html
  • package.json
  • src/api/TmdbClient.ts
  • src/api/apiTypes.ts
  • src/api/movieResponseMapper.ts
  • src/constants/constant.ts
  • src/dom/AppShell.ts
  • src/hero/HeroSection.ts
  • src/main.ts
  • src/modal/MovieDetailModal.ts
  • src/modal/StarRating.ts
  • src/movie-list/InfiniteScroll.ts
  • src/movie-list/MovieListController.ts
  • src/movie-list/MovieListView.ts
  • src/movie-list/movieListMarkup.ts
  • src/rating/LocalStorageRatingRepo.ts
  • src/rating/MovieRatingRepo.ts
  • src/styles/index.css
  • src/styles/main.css
  • src/styles/modal.css
  • src/styles/thumbnail.css
  • tests/API/mapper.test.ts
  • tests/modal/StarRating.test.ts
  • tests/rating/LocalStorageRatingRepo.test.ts
  • types/movie.ts
  • vitest.config.ts

Walkthrough

이 PR은 TMDB API를 활용한 영화 리뷰 애플리케이션의 전면적인 재구성을 포함합니다. API 클라이언트, 영화 목록 관리, 모달 시스템, 별점 기능 등 핵심 기능의 TypeScript 기반 구현을 추가했습니다. 인덱스 HTML을 완전히 재구조화하여 헤더, 히어로 섹션, 모달 오버레이를 포함한 풀스택 UI 스캐폴드로 변경했습니다. 스타일시트와 템플릿을 새로운 소스 구조로 마이그레이션하고 포괄적인 E2E 및 유닛 테스트 스위트를 추가했으며 패키지 의존성을 업데이트했습니다.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed 제목은 미션 단계('[2단계 - 영화 목록 불러오기]')와 제출 의도('이스타 미션 제출합니다')를 명확히 전달하며, PR의 주요 변경사항을 적절히 요약합니다.
Description check ✅ Passed PR 설명이 제공된 템플릿의 모든 필수 섹션(미션 소개, 셀프 리뷰 체크리스트, 리뷰 요청 사항, 리뷰어 체크포인트)을 완성하여 포함하고 있습니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Eastar-DS Eastar-DS changed the title [2단계 - 영화 목록 불러오기] 이스타 미션 제출합니다. [2단계 - 상세 정보 & UI/UX 개선하기] 이스타 미션 제출합니다. Apr 13, 2026
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.

2 participants