μ±ν
λ°©μμλ μ€μκ°μΌλ‘ λ©μμ§κ° μ€κ°λ€.
μλ‘μ΄ λ©μμ§κ° λμ°©ν λ μ¬μ©μκ° λΆνΈν¨ μμ΄ λ΄μ©μ νμΈν μ μλλ‘ νκΈ° μν΄ μλ μ€ν¬λ‘€ κΈ°λ₯μ΄ νμνλ€.
μ²μμλ λ¨μν μλ‘μ΄ λ©μμ§κ° λμ°©νλ©΄ μ΅νλ¨μΌλ‘ μ΄λνλλ‘ κ΅¬ννλλ°,
μ¬μ©μκ° μ€ν¬λ‘€μ μ§μ μ¬λ €μ μ΄μ λ©μμ§λ₯Ό λ³΄κ³ μμ κ²½μ°μλ μλ‘μ΄ λ©μμ§κ° μ¬ κ²½μ°λ₯Ό κ³ λ €ν΄μΌνλ€λ μκ°μ΄ λ€μλ€...
κ·Έλμ μ±ν λ°©μ μ€ν¬λ‘€κ³Ό κ΄λ ¨ν΄μ μΌμ΄λ μ μλ κ²½μ°λ€μ λν΄ μκ°ν΄ 보μλ€.
π ꡬν μ κ³ λ €ν λΆλΆ
- μ΄κΈ° λ λλ§ μ κ°μ₯ μ΅μ λ©μμ§λ‘ μ΄λ
- μλ‘μ΄ λ©μμ§κ° λμ°©νλ©΄ μ¬μ©μκ° νλ¨μ μμ κ²½μ° μλ μ€ν¬λ‘€
- μ¬μ©μκ° μ€ν¬λ‘€μ μ‘°μνμ κ²½μ° μλ μ€ν¬λ‘€ μ€λ¨
- μλ μ€ν¬λ‘€μ΄ μ€λ¨λ κ²½μ° μ λ©μμ§μ λν μλ¦Ό νμ λ° μ¬μ©μκ° μλ¦Όμ ν΄λ¦ν κ²½μ° μλ μ€ν¬λ‘€
- 무ν μ€ν¬λ‘€μ νμ©νμ¬ μ΄μ λ©μμ§ λΆλ¬μ¬ λ νμ¬ μμΉ μ μ§
λ¨Όμ μ΄κΈ° λ λλ§, μ±ν
λͺ©λ‘ 컨ν
μ΄λ λ±μ κ°μ§νκΈ° μν΄ useRef
λ₯Ό νμ©νκ³ ,
μλ¨κ³Ό νλ¨ κ°μ§λ₯Ό μν΄ IntersectionObserver
λ₯Ό νμ©νλ€.
useEffect(() => {
// μ΄κΈ° λ λλ§μ΄ μλ κ²½μ° λ¦¬ν΄
if (!isInitial.current) return;
// μ±ν
λͺ©λ‘ 컨ν
μ΄λκ° μμ κ²½μ° λ¦¬ν΄
if (!chatContainerRef.current) return;
// μ±ν
λ©μΈμ§κ° μμ κ²½μ° λ¦¬ν΄
if (chatMsgs.length === 0) return;
chatContainerRef.current.scrollTo({
top: chatContainerRef.current.scrollHeight,
behavior: 'smooth',
});
// μ΄κΈ° λ λλ§μ΄ λλλ©΄ falseλ‘ μ€μ ν΄ λ€μ λΆν° μ΄ μ½λ λΈλ‘μ΄ μ€νλμ§ μλλ‘ νλ€.
isInitial.current = false;
}, [chatMsgs]);
μ²μμλ μμ‘΄μ± λ°°μ΄μ μ무κ²λ λ΄μ§ μμ μ΄κΈ° λ λλ§ μμλ§ μ€νλλλ‘ κ΅¬ννμλλ°,
μμΌ μ΄λ²€νΈμ λν μ²λ¦¬κ° μλ£λκΈ° μ μ μ€νλμ΄ μ λλ‘ μ€ν¬λ‘€ λμ§ μλ κ²½μ°κ° λ°μνλ€.
κ·Έλμ μμΌ μ΄λ²€νΈ μ²λ¦¬κ° μλ£λμ΄ μ±ν
λ©μμ§κ° μκΈ°λ©΄ μ€νλλλ‘ μμ‘΄μ± λ°°μ΄μ μ±ν
λ©μμ§λ₯Ό λ£μ΄μ£Όκ³ ,
μ΄κΈ° λ λλ§μΈμ§λ₯Ό ꡬλΆν μ μλ useRef
λ₯Ό λ°λ‘ μ€μ νλ€.
const lastChatRef = useCallback((node: HTMLDivElement) => {
if (lastObserverRef.current) lastObserverRef.current.disconnect();
lastObserverRef.current = new IntersectionObserver(
([entry]) => {
// νλ¨μ μ€ν¬λ‘€μ΄ λμ°©νλ©΄ μλ‘μ΄ λ©μμ§ μλ¦Ό UI μν false μ€μ
if (entry.isIntersecting) {
setIsNewMsg(false);
}
// κ΄μ°° μμ, μ¦ νλ¨μ λ§μ§λ§ μ±ν
λ©μΈμ§κ° νλ©΄μμ λ€μ΄μ€κ±°λ λκ° λ λ§λ€ μ€ν
// νλ©΄μ κ΄μ°° μμκ° μμΌλ©΄ true, μμΌλ©΄ false
setIsNearBottom(entry.isIntersecting);
},
{ threshold: 0.1 }
);
if (node) {
lastObserverRef.current.observe(node);
}
}, []);
β
entry.isIntersecting
: κ΄μ°° λμμ΄ λ·°ν¬νΈμ κ΅μ°¨ν λ λ§λ€ μ€νλλ€.
useEffect(() => {
if (!chatContainerRef.current) return;
// intersectionObserverλ₯Ό ν΅ν΄ νλ¨ μΈμ && μ λ©μμ§ μμ μ μ€μ ν stateκ° true μΌλ μλ μ€ν¬λ‘€
if (isNearBottom && isNewMsg) {
chatContainerRef.current.scrollTo({
top: chatContainerRef.current.scrollHeight,
behavior: 'smooth',
});
}
}, [chatMsgs]);
{!isNearBottom && newMsg && isNewMsg && (
<NewChatMsg
nickname={nickname}
// μ λ©μμ§ μμ μ΄λ²€νΈ λ°μ μ ν΄λΉ λ΄μ©μ μνλ‘ μ μ₯ν΄μ νμ©
message={newMsg}
chatContainerRef={chatContainerRef}
setNewMsg={setNewMsg}
/>
)}
// ν΄λΉ UIλ₯Ό ν΄λ¦ν κ²½μ° μ€νλλ ν¨μ
const handleNewMsg = () => {
if (!chatContainerRef.current) return;
// μ±ν
λ°© μ΅νλ¨μΌλ‘ μ€ν¬λ‘€ μ΄λ
const container = chatContainerRef.current;
container.scrollTo({
top: container.scrollHeight,
behavior: 'smooth',
});
// μ λ©μμ§ λ΄μ© μν nullλ‘ λ³κ²½
setNewMsg(null);
};
const handleScroll = useCallback(() => {
if (!chatContainerRef.current) return;
const { scrollTop } = chatContainerRef.current;
// μ΅μλ¨μ λΏκ³ , nextCursorκ° μμ κ²½μ° μ΄μ μ±ν
λΆλ¬μ€κΈ° μμ²
if (scrollTop === 0 && nextCursor) {
if (debounceTimeout.current) return;
debounceTimeout.current = setTimeout(() => {
socket.emit(CHAT.HISTORY.FETCH, {
roomId: chatRoomId,
cursor: nextCursor,
});
socket.on(CHAT.HISTORY.FETCHED, handleGetChatting);
setTimeout(() => {
debounceTimeout.current = null;
}, 100);
}, 300);
}
}, [chatRoomId, nextCursor]);
μ€ν¬λ‘€μ΄ μ΅μλ¨μ λΏμμ λ, nextCursor
λ₯Ό νμ©νμ¬ μ΄μ λ©μΈμ§λ₯Ό νΈμΆνλ€.
debounceTimeout
μΌλ‘ κ³Όλν μμ² λ°μμ λ°©μ§νλ€.
const handleGetChatting = ({ chatHistory, nextCursor }: ChatHistoryData) => {
// μ΄κΈ° λ λλ§μ΄ μλ κ²½μ°μλ§ νΈμΆ
if (!isInitial.current) {
// μ λ©μμ§ μνλ₯Ό falseλ‘ μ€μ νμ¬ νλ¨μΌλ‘ μ€ν¬λ‘€λλ κ² λ°©μ§
setIsNewMsg(false);
setChatMsgs(() => {
const currentMsgs = chatMsgsRef.current;
// κΈ°μ‘΄ λ©μμ§μ μ€λ³΅ μλ μμ΄λ λ°°μ΄ μμ±
const existingMsgIds = new Set(currentMsgs.map((msg) => msg.id));
// κΈ°μ‘΄ λ©μμ§μ μ€λ³΅λμ§ μμ λ©μμ§λ§ λ΄μ μ΄μ λ©μμ§ λ°°μ΄ μμ±
const filteredNewMsgs = chatHistory.filter(
(msg) => !existingMsgIds.has(msg.id)
);
// μ€λ³΅ μ κ±°λ μ΄μ λ©μμ§μ νμ¬ λ©μμ§λ₯Ό μ°¨λ‘λ‘ λ΄μ updatedMsgs λ°°μ΄μ μμ±νμ¬ λ°ν
const updatedMsgs = [...filteredNewMsgs, ...currentMsgs];
return updatedMsgs;
});
// λ€μ μ΄μ λ©μμ§ νΈμΆμ μν nextCursor μ¬μ€μ
setNextCursor(nextCursor ?? null);
// κΈ°μ‘΄ μ€ν¬λ‘€ μμΉλ‘ μ€ν¬λ‘€μ λ€μ λ΄λ €μ€λ€.
if (chatContainerRef.current) {
const currentScrollHeight = chatContainerRef.current.scrollHeight;
chatContainerRef.current.scrollTo({
top: currentScrollHeight * 0.057,
});
}
}
};
μ΄μ λ©μμ§λ₯Ό μμ²νμ λ,
κΈ°μ‘΄μ μ΄λ―Έ λ λλ§ λ μ±ν
λ©μμ§κ° μ€λ³΅μΌλ‘ μλ²μμ λμ΄μ€λ κ²½μ°κ° μλ κ²μ νμΈνλ€.
μ΄ λ©μμ§κ° λ€μ μ€λ³΅μΌλ‘ λ λλ§λλ κ²μ λ°©μ§νκΈ° μν΄
λ©μμ§μ μμ΄λλ‘ κ²μ¬λ₯Ό ν λ€ μ€λ³΅λ λ©μμ§λ μ κ±°νκ³ λ λλ§ ν΄μ€λ€.
μ±ν μ μ€μκ°μΌλ‘ μ΄λ²€νΈκ° λ°μνλ€ λ³΄λ λ€λ₯Έ νμ΄μ§λ€μ λΉν΄
μ΄κΈ° λ‘λ©, μλ‘μ΄ λ©μμ§ μμ , μλ‘μ΄ λ©μμ§ μμ μμ μν©, κ³Όκ±° λ©μμ§ λΆλ¬μ€κΈ°, μ€ν¬λ‘€μ μμΉ λ± λ€μν μν©μ κ³ λ €ν΄μΌ νλ€. λ§μ κ°λ₯μ±μ κ³ λ―Όν΄μ μμ°μ€λ¬μ΄ UXλ₯Ό μ 곡ν μ μλλ‘ λ Έλ ₯νλ€.
λ κΈ°λ₯ ꡬνλ§μ΄ λ¬Έμ κ° μλλΌ μ΄λ²€νΈ 리μ€λ μ λ¦¬κ° λ§€μ°λ§€μ° μ€μνλ€.
μΌλ¨ ꡬνμ μ§μ€ν΄μ λ§λ€λ€ 보면 μ΄λ²€νΈ 리μ€λκ° μ λλ‘ μ 리 λμ§ μμμ λ©λͺ¨λ¦¬ λμκ° λ°μνλ μν©μ΄ μκ²Όλ€.
μμΌλ‘ μ€μκ° μ±ν νκ²½μμ λ λμ UX μ 곡 λ° μ΅μ νλ₯Ό μν΄ κ°μ μ λ ν΄λ³Ό μμ μ΄λ€ !