인프런 커뮤니티 질문&답변

복실묘님의 프로필 이미지
복실묘

작성한 질문수

Slack 클론 코딩[실시간 채팅 with React]

스크롤바 조절하기

onScroll 스크롤 위치 유지가 안됩니다 ㅠ

작성

·

419

0

import Chat from '@components/Chat';
import { ChatZone, Section, StickyHeader } from '@components/ChatList/style';
import { IChat, IDM } from '@typings/db';
import React, { FC, RefObject, VFC, forwardRef, useCallback, useRef } from 'react';
import { Scrollbars } from 'react-custom-scrollbars';

interface Props {
  scrollRef: RefObject<Scrollbars>;
  chatSections: { [key: string]: IDM[] };
  setSize: (f: (index: number) => number) => Promise<IDM[][] | undefined>;
  isEmpty: boolean;
  isReachingEnd: boolean;
}

const ChatList: VFC<Props> = (({ chatSections, setSize, isEmpty, scrollRef, isReachingEnd}) => {

  const onScroll = useCallback((values) => {
    // 끝에 도달하면 불러오지 않기
    if(values.scrollTop === 0 && !isReachingEnd){
      console.log('가장 위');
      setSize((prevSize) => prevSize + 1).then(()=>{
      // 스크롤 위치 유지
      if(scrollRef?.current){
        scrollRef.current?.scrollTop(scrollRef.current?.getScrollHeight() - values.scrollHeight);
        }
      });
    }
  }, []);

  return (
    <ChatZone>
      <Scrollbars autoHide ref={scrollRef} onScrollFrame={onScroll}>
        {Object.entries(chatSections).map(([date, chats]) => {
          return (
            <Section className={`section-${date}`} key={date}>
              <StickyHeader>
                <button>{date}</button>
              </StickyHeader>
              {chats.map((chat) => (
                <Chat key={chat.id} data={chat} />
              ))}
            </Section>
          );
        })}
      </Scrollbars>
    </ChatZone>
  );
});

export default ChatList;

이쪽 코드는 문제가 없는것같은데 희한하게 위치가 유지가 되지않고 원래처럼 쭉 올라가버립니다.. ref쪽이 문제인가요..?


혹시몰라 DirectMessage 컴포넌트도 아래에 첨부하겠습니다.

import React, { useCallback, useEffect, useRef } from 'react';

import gravator from 'gravatar';
import useSWR, { mutate } from 'swr';
// swr 인피니티스크롤링 전용 메서드
import useSWRInfinite from 'swr/infinite';
import { IDM, IUser } from '@typings/db';
import fetcher from '@utils/fetcher';
import { useParams } from 'react-router';
import ChatBox from '@components/ChatBox';
import { Container, Header } from '@pages/DirectMessage/style';
import ChatList from '@components/ChatList';
import useInput from '@hooks/useInput';
import axios from 'axios';
import makeSection from '@utils/makeSection';
import Scrollbars from 'react-custom-scrollbars';

