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

김호준님의 프로필 이미지
김호준

작성한 질문수

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

채널 목록 만들기

DMList 선택 시 무한로딩되는 에러

작성

·

358

0

안녕하세요 제로초님. 강의 잘 듣고 있습니다.

다름이 아니라 채널 토글에서 각 채널을 선택하면 정상적으로 이동하지만, DMlist에서 선택하면 아래와 같이 useEffect가 무한루프처럼 호출되어 문제가 발생하는 것 같습니다.

제로초님께서 업로드해주신 코드와 동일하게 def에 workspace를 넣어 작성했는데, 어떠한 부분에서 위와 같이 무한로딩되는 에러가 발생하는지 못 찾겠습니다ㅠㅠ

 

DMList/index.tsx

// import useSocket from '@hooks/useSocket';
import { CollapseButton } from '@components/DMList/style';
import { IDM, IUser, IUserWithOnline } from '@typings/db';
import fetcher from '@utils/fetcher';
import React, { FC, useCallback, useEffect, useState } from 'react';
import { useParams } from 'react-router';
import { NavLink } from 'react-router-dom';
import useSWR from 'swr';


const DMList: FC = () => {
  const { workspace } = useParams<{ workspace?: string }>();
  const { data: userData, error, mutate } = useSWR<IUser>('/api/users', fetcher, {
    dedupingInterval: 2000, // 2초
    });
  const { data: memberData } = useSWR<IUserWithOnline[]>(
    userData ? `/api/workspaces/${workspace}/members` : null,
    fetcher,
  );
//   const [socket] = useSocket(workspace);
  const [channelCollapse, setChannelCollapse] = useState(false);
  const [countList, setCountList] = useState<{ [key:string]: number}>({});
  const [onlineList, setOnlineList] = useState<number[]>([]);

  const toggleChannelCollapse = useCallback(() => {
    setChannelCollapse((prev) => !prev);
  }, []);

  const resetCount = useCallback(
    (id) => () => {
        setCountList((list) => {
            return{
                ...list,
                [id]: 0,
            };
        });
    },
    [],
  );

  const onMessage = (data: IDM) => {
    console.log("DM 왓따", data);
    setCountList((list) => {
        return {
            ...list,
            [data.SenderId] : list[data.SenderId] ? list[data.SenderId]+1 : 1,
        };
    });
  };

  useEffect(() => {
    console.log('DMList: workspace 바꼈다', workspace);
    setOnlineList([]);
    setCountList({});
  }, [workspace]);

//   useEffect(() => {
//     socket?.on('onlineList', (data: number[]) => {
//       setOnlineList(data);
//     });
//     socket?.on('dm', onMessage);
//     console.log('socket on dm', socket?.hasListeners('dm'), socket);
//     return () => {
//       socket?.off('dm', onMessage);
//       console.log('socket off dm', socket?.hasListeners('dm'));
//       socket?.off('onlineList');
//     };
//   }, [socket]);

  return (
    <>
      <h2>
        <CollapseButton collapse={channelCollapse} onClick={toggleChannelCollapse}>
          <i
            className="c-icon p-channel_sidebar__section_heading_expand c-icon--caret-right c-icon--inherit c-icon--inline"
            data-qa="channel-section-collapse"
            aria-hidden="true"
          />
        </CollapseButton>
        <span>Direct Messages</span>
      </h2>
      <div>
        {!channelCollapse &&
          memberData?.map((member) => {
            const isOnline = onlineList.includes(member.id);
            return (
                <NavLink 
                    key={member.id} 
                    activeClassName="selected" 
                    to={`/workspace/${workspace}/dm/${member.id}`}
                >
                    <i
                    className={`c-icon p-channel_sidebar__presence_icon p-channel_sidebar__presence_icon--dim_enabled p-channel_sidebar__presence_icon--on-avatar c-presence ${
                        isOnline ? 'c-presence--active c-icon--presence-online' : 'c-icon--presence-offline'
                    }`}
                    aria-hidden="true"
                    data-qa="presence_indicator"
                    data-qa-presence-self="false"
                    data-qa-presence-active="false"
                    data-qa-presence-dnd="false"
                    />
                    <span>{member.nickname}</span>
                    {member.id === userData?.id && <span> (나)</span>}
                </NavLink>
            );
          })}
      </div>
    </>
  );
};

export default DMList;

답변 1

0

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

DMList 자체가 unmount됐다가 다시 mount되는것같기도 합니다. useEffect에 return 함수 넣고 그 안에 콘솔찍어보세요.

김호준님의 프로필 이미지
김호준
질문자

아래와 같이 작성해서 콘솔 찍어 보았는데, 'cleanup'은 찍히지 않고 똑같이 'DMList: workspace 바꼈다'만 무한히 찍힙니다.

