Conversation
WalkthroughStorybook 및 Next.js에 SVGR 로더와 타입을 추가해 SVG를 React 컴포넌트로 임포트 가능하게 구성하고, Toast 시스템(타입·Provider·훅·스택 로직·컴포넌트·스타일·애니메이션·스토리)을 신규 도입하며 프리뷰 데코레이터로 Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant App
participant ToastProvider
participant Context as ToastContext
participant Toaster
participant Toast
participant DOM as #toast-portal
App->>ToastProvider: 렌더(옵션)
ToastProvider->>DOM: 포털 노드 준비(존재 확인/생성)
User->>App: 버튼 클릭 -> useToast().add({...})
App->>Context: add(toastOptions)
Context-->>ToastProvider: 새 토스트 요청
ToastProvider->>ToastProvider: id 생성 및 옵션 병합
ToastProvider->>Toaster: toasts 전달
Toaster->>Toast: 위치별로 Toast 렌더
Toast->>Toast: enter 애니메이션 시작
alt duration > 0
Toast->>ToastProvider: 타이머 만료 -> remove(id)
ToastProvider->>Toaster: toasts 갱신
end
opt 사용자가 액션 클릭
User->>Toast: onAction()
Toast->>ToastProvider: remove(id)
end
sequenceDiagram
autonumber
participant Dev as 개발자/스토리북
participant SB as Storybook/Next
participant Rule as SVGR Loader (@svgr/webpack)
participant App as 코드
Dev->>SB: import Icon from "./icon.svg"
SB->>Rule: .svg 파일 매칭
Rule-->>SB: .svg → React 컴포넌트 모듈 변환
SB->>App: Icon 컴포넌트 사용 가능
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~30 minutes Suggested labels
Suggested reviewers
Pre-merge checks and finishing touches✅ Passed checks (5 passed)
✨ Finishing touches
🧪 Generate 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. Comment |
|
📚 Storybook is ready for review! |
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (11)
apps/web/package.json (1)
40-40: @svgr/webpack 최신 버전 및 보안 권고
- 현재 8.1.0이 공식 최신 버전이며, 자체 취약점은 보고되지 않았습니다.
- 다만 loader-utils(CVE-2022-37601), webpack-dev-middleware(CVE-2024-29180) 등 transitive 의존성에 취약점이 있을 수 있으니, 프로젝트 루트에서
npm audit및npm audit fix를 실행해 전체 의존성을 점검해 주세요.apps/web/src/shared/components/Toast/Toast.stories.tsx (3)
2-2: 미사용 임포트 제거 필요
Button컴포넌트가 임포트되었으나 스토리 내에서 사용되지 않습니다.다음 diff를 적용하여 미사용 임포트를 제거하세요:
-import { Button } from "../Button"; import { Toast } from "./Toast";
78-78: Console.log를 실제 액션으로 교체 권장Storybook 스토리에서
console.log를 사용 중입니다. Storybook의 actions addon을 활용하면 더 나은 디버깅 경험을 제공할 수 있습니다.다음과 같이 개선할 수 있습니다:
먼저 파일 상단에 actions를 임포트하세요:
import type { Meta, StoryObj } from "@storybook/nextjs-vite"; +import { action } from "@storybook/addon-actions";그리고 console.log를 action으로 교체하세요:
add({ width: "326px", action: "icon-link", - onAction: () => console.log("link click"), + onAction: action("link-clicked"), type: "warning", message: "토스트팝업 메시지 두줄유형 토스트팝업 메시지 두줄유형 최대글씨는 이만큼", position: "bottom-left", });
46-47: ESLint 비활성화 사유 명시 권장
eslint-disable주석이 명확한 사유 없이 사용되고 있습니다.다음과 같이 구체적인 규칙과 사유를 명시하는 것을 권장합니다:
- // eslint-disable-next-line + // eslint-disable-next-line react-hooks/rules-of-hooks -- useToast hook is called in render function for Storybook demo const { add } = useToast();apps/web/src/shared/components/Toast/useToastStack.ts (1)
45-52: Ref 콜백의 반복 실행으로 인한 성능 저하 가능성현재 ref 콜백은 매 렌더마다 실행되어
getBoundingClientRect()를 호출합니다. 토스트가 많아질 경우 불필요한 계산이 반복될 수 있습니다.
ResizeObserver를 활용하면 실제 크기 변경 시에만 높이를 업데이트할 수 있습니다:+import { useCallback, useEffect, useMemo, useRef, useState } from "react"; export const useToastStack = (toasts: ToastType[]) => { const [heights, setHeights] = useState<Record<string, number>>({}); + const observerRef = useRef<ResizeObserver>(); + useEffect(() => { + observerRef.current = new ResizeObserver((entries) => { + entries.forEach((entry) => { + const id = entry.target.getAttribute('data-toast-id'); + if (id) { + const height = entry.contentRect.height; + setHeights(prev => { + if (prev[id] !== height) { + return { ...prev, [id]: height }; + } + return prev; + }); + } + }); + }); + + return () => observerRef.current?.disconnect(); + }, []); const getToasterProps = useCallback( (toast: ToastType, index: number, position: ToastPosition) => { // ... offset calculation ... return { ref: (el: HTMLDivElement | null) => { if (el) { - const height = el.getBoundingClientRect().height; - if (heights[toast.id] !== height) { - updateHeight(toast.id, height); - } + el.setAttribute('data-toast-id', toast.id); + observerRef.current?.observe(el); + } else { + // Cleanup when element unmounts + const existing = document.querySelector(`[data-toast-id="${toast.id}"]`); + if (existing) observerRef.current?.unobserve(existing); } }, // ... rest of props ... }; }, - [heights, groupedToasts, updateHeight] + [groupedToasts] );apps/web/src/shared/components/Toast/ToastProvider.tsx (2)
45-45: 더 안전한 ID 생성 방법 권장
Math.random().toString(36).substring(2, 9)로 ID를 생성하고 있습니다. 충돌 확률은 낮지만, 고유성 보장을 위해 더 견고한 방법을 권장합니다.다음 중 하나를 선택할 수 있습니다:
방법 1: crypto API 사용 (권장)
const id = Math.random().toString(36).substring(2, 9); + const id = crypto.randomUUID();방법 2: 타임스탬프 + 카운터 조합
파일 상단에 카운터 추가:
+let toastCounter = 0; + export const ToastProvider = ({ options, children }: ToastProviderProps) => {그리고 ID 생성 시:
- const id = Math.random().toString(36).substring(2, 9); + const id = `toast-${Date.now()}-${++toastCounter}`;
23-33: 포털 노드 생성 시 기존 노드 재사용 확인포털 노드가 이미 존재하는 경우 재사용하고 있으나, 여러
ToastProvider인스턴스가 동시에 마운트될 경우 경쟁 조건이 발생할 수 있습니다.현재 구현은 단일 Provider 사용을 가정하므로 대부분의 경우 문제없으나, 명확성을 위해 주석을 추가하거나 싱글톤 패턴을 고려할 수 있습니다:
useEffect(() => { let node = document.getElementById("toast-portal"); if (!node) { + // Create portal node only if it doesn't exist + // Note: Multiple ToastProviders should not be used simultaneously node = document.createElement("div"); node.id = "toast-portal"; document.body.appendChild(node); } setPortalNode(node); }, []);apps/web/src/shared/components/Toast/Toast.tsx (1)
60-65: duration - 500 매직 넘버 상수화 권장
duration - 500계산에서 500ms는 애니메이션 지속 시간과 연관된 것으로 보이나, 매직 넘버로 하드코딩되어 있습니다.상수로 추출하여 의도를 명확히 하세요:
+/** Animation duration for toast hide transition (ms) */ +const HIDE_ANIMATION_DURATION = 500; + export const Toast = ({ ...toast }: ToastProps) => { // ... useEffect(() => { // ... const timer = setTimeout( () => { setVisible(false); }, - duration ? duration - 500 : 300 + duration ? duration - HIDE_ANIMATION_DURATION : 300 ); return () => clearTimeout(timer); }, [isMounted, duration]);apps/web/src/shared/components/Toast/Toast.stlye.ts (3)
16-17: 디자인 토큰 사용을 권장합니다.배경색(
#3D3D3D)과 텍스트 색상(#FFFFFF)이 하드코딩되어 있습니다. 일관성과 유지보수성을 위해 디자인 시스템의 토큰을 사용하는 것을 권장합니다.예시:
- background: "#3D3D3D", - color: "#FFFFFF", + background: "colors.gray.800", + color: "colors.white",
104-104: 액션 버튼 색상도 토큰화를 권장합니다.
#FF8A00색상이 하드코딩되어 있습니다. 브랜드 컬러나 액션 컬러를 디자인 토큰으로 관리하면 테마 변경 시 유리합니다.예시:
- backgroundColor: "#FF8A00", + backgroundColor: "colors.brand.primary",
114-114: z-index 값 검토를 권장합니다.
z-index: 9999는 매우 높은 값입니다. 다른 오버레이 요소(모달, 드롭다운 등)와의 충돌 가능성을 확인하고, 가능하다면 z-index 스케일을 디자인 시스템에 정의하여 사용하는 것을 권장합니다.프로젝트에 z-index 스케일이 정의되어 있는지 확인해 주세요. 예를 들어:
// design-system/z-index.ts export const zIndex = { modal: 1000, toast: 1100, tooltip: 1200, }
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (4)
apps/web/src/shared/components/Toast/assets/toast-error.svgis excluded by!**/*.svgapps/web/src/shared/components/Toast/assets/toast-success.svgis excluded by!**/*.svgapps/web/src/shared/components/Toast/assets/toast-warning.svgis excluded by!**/*.svgpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (17)
apps/web/.storybook/main.ts(1 hunks)apps/web/.storybook/preview.tsx(2 hunks)apps/web/next.config.js(1 hunks)apps/web/package.json(1 hunks)apps/web/src/shared/components/Toast/Toast.Animation.css(1 hunks)apps/web/src/shared/components/Toast/Toast.stlye.ts(1 hunks)apps/web/src/shared/components/Toast/Toast.stories.tsx(1 hunks)apps/web/src/shared/components/Toast/Toast.tsx(1 hunks)apps/web/src/shared/components/Toast/Toast.type.ts(1 hunks)apps/web/src/shared/components/Toast/ToastProvider.tsx(1 hunks)apps/web/src/shared/components/Toast/index.ts(1 hunks)apps/web/src/shared/components/Toast/useToast.ts(1 hunks)apps/web/src/shared/components/Toast/useToastStack.ts(1 hunks)apps/web/src/shared/hooks/useIsMounted.ts(1 hunks)apps/web/src/shared/utils/SwitchCase.tsx(1 hunks)apps/web/svgr.d.ts(1 hunks)apps/web/tsconfig.json(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
apps/web/src/shared/components/Toast/useToast.ts (2)
apps/web/src/shared/components/Toast/Toast.type.ts (1)
ToastContextValue(31-34)apps/web/src/shared/components/Toast/ToastProvider.tsx (1)
ToastContext(17-17)
apps/web/src/shared/components/Toast/useToastStack.ts (1)
apps/web/src/shared/components/Toast/Toast.type.ts (2)
ToastType(22-24)ToastPosition(4-4)
apps/web/src/shared/components/Toast/Toast.tsx (4)
apps/web/src/shared/components/Toast/Toast.type.ts (3)
ToastType(22-24)ToastPosition(4-4)ToastProps(36-38)apps/web/src/shared/components/Toast/useToastStack.ts (1)
useToastStack(8-67)apps/web/src/shared/hooks/useIsMounted.ts (1)
useIsMounted(5-13)apps/web/src/shared/utils/SwitchCase.tsx (1)
SwitchCase(9-15)
apps/web/src/shared/components/Toast/Toast.stories.tsx (3)
apps/web/src/shared/components/Toast/Toast.tsx (1)
Toast(39-117)apps/web/src/shared/components/Toast/ToastProvider.tsx (1)
ToastProvider(40-88)apps/web/src/shared/components/Toast/useToast.ts (1)
useToast(5-13)
apps/web/src/shared/components/Toast/ToastProvider.tsx (3)
apps/web/src/shared/components/Toast/Toast.type.ts (4)
ToastOptions(6-20)ToastContextValue(31-34)ToastProviderProps(26-29)ToastType(22-24)apps/web/src/shared/hooks/useIsMounted.ts (1)
useIsMounted(5-13)apps/web/src/shared/components/Toast/Toast.tsx (1)
Toaster(14-37)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Deploy Storybook to Chromatic
🔇 Additional comments (12)
apps/web/src/shared/utils/SwitchCase.tsx (1)
9-15: 구현이 깔끔하고 타입 안전합니다.제네릭 타입 제약과 nullish coalescing 연산자를 적절히 활용했습니다. Line 10의
==비교는 null과 undefined를 모두 처리하는 관용적인 패턴입니다.apps/web/src/shared/components/Toast/useToast.ts (1)
5-13: 표준 React 컨텍스트 훅 패턴을 올바르게 구현했습니다.Provider 누락 시 명확한 에러 메시지를 제공하고, 타입 안전성도 보장됩니다.
apps/web/.storybook/main.ts (2)
40-43: SVGR webpack 규칙이 올바르게 추가되었습니다.Next.js 설정과 일관되게 SVG를 React 컴포넌트로 처리하는 규칙을 추가했습니다.
27-36: SVG 규칙 탐색 로직에 대한 방어 로직 추가 필요
config.module.rules.find(rule => rule.test.test(".svg"))가 Storybook 내부 webpack 설정 구조 변경 시 undefined를 반환할 수 있으므로,imageRule이 없을 때를 대비해 방어 코드를 추가해 주세요.
[apps/web/.storybook/main.ts:27-36]apps/web/src/shared/components/Toast/index.ts (1)
1-2: Toast 모듈의 공개 API를 명확하게 정의했습니다.배럴 export 패턴을 사용하여 깔끔한 import 경로를 제공합니다.
apps/web/tsconfig.json (1)
21-22: SVGR 통합을 위한 적절한 TypeScript 설정입니다.SVG 파일을 모듈로 인식하고 타입 선언을 포함하도록 하는 설정이 올바르게 추가되었습니다.
apps/web/.storybook/preview.tsx (1)
21-31: Toast 포털을 위한 적절한 Storybook 데코레이터입니다.스토리 렌더링 전에 toast-portal DOM 요소가 존재하도록 보장하는 로직이 올바르게 구현되었습니다. Toast 컴포넌트를 Storybook에서 안전하게 사용할 수 있습니다.
apps/web/src/shared/hooks/useIsMounted.ts (1)
5-13: useIsMounted 훅이 올바르게 구현되었습니다.마운트 상태를 추적하는 로직이 정확합니다. 이 훅은 주로 포털 렌더링 가드용으로 사용되며, 현재 Toast 시스템에서 적절하게 활용되고 있습니다.
참고: 이 패턴을 비동기 작업 후 상태 업데이트를 막는 용도로 사용하는 경우, effect cleanup 함수에서 취소 로직을 구현하는 것이 더 권장됩니다.
apps/web/src/shared/components/Toast/Toast.tsx (1)
14-37: Toaster 컴포넌트 로직 확인 완료토스트를 포지션별로 그룹화하여 렌더링하는 로직이 올바르게 구현되어 있습니다.
useToastStack훅과의 연동도 적절합니다.apps/web/src/shared/components/Toast/Toast.stlye.ts (1)
28-31: 접근성 고려가 잘 되어 있습니다.
prefers-reduced-motion: reduce미디어 쿼리를 통해 애니메이션 접근성을 적절히 처리하고 있습니다.Also applies to: 48-51
apps/web/src/shared/components/Toast/Toast.type.ts (2)
1-38: 타입 정의가 명확하고 포괄적입니다.오타 수정 후에는 Toast API가 잘 구조화되어 있습니다. ToastOptions의 모든 필수/선택 속성이 명확히 정의되어 있고, Context와 Provider 타입도 적절합니다.
9-9: 프로퍼티 이름 오타 수정 및 사용처 검증 요청
Toast.type.ts에서offest를offset으로 수정해주세요.수정 후, 아래 스크립트를 실행해 오타 사용처가 모두 제거되었는지 검증하세요:
#!/bin/bash # Toast 관련 파일에서 'offest' 오타 사용처를 찾습니다. rg -n 'offest' apps/web/src/shared/components/Toast
| turbopack: { | ||
| rules: { | ||
| '*.svg': { | ||
| loaders: ['@svgr/webpack'], | ||
| as: '*.js' | ||
| } | ||
| } | ||
| } |
| <button type="button" onClick={handleAddTopLeftToast}> | ||
| TOP LEFT | ||
| </button> | ||
| <button type="button" onClick={handleAddTopCenterToast}> | ||
| TOP CENTER | ||
| </button> | ||
| <button type="button" onClick={handleAddTopRightToast}> | ||
| TOP RIGHT | ||
| </button> | ||
| <button type="button" onClick={handleAddBottomLeftToast}> | ||
| BOTTOM LEFT | ||
| </button> | ||
| <button type="button" onClick={handleAddBottomCenterToast}> | ||
| BOTTOM CENTER | ||
| </button> | ||
| <button type="button" onClick={handleAddBottomRightToast}> | ||
| BOTTOM RIGHT | ||
| </button> |
|
|
||
| useEffect(() => { | ||
| if (isMounted) { | ||
| requestAnimationFrame(() => { |
There was a problem hiding this comment.
Cleanup을 추가해줄 수 있을 것 같습니다!
https://developer.mozilla.org/ko/docs/Web/API/Window/cancelAnimationFrame
|
📚 Storybook is ready for review! |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
apps/web/src/shared/components/Toast/Toast.tsx (1)
103-104: 하드코딩된 텍스트 확인 필요"링크→" 텍스트가 하드코딩되어 있습니다. 프로젝트에 i18n이 적용되어 있다면 번역 가능하도록 처리하는 것을 고려하세요.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
apps/web/src/shared/components/Toast/Toast.Animation.css(1 hunks)apps/web/src/shared/components/Toast/Toast.stories.tsx(1 hunks)apps/web/src/shared/components/Toast/Toast.tsx(1 hunks)apps/web/src/shared/components/Toast/Toast.type.ts(1 hunks)apps/web/src/shared/components/Toast/ToastProvider.tsx(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- apps/web/src/shared/components/Toast/ToastProvider.tsx
- apps/web/src/shared/components/Toast/Toast.Animation.css
🧰 Additional context used
🧬 Code graph analysis (2)
apps/web/src/shared/components/Toast/Toast.tsx (4)
apps/web/src/shared/components/Toast/Toast.type.ts (3)
ToastType(22-24)ToastPosition(4-4)ToastProps(36-38)apps/web/src/shared/components/Toast/useToastStack.ts (1)
useToastStack(8-67)apps/web/src/shared/hooks/useIsMounted.ts (1)
useIsMounted(5-13)apps/web/src/shared/utils/SwitchCase.tsx (1)
SwitchCase(9-15)
apps/web/src/shared/components/Toast/Toast.stories.tsx (3)
apps/web/src/shared/components/Toast/Toast.tsx (1)
Toast(39-117)apps/web/src/shared/components/Toast/ToastProvider.tsx (1)
ToastProvider(40-88)apps/web/src/shared/components/Toast/useToast.ts (1)
useToast(5-13)
🔇 Additional comments (7)
apps/web/src/shared/components/Toast/Toast.type.ts (1)
1-38: 타입 정의가 잘 구성되어 있습니다.이전 리뷰에서 지적된
"defualt"오타가"default"로 수정되었습니다. 전체 타입 정의가 명확하고 완전하며, 선택적 필드들도 적절하게 표시되어 있습니다.apps/web/src/shared/components/Toast/Toast.stories.tsx (3)
46-47: Storybook 컨텍스트에서 훅 사용은 적절합니다.렌더 함수 내에서
useToast를 사용하는 것은 Storybook의 특수한 컨텍스트에서 정상 동작하며, eslint-disable 주석으로 의도를 명확히 했습니다.
78-78: Storybook 데모용 console.log는 허용 가능합니다.
onAction콜백의console.log는 Storybook 스토리에서 액션 동작을 확인하는 데모 목적으로 적절합니다. 프로덕션 코드가 아니므로 문제없습니다.
41-130: 6가지 위치와 다양한 조합을 테스트하는 포괄적인 스토리입니다.모든 Toast 위치(top/bottom × left/center/right)와 타입(default, success, warning, error), 액션(icon-link, icon-button) 조합을 잘 커버하고 있습니다.
apps/web/src/shared/components/Toast/Toast.tsx (3)
14-37: Toaster 컴포넌트 구현이 잘 되어 있습니다.
useToastStack을 사용하여 위치별로 그룹화된 토스트를 렌더링하는 로직이 명확하고, 적절한 키와 props 전달이 이루어지고 있습니다.
40-40: 이전 리뷰의 오타가 수정되었습니다.
offest→offset으로 올바르게 수정되었습니다.
86-94: SwitchCase를 통한 아이콘 렌더링이 잘 구성되어 있습니다.타입별 아이콘을 조건부로 렌더링하는 로직이 명확하고 유지보수하기 좋습니다.
|
📚 Storybook is ready for review! |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
apps/web/src/shared/components/Toast/Toast.tsx (1)
104-119: 액션 버튼 렌더링 검증 필요
ToastOptions에서buttonText는 optional이므로action === 'icon-button'일 때 값이 없으면 빈 버튼이 렌더링됩니다. 기본값 또는 fallback 텍스트를 처리하세요.- 하드코딩된
"링크→"텍스트는 i18n 지원을 위해 리소스 파일 등으로 분리하는 것을 권장합니다.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
apps/web/src/shared/components/Toast/Toast.tsx(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/src/shared/components/Toast/Toast.tsx (4)
apps/web/src/shared/components/Toast/Toast.type.ts (3)
ToastType(22-24)ToastPosition(4-4)ToastProps(36-38)apps/web/src/shared/components/Toast/useToastStack.ts (1)
useToastStack(8-67)apps/web/src/shared/hooks/useIsMounted.ts (1)
useIsMounted(5-13)apps/web/src/shared/utils/SwitchCase.tsx (1)
SwitchCase(9-15)
🔇 Additional comments (6)
apps/web/src/shared/components/Toast/Toast.tsx (6)
1-12: LGTM! 임포트 구조가 명확합니다."use client" 지시어와 필요한 훅, 유틸리티, 타입, SVG 아이콘, 스타일 임포트가 적절하게 구성되어 있습니다.
14-37: LGTM! Toaster 컴포넌트 구현이 적절합니다.포지션별로 토스트를 그룹화하여 렌더링하는 로직이 명확하며,
useToastStack훅을 통해 스택 위치와 스타일을 올바르게 적용하고 있습니다.
39-46: LGTM! Props 및 상태 초기화가 적절합니다.
offset속성이 올바르게 사용되고 있으며(과거 오타 수정 완료),useIsMounted와visible상태를 통한 애니메이션 제어 로직이 명확합니다.
47-51: LGTM! offset 스타일 메모이제이션이 적절합니다.포지션에 따라
top또는bottom스타일을 올바르게 계산하고 있으며, 의존성 배열도 정확합니다.
75-88: LGTM! Toast 루트 렌더링 구조가 적절합니다.포지션 기반 클래스, visibility 제어, offset 스타일이 올바르게 적용되고 있습니다.
89-102: LGTM! 타입 아이콘 및 메시지 렌더링이 적절합니다.
SwitchCase를 사용하여 토스트 타입별 아이콘을 선택적으로 렌더링하는 구조가 명확하며, 메시지 표시 로직도 올바릅니다.
| useEffect(() => { | ||
| const animation = isMounted | ||
| ? requestAnimationFrame(() => { | ||
| setVisible(true); | ||
| }) | ||
| : null; | ||
|
|
||
| const timer = setTimeout( | ||
| () => { | ||
| setVisible(false); | ||
| }, | ||
| duration ? duration - 500 : 300 | ||
| ); | ||
|
|
||
| return () => { | ||
| if (animation !== null) { | ||
| cancelAnimationFrame(animation); | ||
| } | ||
| clearTimeout(timer); | ||
| }; | ||
| }, [isMounted, duration]); |
There was a problem hiding this comment.
useEffect 의존성 배열로 인한 중복 실행 문제 미해결
과거 리뷰 코멘트에서 지적된 문제가 부분적으로만 해결되었습니다:
- ✅
cancelAnimationFrame클린업이 추가됨 - ❌
isMounted가 여전히 의존성 배열에 포함되어,false → true변경 시 effect가 재실행되며 타이머가 중복 생성됨 - ❌
useRef미사용으로 인해 타이머 ID 추적이 불완전함
현재 플로우:
- 초기 렌더:
isMounted=false, animation=null, timer 생성 - 마운트 후:
isMounted=true, effect 재실행 → 기존 timer 클린업 후 새 animation + timer 생성
이로 인해 의도하지 않은 타이밍 동작이 발생할 수 있습니다.
과거 리뷰 권장사항대로 다음과 같이 수정하세요:
-import { Fragment, useEffect, useMemo, useState } from "react";
+import { Fragment, useEffect, useMemo, useRef, useState } from "react"; export const Toast = ({ ...toast }: ToastProps) => {
const { width, type, action, position, offset, duration, style, message, buttonText, onAction } = toast;
const isMounted = useIsMounted();
const [visible, setVisible] = useState(false);
+ const rafRef = useRef<number | null>(null);
+ const timerRef = useRef<NodeJS.Timeout | null>(null);
const isTop = position?.includes("top");
const positionClass = isTop ? "top" : "bottom";
// ... offsetStyle ...
useEffect(() => {
- const animation = isMounted
- ? requestAnimationFrame(() => {
- setVisible(true);
- })
- : null;
-
- const timer = setTimeout(
- () => {
- setVisible(false);
- },
- duration ? duration - 500 : 300
- );
+ if (!isMounted) return;
+
+ rafRef.current = requestAnimationFrame(() => {
+ setVisible(true);
+ });
return () => {
- if (animation !== null) {
- cancelAnimationFrame(animation);
+ if (rafRef.current !== null) {
+ cancelAnimationFrame(rafRef.current);
}
- clearTimeout(timer);
};
- }, [isMounted, duration]);
+ }, [isMounted]);
+
+ useEffect(() => {
+ if (!isMounted) return;
+
+ timerRef.current = setTimeout(
+ () => {
+ setVisible(false);
+ },
+ duration ? duration - 500 : 300
+ );
+
+ return () => {
+ if (timerRef.current !== null) {
+ clearTimeout(timerRef.current);
+ }
+ };
+ }, [duration, isMounted]);Based on learnings
📝 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.
| useEffect(() => { | |
| const animation = isMounted | |
| ? requestAnimationFrame(() => { | |
| setVisible(true); | |
| }) | |
| : null; | |
| const timer = setTimeout( | |
| () => { | |
| setVisible(false); | |
| }, | |
| duration ? duration - 500 : 300 | |
| ); | |
| return () => { | |
| if (animation !== null) { | |
| cancelAnimationFrame(animation); | |
| } | |
| clearTimeout(timer); | |
| }; | |
| }, [isMounted, duration]); | |
| import { Fragment, useEffect, useMemo, useRef, useState } from "react"; | |
| export const Toast = ({ ...toast }: ToastProps) => { | |
| const { width, type, action, position, offset, duration, style, message, buttonText, onAction } = toast; | |
| const isMounted = useIsMounted(); | |
| const [visible, setVisible] = useState(false); | |
| // Track requestAnimationFrame and timeout IDs for cleanup | |
| const rafRef = useRef<number | null>(null); | |
| const timerRef = useRef<NodeJS.Timeout | null>(null); | |
| const isTop = position?.includes("top"); | |
| const positionClass = isTop ? "top" : "bottom"; | |
| const offsetStyle = useMemo(() => { | |
| const offsetValue = offset; | |
| return isTop ? { top: `${offsetValue}px` } : { bottom: `${offsetValue}px` }; | |
| }, [isTop, offset]); | |
| // Only run once on mount to trigger the show animation | |
| useEffect(() => { | |
| if (!isMounted) return; | |
| rafRef.current = requestAnimationFrame(() => { | |
| setVisible(true); | |
| }); | |
| return () => { | |
| if (rafRef.current !== null) { | |
| cancelAnimationFrame(rafRef.current); | |
| } | |
| }; | |
| }, [isMounted]); | |
| // Separate effect to handle auto-dismiss timing | |
| useEffect(() => { | |
| if (!isMounted) return; | |
| timerRef.current = setTimeout( | |
| () => { | |
| setVisible(false); | |
| }, | |
| duration ? duration - 500 : 300 | |
| ); | |
| return () => { | |
| if (timerRef.current !== null) { | |
| clearTimeout(timerRef.current); | |
| } | |
| }; | |
| }, [duration, isMounted]); | |
| return ( | |
| <Fragment> | |
| {/* your toast JSX here */} | |
| </Fragment> | |
| ); | |
| }; |
📝 PR 유형
📝 PR 설명
Toast 컴포넌트 마이그레인 작업입니다.
추가적으로 next.js, storybook svgr 사용 작업이 포함되어있습니다.
관련된 이슈 넘버
close #54
✅ 작업 목록
MR하기 전에 확인해주세요
📚 논의사항
📚 ETC
Summary by CodeRabbit
신기능
문서
스타일
작업