const DirectMessage = () => {
  const { workspace, id } = useParams<{ workspace: string; id: string }>();
  const { data: userData } = useSWR(`http://localhost:3095/api/workspaces/${workspace}/users/${id}`, fetcher);
  // 내정보
  const { data: myData } = useSWR(`http://localhost:3095/api/users`, fetcher);
  const [chat, onChangeChat, setChat] = useInput('');

  // 과거 채팅리스트에서 채팅을 치면 최신목록으로 바로 스크롤을 내려줄려면 ref를
  // 이 컴포넌트에서 props로 내려줘야하기 때문에 forwardRef를 사용해서 props로 넘겨준다
  // 💡 HTML 엘리먼트가 아닌 React 컴포넌트에서 ref prop을 사용하려면 React에서 제공하는 forwardRef()라는 함수를 사용해야 합니다
  const scrollbarRef = useRef<Scrollbars>(null);

  // 채팅 받아오는곳 (setSize : 페이지수를 바꿔줌)
  // useSWRInfinite를 쓰면 [{id:1},{id:2},{id:3},{id:4}] 1차원배열이 [[{id:1},{id:2}],[{id:3},{id:4}]] 2차원배열이 된다.
  const {
    data: chatData,
    mutate: mutateChat,
    setSize,
  } = useSWRInfinite<IDM[]>(
    (index) => `http://localhost:3095/api/workspaces/${workspace}/dms/${id}/chats?perPage=20&page=${index + 1}`,
    fetcher,
  );

  // 데이터 40 개중에 20개씩 사져오면 첫번째페이지부터 20 + 20 + 0 세번째 페이지 0 이되면 isEmpty, isReachingEnd는 true가 됨
  // 반대의 상황에서 데이터가 45개면 20 + 20 + 5 isEmpty는 0이 아니라서 false isReachingEnd는 여전히 데이터 가져옴
  const isEmpty = chatData?.[0]?.length === 0;
  const isReachingEnd = isEmpty || (chatData && chatData[chatData.length - 1]?.length < 20) || false;

  const onSubmitForm = useCallback(
    (e) => {
      e.preventDefault();

      if (chat?.trim() && chatData) {
        const savedChat = chat;
        // 💡 옵티미스틱 UI
        // 서버쪽에 다녀오지 않아도 성공해서 데이터가 있는거처럼 보이게 미리 만듦
        mutateChat((prevChatData) => {
          // infinite 스크롤링은 2차원 배열이다.
          prevChatData?.[0].unshift({  // unshift : 앞쪽에 추가
            id: (chatData[0][0]?.id || 0)  + 1,
            content: savedChat,
            SenderId: myData.id,
            Sender: myData,
            ReceiverId: userData.id,
            Receiver: userData,
            createdAt : new Date(),
          });
          return prevChatData;
        },false) // 옵티미스틱 UI 할땐 이부분이 항상 false
        .then(()=>{
          setChat(''); // 버튼클릭 시 기존 채팅지우기
          scrollbarRef.current?.scrollToBottom(); // 채팅 첬을때 맨 아래로
        })

        axios
          .post(
            `http://localhost:3095/api/workspaces/${workspace}/dms/${id}/chats`,
            {
              content: chat,
            },
            {
              withCredentials: true,
            },
          )
          .then(() => {
            mutateChat(); //  SWR에서 데이터를 다시 불러와서 캐시를 갱신하는 역할을 합니다.
          })
          .catch(() => {
            console.error;
          });
      }
    },
    [chat, chatData, myData, userData, workspace, id],
  );

  //     (채팅이 최신것을 아래에 두기 위함) = 기존것 데이터를두고 새 데이터를 뒤집어서 출력 / flat() 배열을 1차원 배열로 만들어줌
  const chatSections = makeSection(chatData ? [...chatData].flat().reverse() : []);

  // 로딩 시 스크롤바 제일 아래로
  useEffect(()=>{
    if(chatData?.length === 1){ // 채팅 데이터가 있어서 불러온 경우
      scrollbarRef.current?.scrollToBottom(); // 가장 아래쪽으로 내려줌
    }
  },[chatData])

  // 로딩
  if (!userData || !myData) {
    return null;
  }

  return (
    <Container>
      <Header>
        <img src={gravator.url(userData.email, { s: '24px', d: 'retro' })} alt={userData.nickname}></img>
        <span>{userData.nickname}</span>
      </Header>
      {/* 컴포넌트 위치를 미리 지정해도 좋다. */}
      {/* 전역 상태관리 라이브러리를 사용해도 컴포넌트상황에따라 props 로 넘겨줌*/}
      <ChatList
        scrollRef={scrollbarRef}
        chatSections={chatSections}
        setSize={setSize}
        isEmpty={isEmpty}
        isReachingEnd={isReachingEnd}
      />
      <ChatBox chat={chat} onChangeChat={onChangeChat} onSubmitForm={onSubmitForm} />
    </Container>
  );
};

export default DirectMessage;

답변 1

0

제로초(조현영)님의 프로필 이미지
제로초(조현영)
지식공유자

제가 외부에 있어서 확인이 어려운데요. 일단 제 깃헙 코드는 제대로 될 겁니다.

복실묘님의 프로필 이미지
복실묘
질문자

아직도 해결법을 못찾았는데 혹시 맥 환경이랑 상관이 있나요??

복실묘님의 프로필 이미지
복실묘

작성한 질문수

질문하기