[2단계 - 상세 정보 & UI/UX 개선하기] 이스타 미션 제출합니다.#305
[2단계 - 상세 정보 & UI/UX 개선하기] 이스타 미션 제출합니다.#305Eastar-DS wants to merge 102 commits intowoowacourse:eastar-dsfrom
Conversation
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>
- 최대 4열까지만 표시, 1,2,4열만 사용 - 로고와 서치바 위치, Hero 크기 - Hero 타이틀 등 텍스트 크기 - 모달 작은화면에서는 바텀시트로 변경
|
Warning Rate limit exceeded
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 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 configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (27)
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)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
🎯 미션 소개
🕵️ 셀프 리뷰(Self-Review)
제출 전 체크 리스트
프로젝트 의존성 그래프
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 shared2. 파일 단위 상세 그래프
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 shared3. 별점 저장소의 "교체 가능한 구조" (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 other4. 에러 흐름 (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 ui5. 런타임 흐름 — 영화 상세 모달 열기 (시퀀스)
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: 모달 표시리뷰 요청 & 논의하고 싶은 내용
1) 이번 단계에서 가장 많이 고민했던 문제와 해결 과정에서 배운 점
저번 미션 마지막부터 AI의 도움을 많이 받고있는데요, 저번에 느꼈던 기술부채를 줄이기위해 이번 미션과 관련된 html과 css를 집중하여 먼저 머리속에 틀을 잡고 step2 미션을 시작했습니다. 틀을 잡고 진행하니 AI가 제안해준 코드가 이해되기 시작해서 저번보단 훨씬 기분좋게 미션을 수행했습니다!
(1) 별점 저장소를 어떻게 추상화할 것인가
미션 설명에 "지금은 localStorage로 구현하지만 다음 배포에 서버 API로 교체할 예정" 이라는 제약이 명시되어 있어서, 구현체를 바꿀 때 Controller/Modal 같은 상위 레이어가 흔들리지 않는 구조를 먼저 고민해야한다고 AI를 통해 알게되었습니다. 이 과정에서 레포지토리 패턴이라는 용어를 처음 알게 되었고, 적용해보았습니다.
(2) 브레이크포인트를 어디에 둘 것인가
처음엔 520px / 1120px로 잡았는데, 태블릿에서 3열(3×200px = 600px)을 그리는 순간 그리드가 부모 폭을 넘는 문제가 생겼습니다.
배운 점: 브레이크포인트는 "그 지점에서 레이아웃이 실제로 깨지는가" 를 먼저 고려해야 한다는 것. 숫자를 먼저 박지 않고 콘텐츠(카드 크기 × 열 개수 + gap)부터 계산해야 함을 배웠습니다.
(3) 무한 스크롤 — IntersectionObserver
더보기 버튼을 제거하고 무한 스크롤로 바꿀 때, 도저히 어떻게 해야하는건지 모르겠어서 AI의 도움을 많이 받았습니다.
배운 점: 브라우저가 이미 최적화해 둔 관찰자 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를 한번 깊게 파보는게 필요할까요? 테스트 코드 작성이 아직은 귀찮게 느껴지기도 하고, 테스트 코드를 잘 작성한다고 서비스가 완성되는게 아니다 보니 열정이 샘솟지는 않는거 같아요. 차라리 한번 열을 올려서 공부해두는걸 추천할만하다고 생각하시는지 궁금합니다!
✅ 리뷰어 체크 포인트