diff --git a/frontend/components/messenger/ChatPanel.tsx b/frontend/components/messenger/ChatPanel.tsx index df6ca66a..784f6561 100644 --- a/frontend/components/messenger/ChatPanel.tsx +++ b/frontend/components/messenger/ChatPanel.tsx @@ -23,7 +23,7 @@ export function ChatPanel({ room }: ChatPanelProps) { 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,22 +36,14 @@ export function ChatPanel({ room }: ChatPanelProps) { const lastMessageId = messages?.[messages.length - 1]?.id; - // useLayoutEffect: fires before browser paint → hide + scroll synchronously + // Scroll to bottom: sentinel scrollIntoView before paint (no visible jump) useLayoutEffect(() => { - const el = scrollRef.current; - if (!el) return; - setScrollReady(false); - el.scrollTop = el.scrollHeight; - // Reveal after scroll is applied - requestAnimationFrame(() => setScrollReady(true)); + bottomRef.current?.scrollIntoView(); }, [selectedRoomId, lastMessageId]); - // Second pass: re-scroll after async images load + // Second pass for async image loads useEffect(() => { - const t = setTimeout(() => { - const el = scrollRef.current; - if (el) el.scrollTop = el.scrollHeight; - }, 600); + const t = setTimeout(() => { bottomRef.current?.scrollIntoView(); }, 600); return () => clearTimeout(t); }, [lastMessageId, selectedRoomId]); @@ -167,7 +159,7 @@ export function ChatPanel({ room }: ChatPanelProps) { {/* Messages */} -
+
{messages?.map((msg, idx) => (
@@ -196,6 +188,7 @@ export function ChatPanel({ room }: ChatPanelProps) {
{roomTyping && roomTyping.length > 0 ? `${roomTyping.join(", ")}님이 입력 중...` : ""}
+