useEffect(() => {
    console.log('DMList: workspace 바꼈다', workspace);
    setOnlineList([]);
    return () => {
      console.log('cleanup');
    };
  }, [workspace]);
제로초(조현영)님의 프로필 이미지
제로초(조현영)
지식공유자

일단 빌드가 다시 된 것은 맞나요? setOnlineList([])가 없어도 무한반복되나요? useEffect가 없으면요?

김호준님의 프로필 이미지
김호준
질문자

useEffect를 주석 처리하고 실행하면 ...dm/3으로 이동한 후 화면이 멈춥니다. useEffect가 없어도 같은 문제가 발생하는 것 같습니다. DMList 컴포넌트에서 문제가 발생하는 것은 확실한 것 같은데, workspace/index와 DMList/index 외에는 DMList 컴포넌트를 사용하는 파일이 없습니다ㅠㅠ ChannelList는 문제없이 작동합니다.
workspace/index 코드는 아래와 같습니다.

import Menu from "@components/Menu";
import Modal from "@components/Modal";
import CreateChannelModal from "@components/CreateChannelModal";
import InviteWorkspaceModal from "@components/InviteWorkspaceModal";
import InviteChannelModal from "@components/InviteChannelModal";
import DMList from "@components/DMList";
import ChannelList from "@components/ChannelList";
import useInput from "@hooks/useinput";
import fetcher from "@utils/fetcher";
import axios from "axios";
import React, { FunctionComponent, useCallback, useState } from "react"
import { Link, Redirect, Route, Switch, useParams } from 'react-router-dom';
import { toast } from 'react-toastify';
import useSWR from "swr";
import { 
    AddButton, 
    Channels, 
    Chats, 
    Header, 
    LogOutButton, 
    MenuScroll, 
    ProfileImg, 
    ProfileModal, 
    RightMenu, 
    WorkspaceButton, 
    WorkspaceModal,
    WorkspaceName, 
    WorkspaceWrapper, 
    Workspaces 
} from "@layouts/Workspace/style";
import gravatar from 'gravatar';
import { Button, Input, Label } from '@pages/SignUp/style';
import { IChannel, IUser } from "@typings/db";
import loadable from "@loadable/component";



const Channel = loadable(() => import('@pages/SignUp'));
const DirectMessage = loadable(() => import('@layouts/Workspace'));

