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

mihyun Lee님의 프로필 이미지
mihyun Lee

작성한 질문수

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

라우터 주소 설계(라우트 파라미터)

router.js:954 No routes matched location 에러가 발생합니다.

해결된 질문

작성

·

6.7K

0

안녕하세요 제로초님, 현재 react-router-dom v6.6.2로 강의를 수강하고 있습니다.

 

앞부분에서 잘 따라하다가 어느 부분에서 잘못된 것인지 로그인을 했을 때 흰 화면과 함께 아래 첨부한 사진과 같은 에러가 발생하했습니다.

API를 받아올 때 사용하는 params에 문제가 있나 싶었지만, http://localhost:3095/api/workspaces/sleact/channels 이 주소로 들어갔을 때 아래와 같은 데이터를 받아오는 것을 확인할 수 있었습니다.

[
 {
   "id": 1,
   "name": "일반",
   "private": false,
   "createdAt": "2023-01-26T08:07:33.000Z",
   "updatedAt": "2023-01-26T08:07:33.000Z",
   "WorkspaceId": 1,
   "Members": [
    {
     "id": 2,
     "ChannelMembers": {
     "UserId": 2
    }
   }
  ]
 }
]

 

데이터가 문제인가 싶어서 테이블도 삭제했다가 다시 만들어봤지만 해결할 수 없었습니다ㅠㅠ 어떻게 해결할 수 있을까요?? 혹시 몰라 코드는 모두 첨부하겠습니다!

 

// App/index.tsx
import React from 'react';
import loadable from '@loadable/component';
import { Routes, Route, Navigate } from 'react-router-dom';

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

const App = () => {
  return (
    <Routes>
      <Route path="/" element={<Navigate to="/login" />} />
      <Route path="/login" element={<Login />} />
      <Route path="/signup" element={<SignUp />} />
      <Route path="/workspace/:workspace" element={<Workspace />} />
    </Routes>
  );
};

export default App;
// Workspace/index.tsx
import React, { useCallback, useEffect, useState } from 'react';
import useSWR from 'swr';
import axios from 'axios';
import fetcher from '@utils/fetcher';
import gravatar from 'gravatar';
import { Navigate, Route, Routes } from 'react-router';
import {
  AddButton,
  Channels,
  Chats,
  Header,
  LogOutButton,
  MenuScroll,
  ProfileImg,
  ProfileModal,
  RightMenu,
  WorkspaceButton,
  WorkspaceModal,
  WorkspaceName,
  Workspaces,
  WorkspaceWrapper,
} from './styles';
import loadable from '@loadable/component';
import Menu from '@components/Menu';
import { Link } from 'react-router-dom';
import { IChannel, IUser } from '@typings/db';
import Modal from '@components/Modal';
import { Button, Input, Label } from '@pages/SignUp/styles';
import useInput from '@hooks/useInput';
import { toast } from 'react-toastify';
import CreateChannelModal from '@components/CreateChannelModal';
import { useParams } from 'react-router';

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

const Workspace = () => {
  const [showUserMenu, setShowUserMenu] = useState(false);
  const [showCreateWorkspaceModal, setShowCreateWorkspaceModal] = useState(false);
  const [showWorkspaceModal, setShowWorkspaceModal] = useState(false);
  const [showCreateChannelModal, setShowCreateChannelModal] = useState(false);
  const [newWorkspace, onChangeNewWorkspace, setNewWorkspace] = useInput('');
  const [newUrl, onChangeNewUrl, setNewUrl] = useInput('');

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

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

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

  const onCloseUserProfile = useCallback((e: React.MouseEvent) => {
    e.stopPropagation();
    setShowUserMenu(false);
  }, []);

  const onClickCreateWorkspace = useCallback(() => {
    setShowCreateWorkspaceModal((prev) => !prev);
  }, []);

  const onCreateWorkspace = useCallback(
    (e: React.FormEvent) => {
      e.preventDefault();

      // 필수 값이 들어있는지 검사
      if (!newWorkspace || !newWorkspace.trim()) return;
      if (!newUrl || !newUrl.trim()) return;

      axios
        .post(
          'http://localhost:3095/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);
  }, []);

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

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

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

  return (
    <div>
      <Header>
        <RightMenu>
          <span onClick={onClickUserProfile}>
            <ProfileImg src={gravatar.url(userData.email, { s: '28px', d: 'retro' })} alt={userData.email} />
            <Menu style={{ right: 0, top: 38 }} show={showUserMenu} onCloseModal={onCloseUserProfile}>
              <ProfileModal>
                <img src={gravatar.url(userData.email, { s: '36px', d: 'retro' })} alt={userData.email} />
                <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={`${ws.url}/channel/일반`}>
                <WorkspaceButton>{ws.name.slice(0, 1).toUpperCase()}</WorkspaceButton>
              </Link>
            );
          })}
          <AddButton onClick={onClickCreateWorkspace}>+</AddButton>;
        </Workspaces>
        <Channels>
          <WorkspaceName onClick={toggleWorkspaceModal}>Sleact</WorkspaceName>
          <MenuScroll>
            <Menu style={{ top: 95, left: 80 }} show={showWorkspaceModal} onCloseModal={toggleWorkspaceModal}>
              <WorkspaceModal>
                <h2>Sleact</h2>
                <button onClick={onClickAddChannel}>채널 만들기</button>
                <button onClick={onLogout}>로그아웃</button>
              </WorkspaceModal>
            </Menu>
            {channelData?.map((v) => (
              <div>{v.name}</div>
            ))}
          </MenuScroll>
        </Channels>
        <Chats>
          <Routes>
            <Route path="/:workspace/channel/:channel" element={<Channel />} />
            <Route path="/:workspace/dm/:id" element={<DirectMessage />} />
          </Routes>
        </Chats>
      </WorkspaceWrapper>
      <Modal show={showCreateWorkspaceModal} onCloseModal={onCloseModal}>
        <form onSubmit={onCreateWorkspace}>
          <Label id="workspace-name">
            <span>워크스페이스 이름</span>
            <Input id="workspace" value={newWorkspace} onChange={onChangeNewWorkspace} />
          </Label>
          <Label id="workspace-label">
            <span>워크스페이스 url</span>
            <Input id="workspace" value={newUrl} onChange={onChangeNewUrl} />
          </Label>
          <Button type="submit">생성하기</Button>
        </form>
      </Modal>
      <CreateChannelModal
        show={showCreateChannelModal}
        onCloseModal={onCloseModal}
        setShowCreateChannelModal={setShowCreateChannelModal}
      />
    </div>
  );
};

