feat: 이벤트 배너 이미지 형식을 WebP로 전환하여 이미지 성능 개선#198
Conversation
|
""" Walkthrough이미지 업로드 및 변환 로직이 개선되어, 모든 업로드 이미지가 WebP 포맷으로 변환된 후 업로드되도록 변경되었습니다. 이를 위해 WebP 변환 유틸리티가 추가되었고, JPG 이미지를 WebP로 일괄 변환하는 마이그레이션 스크립트도 도입되었습니다. 개발 서버의 HTTPS 설정은 비활성화되었으며, 주요 UI 컴포넌트들이 외부 상태 제어를 지원하도록 리팩토링되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Frontend
participant convertImageToWebP
participant S3
User->>Frontend: 이미지 파일 선택
Frontend->>convertImageToWebP: 파일 WebP로 변환 요청
convertImageToWebP-->>Frontend: WebP 파일 반환
Frontend->>S3: WebP 파일 presigned URL 요청 및 업로드
S3-->>Frontend: 업로드 완료 응답
Frontend-->>User: 업로드 성공 알림
Possibly related PRs
Poem
""" ✨ Finishing Touches
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 1
🔭 Outside diff range comments (1)
src/features/event/hooks/usePresignedUrlHook.ts (1)
38-50: 중복 WebP 변환 문제
uploadFile함수에서 WebP 변환이 수행되는데,useImageUpload.ts에서도 이미 WebP 변환을 수행한 후 이 함수를 호출합니다. 이는 이미 WebP인 파일을 다시 변환하려고 시도하여 비효율적이거나 오류를 발생시킬 수 있습니다.다음 중 하나의 방법으로 해결하세요:
방법 1: uploadFile에서 변환 로직 제거
export const uploadFile = async (file: File) => { - const webFile = await convertImageToWebP(file); - const fileName = webFile.name; + const fileName = file.name; const presignedUrlResponse = await getPresignedUrl({ fileName }); if (!presignedUrlResponse) { throw new Error('Failed to get presigned url'); } const url = presignedUrlResponse; console.log('Presigned URL:', url); - await putS3Image({ url, file: webFile }); + await putS3Image({ url, file }); return url.split('?')[0]; };방법 2: 파일이 이미 WebP인지 확인
export const uploadFile = async (file: File) => { - const webFile = await convertImageToWebP(file); + const webFile = file.type === 'image/webp' ? file : await convertImageToWebP(file); const fileName = webFile.name; // ... 나머지 코드 };
🧹 Nitpick comments (3)
src/shared/hooks/useImageUpload.ts (1)
33-36: 파일 검증 로직 개선 필요현재 검증 로직이 WebP 변환 전 원본 파일에 대해서만 수행됩니다. WebP 변환 후 파일 크기가 줄어들 수 있으므로, 검증 순서나 메시지를 조정하는 것을 고려해보세요.
const validateFile = (file: File) => { if (file.size > 1000 * 1024) { - alert('파일 크기는 1MB를 초과할 수 없습니다.'); + alert('파일 크기는 1MB를 초과할 수 없습니다. (WebP 변환 후 크기가 줄어듭니다)'); return false; } if (!['image/jpg', 'image/jpeg', 'image/png'].includes(file.type)) { - alert('jpg, jpeg, png 파일만 업로드 가능합니다.'); + alert('jpg, jpeg, png 파일만 업로드 가능합니다. (WebP로 자동 변환됩니다)'); return false; } return true; };src/shared/lib/migrateJpgToWebp.ts (1)
4-6: 마이그레이션 대상 URL 배열이 비어있음현재
oldJpgUrls배열이 비어있습니다. 실제 마이그레이션 시에는 변환할 이미지 URL들을 추가해야 합니다.실제 마이그레이션을 위한 URL 목록이 필요하시면 도움을 드릴 수 있습니다.
src/shared/lib/convertImageToWebP.ts (1)
1-36: WebP 변환 구현 품질 우수전반적으로 잘 구현된 WebP 변환 유틸리티입니다. 에러 핸들링이 적절하고 Promise 기반 비동기 처리가 올바릅니다.
개선 제안사항:
- WebP 품질 옵션 추가: 현재 기본 품질로 변환되는데, 품질 조절 옵션을 추가하면 더 유연합니다.
- 브라우저 호환성 확인: WebP 지원 여부를 확인하는 로직을 추가하세요.
- 이미지 크기 제한: 매우 큰 이미지의 경우 메모리 사용량이 클 수 있습니다.
-export const convertImageToWebP = (file: File): Promise<File> => { +export const convertImageToWebP = (file: File, quality: number = 0.8): Promise<File> => { return new Promise((resolve, reject) => { + // WebP 지원 확인 + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + if (!ctx || !canvas.toBlob) { + return reject(new Error('WebP 변환이 지원되지 않는 브라우저입니다.')); + } + const img = new Image(); const reader = new FileReader(); // ... 기존 로직 ... img.onload = () => { + // 이미지 크기 제한 (예: 4K) + const MAX_DIMENSION = 4096; + let { width, height } = img; + + if (width > MAX_DIMENSION || height > MAX_DIMENSION) { + const ratio = Math.min(MAX_DIMENSION / width, MAX_DIMENSION / height); + width *= ratio; + height *= ratio; + } + - const canvas = document.createElement('canvas'); - canvas.width = img.width; - canvas.height = img.height; + canvas.width = width; + canvas.height = height; - const ctx = canvas.getContext('2d'); - if (!ctx) return reject(new Error('Canvas context error')); - ctx.drawImage(img, 0, 0); + ctx.drawImage(img, 0, 0, width, height); - canvas.toBlob(blob => { + canvas.toBlob(blob => { // ... 나머지 로직 동일 ... - }, 'image/webp'); + }, 'image/webp', quality); };
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
src/features/event/hooks/usePresignedUrlHook.ts(4 hunks)src/shared/hooks/useImageUpload.ts(2 hunks)src/shared/lib/convertImageToWebP.ts(1 hunks)src/shared/lib/migrateJpgToWebp.ts(1 hunks)vite.config.ts(2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
src/shared/hooks/useImageUpload.ts (2)
src/shared/lib/convertImageToWebP.ts (1)
convertImageToWebP(1-36)src/features/event/hooks/usePresignedUrlHook.ts (1)
uploadFile(37-54)
src/features/event/hooks/usePresignedUrlHook.ts (1)
src/shared/lib/convertImageToWebP.ts (1)
convertImageToWebP(1-36)
src/shared/lib/migrateJpgToWebp.ts (2)
src/shared/lib/convertImageToWebP.ts (1)
convertImageToWebP(1-36)src/features/event/hooks/usePresignedUrlHook.ts (1)
uploadFile(37-54)
🔇 Additional comments (6)
vite.config.ts (1)
6-6: 로컬 개발용 HTTPS 비활성화 확인HTTPS 설정이 주석 처리되어 있습니다. 로컬 개발 목적이라면 문제없지만, 프로덕션 배포 시에는 다시 활성화해야 합니다.
Also applies to: 23-27
src/shared/hooks/useImageUpload.ts (2)
3-3: WebP 변환 유틸리티 import 추가 확인WebP 변환 기능이 올바르게 import되었습니다.
45-46: WebP 변환 로직 구현 확인WebP 변환이 올바르게 구현되었습니다. 에러 핸들링도 적절합니다.
src/features/event/hooks/usePresignedUrlHook.ts (2)
5-5: WebP 변환 유틸리티 import 추가 확인WebP 변환 기능이 올바르게 import되었습니다.
27-27: Content-Type 헤더 올바르게 설정WebP 파일 업로드를 위해 Content-Type이 올바르게 설정되었습니다.
src/shared/lib/migrateJpgToWebp.ts (1)
28-31: 파일명 추출 로직 확인파일명 추출 및 확장자 변환 로직이 올바르게 구현되었습니다. URL 파라미터 제거와 확장자 변환이 적절합니다.
| export const runImageMigration = async () => { | ||
| for (const jpgUrl of oldJpgUrls) { | ||
| try { | ||
| const response = await fetch(jpgUrl); | ||
| const blob = await response.blob(); | ||
|
|
||
| const file = new File([blob], extractFileName(jpgUrl), { type: blob.type }); | ||
|
|
||
| const webpFile = await convertImageToWebP(file); | ||
| const webpUrl = await uploadFile(webpFile); | ||
|
|
||
| console.log(`✅ ${jpgUrl} → ${webpUrl}`); | ||
| } catch (error) { | ||
| console.error(`❌ 변환 실패: ${jpgUrl}`, error); | ||
| } | ||
| } | ||
|
|
||
| console.log('✅ 전체 마이그레이션 완료'); | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
마이그레이션 로직 개선 제안
기본 구조는 좋지만 다음 개선사항들을 고려해보세요:
- 동시 요청 제한: 많은 이미지를 한번에 처리하면 메모리 부족이나 서버 부하가 발생할 수 있습니다.
- 네트워크 타임아웃: fetch 요청에 타임아웃을 설정하세요.
- 재시도 로직: 네트워크 오류 시 재시도 메커니즘을 추가하세요.
export const runImageMigration = async () => {
+ const BATCH_SIZE = 5; // 동시 처리할 이미지 수 제한
+
+ for (let i = 0; i < oldJpgUrls.length; i += BATCH_SIZE) {
+ const batch = oldJpgUrls.slice(i, i + BATCH_SIZE);
+ const promises = batch.map(async (jpgUrl) => {
- for (const jpgUrl of oldJpgUrls) {
try {
- const response = await fetch(jpgUrl);
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), 30000); // 30초 타임아웃
+
+ const response = await fetch(jpgUrl, {
+ signal: controller.signal
+ });
+ clearTimeout(timeoutId);
+
const blob = await response.blob();
// ... 나머지 로직
} catch (error) {
console.error(`❌ 변환 실패: ${jpgUrl}`, error);
}
+ });
+
+ await Promise.allSettled(promises);
+
+ // 배치 간 딜레이
+ if (i + BATCH_SIZE < oldJpgUrls.length) {
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export const runImageMigration = async () => { | |
| for (const jpgUrl of oldJpgUrls) { | |
| try { | |
| const response = await fetch(jpgUrl); | |
| const blob = await response.blob(); | |
| const file = new File([blob], extractFileName(jpgUrl), { type: blob.type }); | |
| const webpFile = await convertImageToWebP(file); | |
| const webpUrl = await uploadFile(webpFile); | |
| console.log(`✅ ${jpgUrl} → ${webpUrl}`); | |
| } catch (error) { | |
| console.error(`❌ 변환 실패: ${jpgUrl}`, error); | |
| } | |
| } | |
| console.log('✅ 전체 마이그레이션 완료'); | |
| }; | |
| export const runImageMigration = async () => { | |
| const BATCH_SIZE = 5; // 동시 처리할 이미지 수 제한 | |
| for (let i = 0; i < oldJpgUrls.length; i += BATCH_SIZE) { | |
| const batch = oldJpgUrls.slice(i, i + BATCH_SIZE); | |
| const promises = batch.map(async (jpgUrl) => { | |
| try { | |
| // 네트워크 타임아웃 설정 | |
| const controller = new AbortController(); | |
| const timeoutId = setTimeout(() => controller.abort(), 30000); // 30초 타임아웃 | |
| const response = await fetch(jpgUrl, { signal: controller.signal }); | |
| clearTimeout(timeoutId); | |
| const blob = await response.blob(); | |
| const file = new File([blob], extractFileName(jpgUrl), { type: blob.type }); | |
| const webpFile = await convertImageToWebP(file); | |
| const webpUrl = await uploadFile(webpFile); | |
| console.log(`✅ ${jpgUrl} → ${webpUrl}`); | |
| } catch (error) { | |
| console.error(`❌ 변환 실패: ${jpgUrl}`, error); | |
| } | |
| }); | |
| // 현재 배치의 모든 작업이 끝날 때까지 대기 | |
| await Promise.allSettled(promises); | |
| // 배치 간 딜레이 (옵션) | |
| if (i + BATCH_SIZE < oldJpgUrls.length) { | |
| await new Promise(resolve => setTimeout(resolve, 1000)); | |
| } | |
| } | |
| console.log('✅ 전체 마이그레이션 완료'); | |
| }; |
🤖 Prompt for AI Agents
In src/shared/lib/migrateJpgToWebp.ts around lines 8 to 26, the image migration
runs all fetch and conversion requests sequentially without concurrency control,
lacks timeout handling on fetch calls, and does not retry on network failures.
To fix this, implement a concurrency limit to process a limited number of images
simultaneously, add a timeout mechanism to the fetch requests to avoid hanging,
and include a retry logic that attempts the fetch and conversion a few times
before failing, to improve robustness and resource management.
7dbae90 to
286e15d
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
src/features/event/ui/FileUpload.tsx (1)
24-24: 이미지 URL 초기화 로직 개선 제안nullish coalescing 연산자를 사용한 이미지 URL 초기화 로직이 올바르게 구현되었습니다만, 더 명확한 우선순위 처리를 위해 개선할 수 있습니다.
-value: value ?? eventState?.bannerImageUrl ?? '', +value: value || eventState?.bannerImageUrl || '',또는 더 명시적으로:
-value: value ?? eventState?.bannerImageUrl ?? '', +value: value !== undefined ? value : (eventState?.bannerImageUrl || ''),src/features/event/ui/LinkInput.tsx (2)
37-39: 불필요한 조건 검사 제거 가능newLinks 변수는 항상 배열이므로 조건 검사가 불필요합니다.
-const removeLink = (index: number) => { - const newLinks = links.filter((_, i) => i !== index); - if (newLinks) updateAll(newLinks); -}; +const removeLink = (index: number) => { + const newLinks = links.filter((_, i) => i !== index); + updateAll(newLinks); +};
41-45: 링크 업데이트 로직 개선 제안링크 업데이트 로직이 올바르게 구현되었습니다만, 안전성을 위해 개선할 수 있습니다.
-const updateLink = (index: number, field: keyof Link, value: string) => { - const newLinks = [...(links ?? [])]; - newLinks[index] = { ...newLinks[index], [field]: value }; - updateAll(newLinks); -}; +const updateLink = (index: number, field: keyof Link, value: string) => { + if (index < 0 || index >= links.length) return; + const newLinks = [...links]; + newLinks[index] = { ...newLinks[index], [field]: value }; + updateAll(newLinks); +};
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
src/features/event/ui/FileUpload.tsx(1 hunks)src/features/event/ui/LinkInput.tsx(3 hunks)src/features/event/ui/TextEditor.tsx(4 hunks)src/pages/dashboard/ui/EventDetailPage.tsx(1 hunks)src/pages/dashboard/ui/EventInfoPage.tsx(1 hunks)src/pages/event/ui/create-event/EventInfoPage.tsx(2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (4)
src/pages/event/ui/create-event/EventInfoPage.tsx (1)
src/features/event/model/FunnelContext.tsx (1)
useFunnelState(53-59)
src/features/event/ui/FileUpload.tsx (1)
src/features/event/model/FunnelContext.tsx (1)
FunnelState(5-11)
src/features/event/ui/LinkInput.tsx (1)
src/features/event/model/FunnelContext.tsx (1)
FunnelState(5-11)
src/features/event/ui/TextEditor.tsx (1)
src/features/event/model/FunnelContext.tsx (1)
FunnelState(5-11)
🔇 Additional comments (11)
src/pages/dashboard/ui/EventInfoPage.tsx (1)
58-58: ripgrep이--type tsx를 인식하지 못해 검색이 누락되었습니다. glob 패턴으로 확장자를 지정해 다시 확인해 주세요:#!/bin/bash echo "=== 하이픈 제거 로직 검색 (.replace(/-/g, '')) ===" rg "\\.replace\\(/-\\/g, ''\\)" -g '*.ts' -g '*.tsx' echo "=== organizerPhoneNumber 사용 위치 검색 ===" rg "organizerPhoneNumber" -g '*.ts' -g '*.tsx'src/pages/dashboard/ui/EventDetailPage.tsx (1)
69-71: 제어형 컴포넌트 구현이 올바르게 적용되었습니다FileUpload, TextEditor, LinkInput 컴포넌트에 value prop이 올바르게 전달되어 외부 상태 제어가 가능하도록 구현되었습니다.
다만, 컴포넌트 인터페이스를 고려할 때 일부 prop이 누락될 수 있습니다:
-<FileUpload value={bannerImageUrl} onChange={setBannerImageUrl} useDefaultImage={false} /> -<TextEditor value={description} /> -<LinkInput value={referenceLinks} onChange={setReferenceLinks} /> +<FileUpload value={bannerImageUrl} onChange={setBannerImageUrl} useDefaultImage={false} /> +<TextEditor value={description} onChange={setDescription} /> +<LinkInput value={referenceLinks} onChange={setReferenceLinks} />src/pages/event/ui/create-event/EventInfoPage.tsx (2)
12-12: 상태 추출 및 제어형 컴포넌트 패턴 올바르게 구현useFunnelState에서 eventState와 setEventState를 추출하여 하위 컴포넌트에 전달하는 패턴이 올바르게 구현되었습니다.
30-37: 하위 컴포넌트에 상태 전달 구현 완료FileUpload, TextEditor, LinkInput 컴포넌트에 eventState와 setEventState가 올바르게 전달되어 제어형 컴포넌트로 동작할 수 있도록 구현되었습니다.
src/features/event/ui/FileUpload.tsx (1)
7-13: 제어형 컴포넌트 인터페이스 올바르게 정의value와 eventState prop을 추가하여 외부 상태 제어가 가능하도록 인터페이스가 올바르게 정의되었습니다.
src/features/event/ui/LinkInput.tsx (3)
13-17: 제어형 컴포넌트 인터페이스 올바르게 정의value, onChange, eventState, setEventState prop을 추가하여 외부 상태 제어가 가능하도록 인터페이스가 올바르게 정의되었습니다.
20-20: 링크 배열 초기화 로직 올바르게 구현value prop을 우선적으로 사용하고 eventState를 fallback으로 사용하는 로직이 올바르게 구현되었습니다.
29-32: 상태 업데이트 패턴 올바르게 구현onChange 콜백을 먼저 호출한 후 setEventState를 호출하는 패턴이 올바르게 구현되어 외부 상태와 내부 상태가 동기화됩니다.
src/features/event/ui/TextEditor.tsx (3)
5-5: 임포트 변경이 적절합니다.내부 context hook 사용에서 타입만 직접 임포트하는 방식으로 변경한 것이 컴포넌트의 외부 제어 패턴에 적합합니다.
107-109: 검증 로직이 개선되었습니다.
editorContent를 사용하여 검증하는 것이 일관성 있는 접근입니다.
132-133: 렌더링 로직이 올바르게 개선되었습니다.로컬 상태
editorContent를 사용하여 렌더링하는 것이 일관된 접근입니다.Also applies to: 140-140
| useEffect(() => { | ||
| const initial = value ?? eventState?.description ?? ''; | ||
| if (!editorContent && initial) { | ||
| setEditorContent(initial); | ||
| } | ||
| }, [value, eventState?.description, editorContent]); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
초기화 로직의 우선순위가 불분명합니다.
value와 eventState?.description 중 어떤 것이 우선순위를 갖는지 명확하지 않습니다. 또한 editorContent가 비어있을 때만 초기화하는 조건이 예상과 다르게 동작할 수 있습니다.
useEffect(() => {
- const initial = value ?? eventState?.description ?? '';
- if (!editorContent && initial) {
- setEditorContent(initial);
- }
-}, [value, eventState?.description, editorContent]);
+ // value prop이 제공되면 항상 해당 값을 사용
+ if (value !== undefined) {
+ setEditorContent(value);
+ }
+}, [value]);Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/features/event/ui/TextEditor.tsx around lines 44 to 49, clarify the
priority between `value` and `eventState?.description` by explicitly documenting
or restructuring the initialization logic. Also, revise the condition that
checks if `editorContent` is empty before setting it to ensure it behaves as
intended, possibly by using a more precise check or resetting logic to avoid
unexpected behavior.
| interface TextEditorProps { | ||
| eventState?: FunnelState['eventState']; | ||
| setEventState?: React.Dispatch<React.SetStateAction<FunnelState['eventState']>>; | ||
| value?: string; | ||
| onChange?: (value: string) => void; | ||
| onValidationChange?: (isValid: boolean) => void; | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Props 인터페이스 설계를 개선해야 합니다.
현재 설계는 제어형(value/onChange)과 비제어형(eventState/setEventState) 패턴을 동시에 지원하려고 하는데, 이는 사용자에게 혼란을 줄 수 있습니다. 어떤 props가 우선순위를 갖는지 명확하지 않습니다.
interface TextEditorProps {
- eventState?: FunnelState['eventState'];
- setEventState?: React.Dispatch<React.SetStateAction<FunnelState['eventState']>>;
- value?: string;
- onChange?: (value: string) => void;
+ // 제어형 패턴 또는 비제어형 패턴 중 하나를 선택
+ value: string;
+ onChange: (value: string) => void;
onValidationChange?: (isValid: boolean) => void;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| interface TextEditorProps { | |
| eventState?: FunnelState['eventState']; | |
| setEventState?: React.Dispatch<React.SetStateAction<FunnelState['eventState']>>; | |
| value?: string; | |
| onChange?: (value: string) => void; | |
| onValidationChange?: (isValid: boolean) => void; | |
| } | |
| interface TextEditorProps { | |
| // 제어형 패턴 또는 비제어형 패턴 중 하나를 선택 | |
| value: string; | |
| onChange: (value: string) => void; | |
| onValidationChange?: (isValid: boolean) => void; | |
| } |
🤖 Prompt for AI Agents
In src/features/event/ui/TextEditor.tsx lines 7 to 13, the TextEditorProps
interface mixes controlled (value/onChange) and uncontrolled
(eventState/setEventState) patterns, causing ambiguity. Refactor the props to
clearly separate or choose one pattern, ensuring only one control method is used
at a time. Define which props take precedence or create distinct interfaces for
controlled and uncontrolled usage to avoid confusion.
| const handleChange = (val: string) => { | ||
| const totalLength = getTotalContentLength(val); | ||
|
|
||
| if (totalLength <= MAX_LENGTH) { | ||
| setEventState?.(prev => ({ ...prev, description: value })); | ||
| onValidationChange?.(getPlainText(value).length > 0); | ||
| setEditorContent(val); | ||
| onChange?.(val); | ||
| setEventState?.(prev => ({ ...prev, description: val })); | ||
| onValidationChange?.(getPlainText(val).length > 0); | ||
| setIsOverLimit(false); | ||
| } else { | ||
| const editorInstance = quillRef.current?.getEditor(); | ||
| if (editorInstance) { | ||
| editorInstance.setContents(editorInstance.clipboard.convert(eventState.description)); | ||
| editorInstance.setContents(editorInstance.clipboard.convert(eventState?.description)); | ||
| } | ||
| setIsOverLimit(true); | ||
| } | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
handleChange 함수의 복잡성을 줄여야 합니다.
현재 함수는 로컬 상태와 외부 상태를 동시에 업데이트하여 상태 불일치 가능성이 있습니다. 또한 오류 시 fallback 로직에서 eventState?.description을 사용하는데, value prop을 통해 제어되는 경우 적절하지 않습니다.
const handleChange = (val: string) => {
const totalLength = getTotalContentLength(val);
if (totalLength <= MAX_LENGTH) {
setEditorContent(val);
onChange?.(val);
- setEventState?.(prev => ({ ...prev, description: val }));
onValidationChange?.(getPlainText(val).length > 0);
setIsOverLimit(false);
} else {
const editorInstance = quillRef.current?.getEditor();
if (editorInstance) {
- editorInstance.setContents(editorInstance.clipboard.convert(eventState?.description));
+ // 현재 유효한 내용으로 되돌리기
+ editorInstance.setContents(editorInstance.clipboard.convert(editorContent));
}
setIsOverLimit(true);
}
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const handleChange = (val: string) => { | |
| const totalLength = getTotalContentLength(val); | |
| if (totalLength <= MAX_LENGTH) { | |
| setEventState?.(prev => ({ ...prev, description: value })); | |
| onValidationChange?.(getPlainText(value).length > 0); | |
| setEditorContent(val); | |
| onChange?.(val); | |
| setEventState?.(prev => ({ ...prev, description: val })); | |
| onValidationChange?.(getPlainText(val).length > 0); | |
| setIsOverLimit(false); | |
| } else { | |
| const editorInstance = quillRef.current?.getEditor(); | |
| if (editorInstance) { | |
| editorInstance.setContents(editorInstance.clipboard.convert(eventState.description)); | |
| editorInstance.setContents(editorInstance.clipboard.convert(eventState?.description)); | |
| } | |
| setIsOverLimit(true); | |
| } | |
| }; | |
| const handleChange = (val: string) => { | |
| const totalLength = getTotalContentLength(val); | |
| if (totalLength <= MAX_LENGTH) { | |
| setEditorContent(val); | |
| onChange?.(val); | |
| onValidationChange?.(getPlainText(val).length > 0); | |
| setIsOverLimit(false); | |
| } else { | |
| const editorInstance = quillRef.current?.getEditor(); | |
| if (editorInstance) { | |
| // 현재 유효한 내용으로 되돌리기 | |
| editorInstance.setContents( | |
| editorInstance.clipboard.convert(editorContent) | |
| ); | |
| } | |
| setIsOverLimit(true); | |
| } | |
| }; |
🤖 Prompt for AI Agents
In src/features/event/ui/TextEditor.tsx around lines 89 to 105, the handleChange
function updates both local and external states, risking state inconsistencies,
and uses eventState?.description in fallback which is inappropriate if value
prop controls the content. Refactor handleChange to separate concerns by
updating only one source of truth for the editor content, preferably relying on
the value prop or a single state source, and adjust the fallback logic to use
the current valid content from props or state instead of eventState?.description
to ensure consistency and reduce complexity.
문제 정의
사용자가 접근 시, 티켓 목록 이미지가 늦게 로딩되며 첫 화면이 완전히 보이기까지 지연이 발생함. 이로 인해 사용자 체감 성능 저하, Lighthouse 성능 점수 하락(LCP 지연)이 확인됨.
문제 원인 분석
Lighthouse 측정 결과:
-> Lighthouse의 진단 결과를 보면 가장 큰 문제는 “이미지”에 있었다.
해결 과정
유저가 이벤트 배너 이미지를 업로드하는 과정에서 이미지 형식을 webP로 바꿔서 업로드되도록 하는
convertImageToWebP을 구현하여 이미지 성능 개선결과
Summary by CodeRabbit
신규 기능
버그 수정
환경 설정