const Workspace: FunctionComponent = () => {
    const [showUserMenu, setShowUserMenu] = useState(false);
    const [showCreateWorkspaceModal, setShowCreateWorkspaceModal] = useState(false);
    const [showInviteWorkspaceModal, setShowInviteWorkspaceModal] = useState(false);
    const [showInviteChannelModal, setShowInviteChannelModal] = useState(false);
    const [showWorkspaceModal, setShowWorkspaceModal] = useState(false);
    const [showCreateChannelModal, setShowCreateChannelModal] = useState(false);
    const [newWorkspace,onChangeNewWorkspace, setNewWorkSpace] = useInput('');
    const [newUrl, onChangeNewUrl, setNewUrl] = useInput('');

    const { workspace } = useParams<{workspace: string}>();
    const { data: userData , error, mutate} = useSWR<IUser | false>(
        '/api/users', 
        fetcher, 
        {
            dedupingInterval: 2000,
        }
    );
    const { data: channelData } = useSWR<IChannel[]>(
        userData? `api/workspaces/${workspace}/channels` : null, 
        fetcher
    );
    const { data: memberData } = useSWR<IChannel[]>(
        userData? `api/workspaces/${workspace}/channels` : null, 
        fetcher
    );

    const onLogout = useCallback(() => {
        axios
        .post('/api/users/logout', null, {
            withCredentials: true,
        })
        .then(() => {
            mutate(false, false);
        });
    }, [])

    const onClickUserProfile = useCallback((e) => {
        e.stopPropagation();
        setShowUserMenu((prev) => !prev);
    }, []);

    const onClickCreateWorkSpace = useCallback(() => {
        setShowCreateWorkspaceModal(true);
    }, []);

    const onCreateWorkspace = useCallback((e) => {
        e.preventDefault();
        if(!newWorkspace || !newWorkspace.trim()) return;
        if(!newUrl || !newUrl.trim()) return;
        axios
        .post(
        '/api/workspaces', 
        {
            workspace: newWorkspace,
            url : newUrl,
        }, 
        {
            withCredentials: true,
        })
        .then(() => {
            mutate();
            setShowCreateWorkspaceModal(false);
            setNewWorkSpace('');
            setNewUrl('');
        })
        .catch((error)=>{
            console.dir(error);
            toast.error(error.response?.data, { position: 'bottom-center' });
        });
    },
    [newWorkspace, newUrl]);

    const onCloseModal = useCallback(() => {
        setShowCreateWorkspaceModal(false);
        setShowCreateChannelModal(false);
        setShowInviteWorkspaceModal(false);
        setShowInviteChannelModal(false);
    }, []);

    if(!userData) {
        return <Redirect to="/login"/>
    }

    const toggleWorkspaceModal = useCallback(()=> {
        setShowWorkspaceModal((prev) => !(prev));
    },[]);

    const onClickAddChannel = useCallback(() => {
        setShowCreateChannelModal(true);
    }, []);

    const onClickInviteWorkspace = useCallback(() => {
        setShowInviteWorkspaceModal(true);
    }, []);

    return (
        <div>
            <Header>
                <RightMenu>
                    <span onClick = {onClickUserProfile}>
                        <ProfileImg src = {gravatar.url(userData.email, {s: '28px', d: 'retro'})} alt= {userData.nickname}/>
                        {showUserMenu && (
                        <Menu style={{right:0, top:38}} show={showUserMenu} onCloseModal={onClickUserProfile}>
                            <ProfileModal>
                                <img src={gravatar.url(userData.email, {s: '36px', d: 'retro'})} alt= {userData.nickname}/>
                                <div>
                                    <span id="profile-name">{userData.nickname}</span>
                                    <span id="profile-active">Active</span>
                                </div>
                            </ProfileModal>
                            <LogOutButton onClick={onLogout}>로그아웃</LogOutButton>
                        </Menu>
                        )}
                    </span>
                </RightMenu>
            </Header>
            <WorkspaceWrapper>
                <Workspaces>
                    {userData?.Workspaces?.map((ws) => {
                        return (
                            <Link key={ws.id} to={'/workspace/${123}/channel/일반'}>
                                <WorkspaceButton>{ws.name.slice(0,1).toUpperCase()}</WorkspaceButton>
                            </Link>
                        );
                    })}
                    <AddButton onClick={onClickCreateWorkSpace}>+</AddButton>
                </Workspaces>
                <Channels>
                    <WorkspaceName onClick={toggleWorkspaceModal}>
                        Sleact
                    </WorkspaceName>
                    <MenuScroll>
                        <Menu show={showWorkspaceModal} onCloseModal={toggleWorkspaceModal} style={{ top: 95, left: 80}}>
                            <WorkspaceModal>
                                <h2>Sleact</h2>
                                <button onClick={onClickInviteWorkspace}>워크스페이스에 사용자 초대</button>
                                <button onClick={onClickAddChannel}>채널 만들기</button>
                                <button onClick={onLogout}>로그아웃</button>
                            </WorkspaceModal>
                        </Menu>
                        <ChannelList/>
                        <DMList/>
                        {/* {channelData?.map((v) => (<div>{v.name}</div>))} */}
                    </MenuScroll>
                </Channels>
                <Chats>
                    <Switch>
                        <Route path="/workspace/:workspace/channel/:channel" component={Channel} />
                        <Route path="/workspace/:workspace/dm/:id" component={DirectMessage} />
                    </Switch>
                </Chats>
            </WorkspaceWrapper>
            <Modal show={showCreateWorkspaceModal} onCloseModal={onCloseModal}>
                <form onSubmit={onCreateWorkspace}>
                    <Label id="workspace-label">
                        <span>워크스페이스 이름</span>
                        <Input id="workspace" value={newWorkspace} onChange={onChangeNewWorkspace}/>
                    </Label>
                    <Label id="workspace-url-label">
                        <span>워크스페이스 url</span>
                        <Input id="workspace-url" value={newUrl} onChange={onChangeNewUrl}/>
                    </Label>
                    <Button type="submit">생성하기</Button>
                </form>
            </Modal>
            <CreateChannelModal
                show={showCreateChannelModal}
                onCloseModal={onCloseModal}
                setShowCreateChannelModal={setShowCreateChannelModal}
            />
            <InviteWorkspaceModal
                show={showInviteWorkspaceModal} 
                onCloseModal={onCloseModal}
                setShowInviteWorkspaceModal={setShowInviteWorkspaceModal}
            />
            <InviteChannelModal 
                show={showInviteChannelModal} 
                onCloseModal={onCloseModal} 
                setShowInviteChannelModal={setShowInviteChannelModal}
            />
        </div>
    )
}

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

눈으로봐서는 저도 알수가 없고요. 전체를 주석처리한다음에 하나씩 주석을 풀면서 에러 원인을 찾아보는게 좋습니다.

김호준님의 프로필 이미지
김호준

작성한 질문수

질문하기