From 3661b469cd8b16d31e5e99d50f5a84d70011192f Mon Sep 17 00:00:00 2001 From: syc0123 Date: Tue, 31 Mar 2026 16:17:30 +0900 Subject: [PATCH] =?UTF-8?q?[RAPID-fix]=20=EC=B1=84=ED=8C=85=EB=B0=A9=20?= =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EC=A0=90=ED=94=84=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0:=20useEffect=20=E2=86=92=20useLayoutEffect=20(?= =?UTF-8?q?=ED=8E=98=EC=9D=B8=ED=8A=B8=20=EC=A0=84=20=EC=B2=98=EB=A6=AC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [RAPID-fix] 스크롤 sentinel 방식으로 교체: scrollIntoView useLayoutEffect (페인트 전 보장) [RAPID-fix] 스크롤 근본 원인 수정: isOpen deps 추가로 메신저 열릴 때마다 하단 스크롤 [RAPID-fix] 스크롤 ResizeObserver 추가: 이미지 로드 후 높이 변화 감지해 자동 하단 스크롤 --- frontend/components/messenger/ChatPanel.tsx | 52 ++++++++++++--------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/frontend/components/messenger/ChatPanel.tsx b/frontend/components/messenger/ChatPanel.tsx index c3119ddc..b223ba3a 100644 --- a/frontend/components/messenger/ChatPanel.tsx +++ b/frontend/components/messenger/ChatPanel.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useEffect, useRef, useState } from "react"; +import React, { useEffect, useLayoutEffect, useRef, useState } from "react"; import { MessageSquare, Pencil, Check, X, ChevronsDown } from "lucide-react"; import { useMessages, useMarkAsRead, useUpdateRoom } from "@/hooks/useMessenger"; import { useAuth } from "@/hooks/useAuth"; @@ -16,14 +16,14 @@ interface ChatPanelProps { export function ChatPanel({ room }: ChatPanelProps) { const { user } = useAuth(); - const { selectedRoomId } = useMessengerContext(); + const { selectedRoomId, isOpen } = useMessengerContext(); const { data: messages } = useMessages(selectedRoomId); const markAsRead = useMarkAsRead(); const updateRoom = useUpdateRoom(); const { emitTypingStart, emitTypingStop, typingUsers } = useMessengerSocket(); const scrollRef = useRef(null); const [isAtBottom, setIsAtBottom] = useState(true); - const [scrollReady, setScrollReady] = useState(false); + const bottomRef = useRef(null); const [isEditingName, setIsEditingName] = useState(false); const [editName, setEditName] = useState(""); const editInputRef = useRef(null); @@ -36,34 +36,41 @@ export function ChatPanel({ room }: ChatPanelProps) { const lastMessageId = messages?.[messages.length - 1]?.id; - // Hide messages until scrolled to bottom (prevents visible jump on room open) - useEffect(() => { - setScrollReady(false); - }, [selectedRoomId]); + // Scroll to bottom: sentinel scrollIntoView before paint (no visible jump) + // Scroll to bottom on room open / new message + useLayoutEffect(() => { + if (isOpen) bottomRef.current?.scrollIntoView(); + }, [selectedRoomId, lastMessageId, isOpen]); - // Scroll to bottom when messages load or room changes - // Two-pass: immediate rAF + 600ms delayed (for async image loads) + // ResizeObserver: re-scroll whenever content height changes (images loading, etc.) + const shouldAutoScrollRef = useRef(true); useEffect(() => { - requestAnimationFrame(() => { - const el = scrollRef.current; - if (el) { - el.scrollTop = el.scrollHeight; - setScrollReady(true); + const el = scrollRef.current; + if (!el) return; + const inner = el.firstElementChild as HTMLElement | null; + if (!inner) return; + const ro = new ResizeObserver(() => { + if (shouldAutoScrollRef.current) { + bottomRef.current?.scrollIntoView(); } }); - const t = setTimeout(() => { - const el = scrollRef.current; - if (el) el.scrollTop = el.scrollHeight; - }, 600); - return () => clearTimeout(t); - }, [lastMessageId, selectedRoomId]); + ro.observe(inner); + return () => ro.disconnect(); + }, [selectedRoomId]); + + // Track whether user has scrolled up (disable auto-scroll while reading old messages) + useEffect(() => { + shouldAutoScrollRef.current = true; + }, [selectedRoomId, lastMessageId]); // Re-attach scroll listener whenever room changes (scrollRef mounts after room is set) useEffect(() => { const el = scrollRef.current; if (!el) return; const onScroll = () => { - setIsAtBottom(el.scrollHeight - el.scrollTop - el.clientHeight < 60); + const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 60; + setIsAtBottom(atBottom); + shouldAutoScrollRef.current = atBottom; }; el.addEventListener("scroll", onScroll); return () => el.removeEventListener("scroll", onScroll); @@ -170,7 +177,7 @@ export function ChatPanel({ room }: ChatPanelProps) { {/* Messages */} -
+
{messages?.map((msg, idx) => (
@@ -199,6 +206,7 @@ export function ChatPanel({ room }: ChatPanelProps) {
{roomTyping && roomTyping.length > 0 ? `${roomTyping.join(", ")}님이 입력 중...` : ""}
+