export default Workspace;
// CreateChannel/index.tsx
import Modal from '@components/Modal';
import useInput from '@hooks/useInput';
import { Button, Input, Label } from '@pages/SignUp/styles';
import { IChannel, IUser } from '@typings/db';
import fetcher from '@utils/fetcher';
import axios from 'axios';
import React, { useCallback } from 'react';
import { useParams } from 'react-router';
import { toast } from 'react-toastify';
import useSWR from 'swr';

interface Props {
  show: boolean;
  onCloseModal: () => void;
  setShowCreateChannelModal: (flag: boolean) => void;
}

const CreateChannelModal: React.FC<Props> = ({ show, onCloseModal, setShowCreateChannelModal }) => {
  const [newChannel, onChangeNewChannel, setNewChannel] = useInput('');

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

  const onCreateChannel = useCallback(
    (e: React.FormEvent) => {
      e.preventDefault();
      axios
        .post(
          `http://localhost:3095/api/workspaces/${workspace}/channels`,
          {
            name: newChannel,
          },
          { withCredentials: true },
        )
        .then(() => {
          setShowCreateChannelModal(false);
          mutateChannel();
          setNewChannel('');
        })
        .catch((error) => {
          console.dir(error);
          toast.error(error.response?.data, { position: 'bottom-center' });
        });
    },
    [newChannel, workspace],
  );

  return (
    <Modal show={show} onCloseModal={onCloseModal}>
      <form onSubmit={onCreateChannel}>
        <Label id="channel-label">
          <span>채널 이름</span>
          <Input id="workspace" value={newChannel} onChange={onChangeNewChannel} />
        </Label>
        <Button type="submit">생성하기</Button>
      </form>
    </Modal>
  );
};

export default CreateChannelModal;

답변 2

0

mihyun Lee님의 프로필 이미지
mihyun Lee
질문자

기존 코드에서 코드를 아래와 같이 바꿔서 해결했습니다!!

// App/index.tsx
<Route path="/workspace/:workspace/*" element={<Workspace />} />

⬆️ path에 /* 추가

 

// Workspace/index.tsx 
<Workspaces>
  {userData?.Workspaces?.map((ws) => {
    return (
      <Link key={ws.id} to={`/workspace/${ws.url}/channel/일반`}>
        <WorkspaceButton>{ws.name.slice(0, 1).toUpperCase()}</WorkspaceButton>
      </Link>
    );
  })}
  <AddButton onClick={onClickCreateWorkspace}>+</AddButton>;
</Workspaces>

⬆️to 속성에 /workspace/ 추가

 

모두 해피 코딩 하세요,,,^^!!

0

mihyun Lee님의 프로필 이미지
mihyun Lee
질문자

추가적으로 해본 방법은 Login과 Signup 컴포넌트에서 Navigate url을 수정해보았습니다.

if (data) {
    return <Navigate to="/workspace/sleact/channel/일반" />;
}
if (data) {
    return <Navigate to="/workspace/sleact" />;
}

 

// Workspace/index.tsx
const params = useParams<{ workspace?: string }>();
const { workspace } = params;

console.log(workspace) // sleact

위와 같은 결과가 나오게 되어 채널을 추가하거나 워크스페이스를 추가하는 등의 동작은 잘 됩니다.

 

하지만, 각 워크스페이스를 클릭했을 경우,

<Workspaces>
  {userData?.Workspaces?.map((ws) => {
    return (
      <Link key={ws.id} to={`${ws.url}/channel/일반`}>
        <WorkspaceButton>{ws.name.slice(0, 1).toUpperCase()}</WorkspaceButton>
      </Link>
    );
  })}
  <AddButton onClick={onClickCreateWorkspace}>+</AddButton>;
</Workspaces>

위의 코드에 의해서인지 http://localhost:3090/workspace/sleact/sleact/channel/일반 이렇게 잘못된 url로 이동하게 됩니다.. 결론적으로는 아직 해결하지 못했습니다😂

mihyun Lee님의 프로필 이미지
mihyun Lee

작성한 질문수

질문하기