[refactor] LazyImage 컴포넌트 기능 개선 및 성능 최적화#913
Conversation
- LazyImage에 커스터마이징 가능한 props 추가 (placeholder, threshold, rootMargin, isEager) - 이미지 로딩 완료 시 fade-in 효과 추가로 UX 개선 - IntersectionObserver 설정을 더 유연하게 커스터마이징 가능하도록 개선 - PhotoCardList에서 첫 4개 이미지는 eager loading 적용하여 초기 로딩 성능 개선 - ClubDetailPage에서 매직 넘버를 상수로 추출하고 불필요한 주석 코드 제거 - 불필요한 React import 제거
- LazyImage를 common에서 PhotoList 컴포넌트 내부로 이동하여 응집도 향상 - inline style을 styled-components로 리팩토링하여 프로젝트 스타일 가이드 준수 - transient props ($isLoaded, $placeholder) 사용으로 DOM 오염 방지 - PhotoCardList의 import 경로 업데이트 - 테스트 파일도 함께 이동하여 일관성 유지
- IntersectionObserver의 rootMargin 설정을 제거하여 기본값(0px) 사용 - 이미지 로딩 시점을 뷰포트 진입 시점으로 명확히 하여 불필요한 미리 로딩 제거 - 컴포넌트 props 인터페이스 단순화
- 테스트 종속적인 data-testid 속성을 프로덕션 코드에서 제거
- 테스트 코드에서 이미지 부재 여부로 로딩 상태를 검증하도록 수정
- 테스트 설명 문구를 평서문으로 통일 ('~함' -> '~한다')
- 불필요한 타이머 모킹(fake timers) 제거
- LazyImage 컴포넌트의 placeholder 기본값 상수화
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning
|
| 코호트 / 파일(s) | 요약 |
|---|---|
공통 LazyImage 제거 frontend/src/components/common/LazyImage/LazyImage.tsx |
기존 공통 LazyImage 컴포넌트 파일 삭제 (default export 제거) |
ClubDetailPage 수정 frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx |
MobileWindowWidth 상수 도입(500), 헤더 표시 로직과 리사이즈 핸들러에서 상수 사용, 리사이즈 이벤트 정리 추가 |
클럽 상세 전용 LazyImage 추가 frontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.tsx |
IntersectionObserver 기반 지연 로딩 재구현, 100ms 지연 제거, props: src, alt, onError, placeholder, threshold, isEager, isEager로 즉시 로드 제어, isVisible/isLoaded 상태 관리, default export 추가 |
LazyImage 스타일링 추가 frontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.styles.ts |
ImageContainer, StyledImage styled-components 추가: 로딩 상태에 따른 배경·투명도 전환(0.3s) |
LazyImage 테스트 업데이트 frontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.test.tsx |
테스트 리팩토링: 100ms 타이머 제거, IntersectionObserver 동작 검증 유지, 설명 한국어화 및 에러 핸들링 검증 방식 조정 |
PhotoCardList 통합 변경 frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoCardList/PhotoCardList.tsx |
LazyImage import 경로를 클럽 상세 페이지 전용으로 변경, IMAGE_EAGER_LOADING_COUNT = 4 도입, index < 4인 항목에 isEager 전달 |
Estimated code review effort
🎯 3 (Moderate) | ⏱️ ~30 minutes
- 주의할 파일/포인트:
frontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.tsx의 IntersectionObserver 생성/해제 및 isEager 초기화 로직- 이미지 로드 흐름: loading 속성('eager' vs 'lazy')과 isVisible/isLoaded 상태 일치성
- 테스트(
LazyImage.test.tsx)에서 IntersectionObserver 시뮬레이션과 에러/언마운트 검증 적절성 - PhotoCardList에서 isEager 전달 조건과 import 경로 변경으로 인한 번들 영향
- ClubDetailPage의 resize 이벤트 cleanup이 누락되지 않았는지
Possibly related issues
- MOA-401: LazyImage 컴포넌트 로직 개선 — 본 PR은 지연 로딩 딜레이 제거 및 지연 로딩 기준 재설정 목적과 일치합니다.
- [refactor] MOA-401 LazyImage 컴포넌트 로직 개선 #907: 공통 LazyImage의 지연로딩 로직 변경 관련 — 100ms 딜레이 제거 및 lazy/eager 동작 변경과 관련됩니다.
Possibly related PRs
- [refactor] 활동사진 지연로딩 제거 #354 — LazyImage 지연 로딩 로직 단순화 및 딜레이 제거와 직접적인 코드 수준 연관성 존재
- [feature] 상세페이지 활동사진 추가 #238 — LazyImage 컴포넌트 도입/수정 관련으로 컴포넌트 설계가 유사함
- [release] v1.0.2 #388 — 공통 LazyImage 변경/대체와 관련된 작업으로 연관성이 높음
Suggested reviewers
- oesnuj
Pre-merge checks and finishing touches
✅ Passed checks (5 passed)
| Check name | Status | Explanation |
|---|---|---|
| Title check | ✅ Passed | PR 제목은 LazyImage 컴포넌트 기능 개선 및 성능 최적화라는 주요 변경사항을 명확하게 반영하고 있으며, 실제 코드 변경 내용(setTimeout 제거, isEager prop 추가, 로드 최적화)과 일치합니다. |
| Linked Issues check | ✅ Passed | PR 변경사항이 MOA-401의 모든 요구사항을 충족합니다: setTimeout 제거로 불필요한 딜레이 제거, isEager prop 추가로 지연로딩 기준 재설정, ImageEagerLoadingCount=4로 로딩 로직 개선되었습니다. |
| Out of Scope Changes check | ✅ Passed | 모든 변경사항이 MOA-401의 범위 내에 있습니다. ClubDetailPage의 MobileWindowWidth 상수 도입 및 리스너 정리는 컴포넌트 최적화의 일부이며, 테스트 리팩토링도 LazyImage 개선에 맞춰 적절하게 조정되었습니다. |
| Docstring Coverage | ✅ Passed | No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. |
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
✨ Finishing touches
- 📝 Generate docstrings
🧪 Generate unit tests (beta)
- Create PR with unit tests
- Post copyable unit tests in a comment
- Commit unit tests in branch
refactor/#907-improve-lazy-image-MOA-401
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 @coderabbitai help to get the list of available commands and usage tips.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx (1)
19-20: 상수 명명 규칙 검토 필요매직 넘버를 상수로 추출한 점은 좋습니다. 다만, 일반적인 JavaScript/TypeScript 컨벤션에서 상수는
MOBILE_WINDOW_WIDTH(UPPER_SNAKE_CASE)로 작성하는 것이 권장됩니다. PascalCase는 주로 컴포넌트나 클래스에 사용됩니다.-const MobileWindowWidth = 500; +const MOBILE_WINDOW_WIDTH = 500;frontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.test.tsx (1)
41-46:isEagerprop에 대한 테스트 케이스 추가 고려현재 테스트는 기본 lazy loading 동작만 검증합니다.
isEager={true}케이스에 대한 테스트를 추가하면 새로운 기능의 동작을 보장할 수 있습니다.it('isEager가 true일 때 이미지가 즉시 렌더링되어야 한다', () => { render(<LazyImage {...defaultProps} isEager={true} />); const img = screen.getByRole('img'); expect(img).toBeInTheDocument(); expect(img).toHaveAttribute('src', 'test-image.jpg'); });frontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.tsx (1)
49-61:loading속성과 IntersectionObserver 중복 고려
loading="lazy"속성은 브라우저 네이티브 lazy loading을 활성화합니다. 현재 구현에서는 IntersectionObserver로 이미 visibility를 제어하므로, 이미지가 렌더링될 때는 이미 viewport 내에 있어loading="lazy"가 실질적인 효과가 없을 수 있습니다.의도적인 fallback 전략이라면 현재 구현을 유지해도 무방합니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (6)
frontend/src/components/common/LazyImage/LazyImage.tsx(0 hunks)frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx(1 hunks)frontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.styles.ts(1 hunks)frontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.test.tsx(2 hunks)frontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.tsx(1 hunks)frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoCardList/PhotoCardList.tsx(3 hunks)
💤 Files with no reviewable changes (1)
- frontend/src/components/common/LazyImage/LazyImage.tsx
🧰 Additional context used
📓 Path-based instructions (3)
frontend/**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (frontend/.cursorrules)
frontend/**/*.{ts,tsx,js,jsx}: Replace magic numbers with named constants for clarity
Replace complex/nested ternaries withif/elseor IIFEs for readability
Assign complex boolean conditions to named variables for explicit meaning
Avoid hidden side effects; functions should only perform actions implied by their signature (Single Responsibility Principle)
Use unique and descriptive names for custom wrappers/functions to avoid ambiguity
Define constants near related logic or ensure names link them clearly to avoid silent failures
Break down broad state management into smaller, focused hooks/contexts to reduce coupling
Files:
frontend/src/pages/ClubDetailPage/ClubDetailPage.tsxfrontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.styles.tsfrontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.tsxfrontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.test.tsxfrontend/src/pages/ClubDetailPage/components/PhotoList/PhotoCardList/PhotoCardList.tsx
frontend/**/*.{tsx,jsx}
📄 CodeRabbit inference engine (frontend/.cursorrules)
frontend/**/*.{tsx,jsx}: Abstract complex logic/interactions into dedicated components/HOCs
Separate significantly different conditional UI/logic into distinct components
Colocate simple, localized logic or use inline definitions to reduce context switching
Choose field-level or form-level cohesion based on form requirements when using form libraries like react-hook-form
Use Component Composition instead of Props Drilling to reduce coupling
Files:
frontend/src/pages/ClubDetailPage/ClubDetailPage.tsxfrontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.tsxfrontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.test.tsxfrontend/src/pages/ClubDetailPage/components/PhotoList/PhotoCardList/PhotoCardList.tsx
frontend/**/*.{ts,tsx}
📄 CodeRabbit inference engine (frontend/.cursorrules)
Use consistent return types for similar functions/hooks
Files:
frontend/src/pages/ClubDetailPage/ClubDetailPage.tsxfrontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.styles.tsfrontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.tsxfrontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.test.tsxfrontend/src/pages/ClubDetailPage/components/PhotoList/PhotoCardList/PhotoCardList.tsx
🧠 Learnings (3)
📓 Common learnings
Learnt from: seongwon030
Repo: Moadong/moadong PR: 195
File: frontend/src/pages/AdminPage/AdminPage.tsx:7-7
Timestamp: 2025-03-19T05:18:07.818Z
Learning: AdminPage.tsx에서 현재 하드코딩된 클럽 ID('67d2e3b9b15c136c6acbf20b')는 로그인 기능 구현 후 동적으로 가져오는 방식으로 수정될 예정입니다.
📚 Learning: 2025-03-19T05:18:07.818Z
Learnt from: seongwon030
Repo: Moadong/moadong PR: 195
File: frontend/src/pages/AdminPage/AdminPage.tsx:7-7
Timestamp: 2025-03-19T05:18:07.818Z
Learning: AdminPage.tsx에서 현재 하드코딩된 클럽 ID('67d2e3b9b15c136c6acbf20b')는 로그인 기능 구현 후 동적으로 가져오는 방식으로 수정될 예정입니다.
Applied to files:
frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
📚 Learning: 2025-11-25T14:08:23.253Z
Learnt from: CR
Repo: Moadong/moadong PR: 0
File: frontend/.cursorrules:0-0
Timestamp: 2025-11-25T14:08:23.253Z
Learning: Applies to frontend/**/*.{ts,tsx,js,jsx} : Replace magic numbers with named constants for clarity
Applied to files:
frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
🧬 Code graph analysis (1)
frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx (1)
frontend/src/hooks/queries/club/useGetClubDetail.ts (1)
useGetClubDetail(6-20)
🔇 Additional comments (6)
frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx (1)
28-35: LGTM!resize 이벤트 리스너에 cleanup 함수가 올바르게 구현되어 메모리 누수를 방지합니다.
frontend/src/pages/ClubDetailPage/components/PhotoList/PhotoCardList/PhotoCardList.tsx (2)
11-12: LGTM!매직 넘버
4를IMAGE_EAGER_LOADING_COUNT상수로 추출하여 코드 가독성을 높였습니다. PR 목표에 맞게 데스크탑/태블릿/모바일 공통으로 4개 기준을 적용했습니다.
21-34: LGTM!
isEagerprop을 인덱스 기반으로 전달하여 초기 렌더링 시 보이는 이미지들은 즉시 로딩되도록 구현했습니다. 기존 에러 처리 로직도 유지됩니다.frontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.styles.ts (1)
1-18: LGTM!Transient props (
$isLoaded,$placeholder)를 올바르게 사용하여 DOM에 불필요한 속성이 전달되지 않도록 했습니다. 0.3s 트랜지션으로 부드러운 로딩 효과를 제공합니다.frontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.test.tsx (1)
57-75: LGTM!IntersectionObserver 콜백 호출 시 이미지가 로드되고, observer가 disconnect되는 동작을 올바르게 검증합니다. 100ms setTimeout 제거에 맞게 타이머 관련 코드도 제거되었습니다.
frontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.tsx (1)
28-47: LGTM!PR 목표에 맞게 100ms setTimeout이 제거되어 IntersectionObserver 감지 즉시
setIsVisible(true)가 호출됩니다.isEager시 early return으로 불필요한 observer 생성을 방지하고, cleanup 함수로 메모리 누수를 방지합니다.
frontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.tsx
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (1)
frontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.tsx (1)
48-61: 렌더링 로직 검토 및 선택적 리팩토링 제안전반적인 구현은 정확하지만, Line 56의
loading속성 사용이 약간 중복됩니다:
isEager=true일 때:loading="eager"✅ 즉시 고우선순위 로딩isEager=false일 때: IntersectionObserver가 이미 지연 로딩을 처리했고,<img>엘리먼트가 viewport에 있을 때만 생성되므로loading="lazy"는 실질적 효과가 없습니다.더 명확한 접근 방식:
- loading={isEager ? 'eager' : 'lazy'} + loading="eager"또는 의도를 명확히 하기 위해 조건부로:
- loading={isEager ? 'eager' : 'lazy'} + loading={isEager ? 'eager' : 'eager'}설명: IntersectionObserver가 "언제 로드할지"를 결정하고 나면, 브라우저에게 즉시 로드하도록 지시하는 것이 더 일관성 있습니다. 현재 구현도 동작하지만, 두 가지 지연 로딩 메커니즘(IntersectionObserver + 네이티브 lazy loading)이 혼재되어 약간 혼란스러울 수 있습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
frontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.tsx(1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
frontend/**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (frontend/.cursorrules)
frontend/**/*.{ts,tsx,js,jsx}: Replace magic numbers with named constants for clarity
Replace complex/nested ternaries withif/elseor IIFEs for readability
Assign complex boolean conditions to named variables for explicit meaning
Avoid hidden side effects; functions should only perform actions implied by their signature (Single Responsibility Principle)
Use unique and descriptive names for custom wrappers/functions to avoid ambiguity
Define constants near related logic or ensure names link them clearly to avoid silent failures
Break down broad state management into smaller, focused hooks/contexts to reduce coupling
Files:
frontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.tsx
frontend/**/*.{tsx,jsx}
📄 CodeRabbit inference engine (frontend/.cursorrules)
frontend/**/*.{tsx,jsx}: Abstract complex logic/interactions into dedicated components/HOCs
Separate significantly different conditional UI/logic into distinct components
Colocate simple, localized logic or use inline definitions to reduce context switching
Choose field-level or form-level cohesion based on form requirements when using form libraries like react-hook-form
Use Component Composition instead of Props Drilling to reduce coupling
Files:
frontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.tsx
frontend/**/*.{ts,tsx}
📄 CodeRabbit inference engine (frontend/.cursorrules)
Use consistent return types for similar functions/hooks
Files:
frontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.tsx
🧠 Learnings (1)
📓 Common learnings
Learnt from: seongwon030
Repo: Moadong/moadong PR: 195
File: frontend/src/pages/AdminPage/AdminPage.tsx:7-7
Timestamp: 2025-03-19T05:18:07.818Z
Learning: AdminPage.tsx에서 현재 하드코딩된 클럽 ID('67d2e3b9b15c136c6acbf20b')는 로그인 기능 구현 후 동적으로 가져오는 방식으로 수정될 예정입니다.
🔇 Additional comments (5)
frontend/src/pages/ClubDetailPage/components/PhotoList/LazyImage/LazyImage.tsx (5)
1-2: LGTM!필요한 의존성만 깔끔하게 import하고 있습니다.
4-11: LGTM!인터페이스 정의가 명확하고, 이전 리뷰에서 지적된 미사용
rootMarginprop이 제거되었습니다.isEagerprop 추가로 PR 목표가 잘 반영되었습니다.
13-13: LGTM!매직 문자열을 명명된 상수로 추출하여 코드 가독성과 유지보수성을 개선했습니다.
15-25: LGTM!
isVisible상태를isEager값으로 초기화하는 것은 효율적인 최적화입니다. eager 이미지는 IntersectionObserver 설정 없이 즉시 렌더링됩니다.
27-46: LGTM!IntersectionObserver 로직이 PR 목표를 완벽하게 달성했습니다:
- ✅ 100ms setTimeout 제거: 교차 감지 즉시
setIsVisible(true)호출- ✅
isEager플래그 지원: eager 이미지는 observer 설정 생략- ✅ 적절한 cleanup과 의존성 관리
#️⃣연관된 이슈
📝작업 내용
문제점
1. 불필요한 timeout 설정
timeout 100ms가 설정되어 있어 사용자 경험을 해칠 수 있습니다.
React와 Oberver 순서에서도 딜레이는 불필요합니다.
딜레이를 설정하면 이 흐름을 강제로 끊게 됩니다.
2. 모든 이미지에 지연로딩 적용
Intersection Obersever로 활동사진 슬라이드의 "다음 사진"의 교차점을 감지하여 지연로딩을 시키는 것이 목적이었습니다.하지만 기존 로직으로는 최초 로딩 시 보이는 이미지들도 지연 로딩하고 있었습니다.
개선
1. setTimeout 제거
setTimeout 관련 코드를 제거하고 observer만 남겼습니다. 이제 100ms 딜레이없이 바로 이미지를 로드해 옵니다.
2. isEager prop추가
최초 렌더링 시 보이는 이미지는 즉시 로딩하도록
isEager를 LazyImageProps에 추가했습니다그리고 isEager의 기준을
index < 4로 설정하여 데스크탑, 태블릿, 모바일 모두 최초 렌더링 이미지 개수를 4개로 통일했습니다.최초 로드 시 보이는 이미지 최대 개수가 데스크탑 3개, 태블릿 2개, 모바일 4개였기에 4개를 기준으로 잡았습니다.
이미지 개수 설정 고민
반응형 훅인 useDevice를 사용할 시 자바스크립트가 로드되고 실행되어야만 화면 크기를 알 수 있기에 비효율적.
css만으로
nth-child를 통해 Eager 로딩 개수를 정하는 방법도 고민했는데, 반응형 훅을 사용하는 것보다 확실히 LCP 손해가 없고 책임 분리에도 용이할 것 같았습니다.그러나.. LCP까지 고려할 정도로 사이트가 사용성이 나쁘지도 않고, 또
index < 4라는 하나의 코드로도 모든 반응형을 커버할 수 있는데 이 방법을 적용하는 것은 오버 엔지니어링이라 판단했습니다.3. 파일 위치 변경
활동사진을 위한 공통 훅이기에 파일 위치를 옮겼습니다.
기존:
src/common/Lazy~변경:
src/pages/ClubDetailPage/components/PhotoList/Lazy~중점적으로 리뷰받고 싶은 부분(선택)
논의하고 싶은 부분(선택)
🫡 참고사항
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.