묻고 답해요
141만명의 커뮤니티!! 함께 토론해봐요.
인프런 TOP Writers
-
미해결Slack 클론 코딩[실시간 채팅 with React]
(배포 이슈)webpack 빌드 후 index.html을 열어보았는데 router부분이 실행이 안되는거같습니다..
App.tsx부분입니다. 여기에서 Router부분 주석처리하고 테스트중입니다라는 텍스트만 적어놓고 npm run build 후 생긴 index.html을 클릭하면 아래 이미지처럼 텍스트가 제대로 나오는거까진 확인을 하였습니다. 그런데 텍스트를 지우고 index.html 을 실행하면 화면에 아무것도 나오질 않습니다. 코드대로라면 백엔드와 통신이 안될 땐 로딩중입니다는 텍스트가 떠야하는데 말이죠.. import React, { FC } from 'react'; import './App.css'; import { Router } from '../router'; const App = () => { return ( <div className="App"> <Router /> </div> ); }; export default App; -npm run build 후 App.tsx에 <Router/> 주석 후 테스트중입니다 텍스트 입력 후 index.html 실행 화면-npm run build 후 App.tsx에 <Router/> 주석 해제 후 index.html 실행 화면 및 네트워크 개발자도구 -npm run dev (정상일 경우 동작해야하는 화면)제가 이 질문을 드린건 지금 s3에 버킷 생성 후 dist에 있는 폴더를 아래처럼 다 올렸습니다. 그 후 엔드포인트에 접속했는데 아무것도 안 뜨길래 로컬에서 빌드 후 생긴 index.html에 로딩중입니다가 떠야 s3 엔드포인트에 접속했을 때도 로딩중입니다가 뜰 거 같아서 로컬에서 문제를 해결해보고 있습니다.-s3 파일 업로드 화면- 아래는 웹 시작초기에 관련 있는 코드들이라 생각되어 같이 올립니다. client.tsx부분입니다. import 'core-js/stable'; import 'regenerator-runtime/runtime'; import React from 'react'; import { render } from 'react-dom'; import { BrowserRouter } from 'react-router-dom'; import axios from 'axios'; import App from './layouts/App'; axios.defaults.withCredentials = true; axios.defaults.baseURL = process.env.NODE_ENV === 'production' ? 'http://localhost:3095' : 'http://localhost:3095'; render( <BrowserRouter> <App /> </BrowserRouter>, document.querySelector('#app'), ); index.html입니다. <html> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" /> <title>주식 캘린더</title> <style> html, body { margin: 0; padding: 0; overflow: initial !important; } body { font-size: 15px; line-height: 1.46668; font-weight: 400; font-variant-ligatures: common-ligatures; -moz-osx-font-smoothing: grayscale; -webkit-font-smoothing: antialiased; } * { box-sizing: border-box; } </style> <!-- <link rel="icon" type="image/png" sizes="16x16" href="/images/favicon.png" /> --> </head> <body> <div id="modal"></div> <div id="app"></div> <script src="./dist/app.js"></script> </body> </html> 하루 반나절동안 잡고 있는데도 해결이 안되어서 질문드립니다.. ㅠㅠ... 도와주세요 제발...ㅠㅠㅠㅠ
-
미해결Slack 클론 코딩[실시간 채팅 with React]
현재 깃허브는 셋팅이 완료되있는 상태인가요?
안녕하세요 선생님 다름 아니라 현재 세팅하기 부분 강의 듣고있는데,화면과 다른게 제가 클론해온 파일은 alecture 폴더에 셋팅이 다 완료되어있는것같아서 문의드립니다.https://github.com/zerocho/sleact/tree/master여기서 클론 해왔구요그러면 현재 셋팅하는 강의는 다 건너뛰고 바로 회원가입 파트로 넘어가면 되는지요?
-
미해결Slack 클론 코딩[실시간 채팅 with React]
Cannot GET / 404 에러 발생하시는분
강의 7:40부근webpack.config.ts 파일72번째 라인devServer: { historyApiFallback: true, // react router port: 3090, devMiddleware: { publicPath: '/dist/' }, static: { directory: path.resolve(__dirname) }, }, 이부분 추가하시면 될것같습니다.static: { directory: path.resolve(__dirname) }
-
미해결Slack 클론 코딩[실시간 채팅 with React]
프로젝트에 사용할 상태와 로직 관리 라이브러리 훅 사용 질문입니다.
로그인 및 인증,인가(세션,jwt둘다 사용), 게시판(이미지포함), 댓글, 소켓채팅 정도의 기능을 구현하여 테스트 코드와 docker로 띄워서 CI/CD까지 구현하려고 합니다. 상태랑 로직 관리를 useReducer, React Context API, React Query, redux, graphql 정도로 생각하고 있는데 어떤걸로 하는게 좋을까요?!
-
미해결Slack 클론 코딩[실시간 채팅 with React]
npx sequelize db:create 실패
mac os에서 mysql을 homebrew로 사용중입니다.root계정의 비밀번호 설정 완료했습니다.back 환경설정에서 문제를 겪고있습니다.cd back으로 경로 이동후npm i로 node 모듈 설치 성공후에.env파일 구성(비밀번호 맞음),npx sequelize db:create 명령어 입력시 아래와 같은 에러가 발생합니다.-- 에러메세지 시작Sequelize CLI [Node: 16.18.0, CLI: 6.6.0, ORM: 6.28.0]Loaded configuration file "config/config.js".Using environment "development".ERROR: Failed to create schema directory 'sleact' (errno: 2 - No such file or directory)-- 에러메세지 종료-- .env파일 시작COOKIE_SECRET=sleactcookieMYSQL_PASSWORD=kaadal-- .env파일 종료--config.js 시작require('dotenv').config();module.exports = { "development": { "username": "root", "password": process.env.MYSQL_PASSWORD, "database": "sleact", "host": "127.0.0.1", "dialect": "mysql" }, "test": { "username": "root", "password": process.env.MYSQL_PASSWORD, "database": "sleact", "host": "127.0.0.1", "dialect": "mysql" }, "production": { "username": "root", "password": process.env.MYSQL_PASSWORD, "database": "sleact", "host": "127.0.0.1", "dialect": "mysql" }}-- config.js 종료이상입니다.
-
미해결프론트엔드 개발환경의 이해와 실습 (webpack, babel, eslint..)
깃허브 확인 문의
git clone git@github.com:jeonghwan-kim/lecture-fronted-dev-env.gitCloning into 'lecture-fronted-dev-env'...git@github.com: Permission denied (publickey).fatal: Could not read from remote repository. 이런 오류가 뜨면서 파일이 안받아지는데 어떻게 해야할까요? ㅠ
-
해결됨Slack 클론 코딩[실시간 채팅 with React]
event.dataTransfer.items의 타입이 무엇인가요?
MDN의 가이드에 따라 DragNDrop 코드를 작성하는데파일을 가져오기 위한 코드인 event.dataTransfer.items 에서 아래와 같은 오류가 발생햇습니다.'DataTransferItemList' 형식은 배열 형식이 아닙니다.ts(2461)MDN DataTransfer: items 속성 가이드에서는 목록을 반환하고 항목이없어도 빈목록을 반환한다고 되어있고MDN DataTransferItemList 타입 가이드에서 객체라고 명시되어 있던데 개별항목에는 [ ]표기법으로 접근할수 있다는걸로 보아 event.dataTransfer.items의 타입은 리스트가 아닌 오브젝트에 숫자를 KEY로 값을 넣어놓은 형태인가요?? 아님 또다른 타입인건가요?코드 function dropHandler(ev: React.DragEvent<HTMLDivElement>): void { console.log('File(s) dropped'); // Prevent default behavior (Prevent file from being opened) ev.preventDefault(); if (ev.dataTransfer.items) { // Use DataTransferItemList interface to access the file(s) [...ev.dataTransfer.items].forEach((item, i) => { // If dropped items aren't files, reject them if (item.kind === 'file') { const file = item.getAsFile(); if (file) { console.log(`… file[${i}].name = ${file.name}`); } } }); } else { // Use DataTransfer interface to access the file(s) const files = ev.dataTransfer.files; [...files].forEach((file, i) => { console.log(`… file[${i}].name = ${file.name}`); }); } }MDN DragNDrop가이드https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/File_drag_and_dropMDN DataTransfer: items 속성 가이드https://developer.mozilla.org/ko/docs/Web/API/DataTransfer/itemsMDN DataTransferItemList 타입 가이드https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItemList
-
해결됨Slack 클론 코딩[실시간 채팅 with React]
infinite scroll에서 최초 랜더링한 페이지가 한페이지에 안나올때 추가로 페이지를 불러 올수 있나요?
만약 Page의 단위를 5로 잡아서 최초 랜더링한 페이지가 스크롤이 되지않는다면 onScroll 이벤트가 발생하지 않으니 setSize 이벤트도 발생할수 없습니다.이렇게 최초 데이터의 개수가 모자라서 이벤트 자체가 발생하지 않으면 별개의 이벤트로 scroll이 가능할때까지 페이지를 불러와야 하는데 좋은 방법이 잇는가요?페이지를 5개로 잡을떄페이지를 20개로 잡을떄참조 코드import ChatBox from '@components/ChatBox'; import ChatList from '@components/ChatList'; import useInput from '@hooks/useInput'; import { Header, Container } from '@pages/DirectMessage/styles'; import fetcher from '@utils/fetcher'; import makeSection from '@utils/makeSection'; import axios from 'axios'; import gravatar from 'gravatar'; import React, { FC, FormEventHandler, useCallback, useEffect, useRef, useState } from 'react'; import Scrollbars from 'react-custom-scrollbars-2'; import { useParams } from 'react-router'; import useSWR from 'swr'; import useSWRInfinite from 'swr/infinite'; const PAGE_SIZE = 5; const DirectMessage: FC = () => { const { workspace, id } = useParams(); const { data: myData } = useSWR<IUser, false>('/api/users', fetcher); const { data: userData } = useSWR<IUser, false>(`/api/workspaces/${workspace}/users/${id}`, fetcher); const [chat, onChangeChat, setChat] = useInput(''); // const scrollbarRef = useRef(null); const { data: chatData, mutate: mutateChat, setSize, } = useSWRInfinite<IDM[]>( (index) => `/api/workspaces/${workspace}/dms/${id}/chats?perPage=${PAGE_SIZE}&page=${index + 1}`, fetcher, ); const isEmpty = chatData?.[0]?.length === 0; const isReachingEnd = isEmpty || (chatData && chatData[chatData.length - 1]?.length < PAGE_SIZE); const chatSection = makeSection(chatData ? [...chatData].flat().reverse() : []); const onSubmitForm = useCallback<FormEventHandler>( (event) => { event.preventDefault(); if (!chat || !chat?.trim()) { return; } axios .post(`/api/workspaces/${workspace}/dms/${id}/chats`, { content: chat, }) .then(() => { mutateChat(); setChat(''); }) .catch(console.error); console.log('제출'); }, [chat, id, mutateChat, setChat, workspace], ); const scrollbarRef = useRef<Scrollbars>(null); return !userData || !myData || !chatData ? null : ( <Container> <Header> <img src={gravatar.url(userData.email, { s: '24px', d: 'retro' })} alt={userData.nickname} /> <span>{userData.nickname}</span> </Header> <ChatList chatSections={chatSection} isEmpty={isEmpty} isReachingEnd={isReachingEnd} setSize={setSize} ref={scrollbarRef} /> <ChatBox onSubmitForm={onSubmitForm} chat={chat} onChangeChat={onChangeChat} placeholder={`Message ${userData.nickname}`} otherData={[userData]} /> </Container> ); }; export default DirectMessage;import Chat from '@components/Chat'; import { ChatZone, Section, StickyHeader } from '@components/ChatList/styles'; import React, { FC, MutableRefObject, forwardRef, useCallback } from 'react'; import { Scrollbars, positionValues } from 'react-custom-scrollbars-2'; interface Props { chatSections: { [key: string]: (IDM | IChat)[] }; isEmpty: boolean; isReachingEnd?: boolean; setSize: (f: (size: number) => number) => Promise<(IDM | IChat)[][] | undefined>; } const ChatList = forwardRef<Scrollbars, Props>(({ chatSections, isReachingEnd, isEmpty, setSize }, scrollRef) => { const onScroll = useCallback( (values: positionValues) => { if (values.scrollTop === 0 && !isReachingEnd) { setSize((size) => size + 1).then(() => { const current = (scrollRef as MutableRefObject<Scrollbars>)?.current; if (current) { current.scrollTop(current.getScrollHeight() - values.scrollHeight); } }); } }, [isReachingEnd, scrollRef, setSize], ); return ( <ChatZone> <Scrollbars autoHide ref={scrollRef} onScrollFrame={onScroll}> {Object.entries(chatSections).map(([dateData, chatData]) => ( <Section className={`section-${dateData}`} key={dateData}> <StickyHeader> <button>{dateData}</button> </StickyHeader> {chatData.map((chat) => ( <Chat key={chat.id} data={chat} /> ))} </Section> ))} </Scrollbars> </ChatZone> ); }); export default ChatList;
-
해결됨Slack 클론 코딩[실시간 채팅 with React]
concat시 2차원 배열이면 쪼개지지않나요?
원본값을 유지하기위해 concat을 사용하셧는데지금 같은 1차원 배열일때는 문제가 없지만 2차원 배열일 경우 해당 배열이 다쪼개져서 1차원 배열이 되는걸로 알고있습니다 그래서 저는 원본을 유지할때 스프레드 문법을 사용하는데 concat이 더 좋은경우도 있나요?두가지 방법을 다 알려주시긴 하셧는데 차이점이 잇는가 궁금합니다.예시상황const chatData = [[1, 2], [3, 4], [5, 6]];[].concat(...chatData).reverse() => [6, 5, 4, 3, 2, 1][...chatData].reverse()=> [[5, 6], [3, 4], [1, 2]]
-
해결됨Slack 클론 코딩[실시간 채팅 with React]
react-custom-scrollbars 를 최상위 컴포넌트에 적용하면 시스템 스크롤바가 안생길까요?
기존 윈도우 스크롤바의 경우 스크롤바가 width를 잡아먹어 의도햇던 디자인이 찌그러 지는경우가 있어서 고민이엿습니다.이번 강의에서 알려주신 react-custom-scrollbars 의경우 width를 잡아먹지 않고 내부에 생성되는걸로 확인되는데vw,vh를 100%로 잡은 최상위 레이아웃을 만들고 react-custom-scrollbars 를 추가한뒤 그 자식으로 기존 코드들을 옮기려 합니다.이때 문제가 될만한 이슈 또는 더 나은 방법이 잇을까요?
-
해결됨Slack 클론 코딩[실시간 채팅 with React]
ComponentPropsWithoutRef 와 FC<PropsWithChildren<Props>> 의 차이점이 무엇인가요
저는 평소에 아래와 같이 ComponentPropsWithoutRef을 이용하여 children이나 스타일등을 props로 내려받아 사용하고 있엇는데 export interface Props { /** 북마크 여부 */ isBookmark: boolean; /** 클릭했을 때 호출할 함수 */ onClick: (event: React.MouseEvent<HTMLButtonElement>) => void; } export const Bookmark = (Props: Props & ComponentPropsWithoutRef<"button">) => { return ( <ButtonStyle {...Props}> <BookmarkIcon isBookmark={Props.isBookmark} /> </ButtonStyle> ); }; export default Bookmark; 강의에서는 아래와같이 FC<PropsWithChildren<Props>> 형식으로 받아 오던데 둘이 어떤 차이가 있을까요?interface Props { show: boolean; onCloseModal: () => void; style: CSSProperties; closeButton?: boolean; } const Menu: FC<PropsWithChildren<Props>> = ({ closeButton, style, show, children, onCloseModal }) => { const stopPropagation = useCallback<MouseEventHandler<HTMLDivElement>>((event) => { event.stopPropagation(); }, []); if (!show) { return null; } return ( <CreateMenu onClick={onCloseModal}> <div onClick={stopPropagation} style={style}> {closeButton && <CloseModalButton onClick={onCloseModal}>×</CloseModalButton>} {children} </div> </CreateMenu> ); };
-
미해결Slack 클론 코딩[실시간 채팅 with React]
페이지 접속시 다수 렌더링 이슈(slack환경으로 새 프로젝트 구축)
슬랙 프로젝트의 환경설정(웹팩 등)을 토대로 새 프로젝트를 만들고 있습니다.페이지 접속시 console.log를 찍어보니 3번 찍히는 문제가 있습니다.원인 및 해결방법을 모르겠어서 질문드립니다. 아래는 라우터 쪽입니다.login과 signup은 loadble을 사용해서 그런지 1번만 console.log가 찍혔습니다.아래 stockrecode에 해당하는 페이지가 3번 console.log 찍힙니다. 아래는 stockrecode 컴포넌트에 있는 코드입니다. 나머지는 다 주석처리하였습니다.아래는 로 이동시에 표시된 내용입니다. useSwr 사용 부분을 지우면 2번 표시됩니다.<해본 방법>strict-mode를 지우면 되는 글을 보아서 찾아봤는데이미 strict-mode가 없는 상태였습니다. 2.웹팩에서 strict-mode를 false로 하면 되는 글을 보고 해보았는데(관련 링크 https://www.sobyte.net/post/2021-09/webpack-strip-use-strict/)아래처럼 @babel/plugin-transform-modules-commonjs 설치를 하고 적용해보았는데도아래처럼 에러가 생겨서 이 방법으로도 해결하지 못하였습니다 답변주시는데 더 필요한 정보가 있으시면 말씀부탁드립니다.꼭 해결하고 싶은데 제자리만 멤도는거 같아 질문 올립니다. ㅠ
-
해결됨Slack 클론 코딩[실시간 채팅 with React]
baseURL 변경 후에도 같은 내용의 cors 에러가 나옵니다.
안녕하세요 질문드립니다!아직 초반이지만 언젠가부터 cors 에러가 계속 나고 있는데 baseURL 도 아래와 같이 변경했습니다.axios.defaults.baseURL = process.env.NODE_ENV === 'production' ? 'http://localhost:3095' : 'http://localhost:3090'; 제 코드엔 nodebird 라는 글자 자체가 없는데 어디가 잘못된 건가요......
-
미해결Slack 클론 코딩[실시간 채팅 with React]
npm warn 은 고치지 않고 넘어가도 되나요?
안녕하세요 질문 드립니다.npm 설치할 때 이러 경고가 나타났는데, 에러는 아니길래 그냥 지나갔는데,로컬호스트3095 들어가면 계속 이러 에러가 나와서 진행이 안 되고 있어요서버는 잘 연결되었습니다
-
미해결Slack 클론 코딩[실시간 채팅 with React]
워크스페이스 내에서 채널 및 디엠 채팅 시 소켓의 구조가 궁금합니다.
안녕하세요! 강의 정말 잘 듣고 있습니다. 저에게 여러모로 큰 도움이 되고 있는 것 같습니다!다른게 아니라 강의를 들으면서 아직 웹소켓에 대해 잘 몰라서 정확한 로직을 잘 모르겠어서 질문을 드립니다! 소켓을 사용하는 로직이한 클라이언트에 대해 워크스페이스를 바꿀 때 마다 해당 워크스페이스에 대한 소켓을 연결서버는 워크스페이스 별로 소켓을 관리. 채널과 디엠 상관없이 워크스페이스 별로 들어오는 모든 소켓 요청(채팅 내용)을 받아서 전달함클라이언트는 해당 채팅 내용들을 다 받지만 현재 접속해있는 채널이나 dm을 주고 받는 상대에 대한 채팅 내용만 걸러서 화면에 보여줌이렇게 진행되는 것이 맞을까요? 맞다면 혹시 워크스페이스라는 개념이 없이 채팅방만 있거나 1:1 채팅의 기능만 소켓을 사용하여 구현한다고 했을 때는 채팅방 별로 혹은 클라이언트 별로 소켓을 생성해서 구현을 하게 되나요..? 그게 아니라면 보통 어떻게 구현하는지 질문 드리고 싶습니다..! 감사합니다
-
미해결Slack 클론 코딩[실시간 채팅 with React]
DMList 선택 시 무한로딩되는 에러
안녕하세요 제로초님. 강의 잘 듣고 있습니다.다름이 아니라 채널 토글에서 각 채널을 선택하면 정상적으로 이동하지만, 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;
-
해결됨Slack 클론 코딩[실시간 채팅 with React]
서버 켜는 명령어가 안 되요ㅜㅜ
안녕하세요. 서버 켜려고 back 폴더에서 npm run dev 하면 [nodemon] app crashed - waiting for file changes before starting...와 같은 에러가 나옵니다. node.js 버전은 18.16.1 입니다.결국 로컬호스트3095 들어가면:3095/l:1 GET http://localhost:3095/l 500 (Internal Server Error)이런 에러가 나오는데, 이건 서버 접속이 아예 안되었기 때문에 나오는 거겠죠..?
-
미해결Slack 클론 코딩[실시간 채팅 with React]
로그인 화면에서 리다이렉트 시 workspace 목록이 표시되지 않는 문제
안녕하세요. 강의 잘 듣고 있습니다. 다름이 아니라 login 한 후 workspace로 리다이렉트 된 후, workspace 목록이 아래와 같이 나타나지 않는 현상이 발생합니다. 하지만 다른 탭을 다녀오거나, workspace 추가를 하면 다시 정상적으로 아래와 같이 workspace 목록이 나타납니다. 아마 userData를 리다이렉트하면서 불러오지 않아 생기는 문제 같습니다. mutate()를 사용하고, dedupinginterval을 줄여도 문제 해결이 안되는 것 같아 리다이렉트됨과 동시에 다시 userData를 불러오도록 수정해야 할 것 같은데, 이를 구현할 수 있는 방법이 생각이 나지 않습니다. 현재 제 코드 일부 아래에 첨부합니다.App.jsconst App: FC = () => { return ( <Switch> <Redirect exact path="/" to="/login" /> <Route path="/login" component={LogIn} /> <Route path="/signup" component={SignUp} /> <Route path="/workspace/:workspace" component={Workspace} /> </Switch> ); };Login/index.tsxconst LogIn = () => { const {data, error, mutate} = useSWR('/api/users', fetcher, { dedupingInterval: 100000, }); const [logInError, setLogInError] = useState(false); const [email, onChangeEmail] = useInput(''); const [password, onChangePassword] = useInput(''); const onSubmit = useCallback( (e) => { e.preventDefault(); setLogInError(false); axios .post( '/api/users/login', { email, password }, { withCredentials: true }, ) .then((response) => { mutate(response.data, false); //Optimistic UI }) .catch((error) => { setLogInError(error.response?.status === 401); }); }, [email, password], ); if (data === undefined) { return <div>로딩중...</div>; } if (data) { return <Redirect to="/workspace/sleact/channel/일반" />; }Workspace/index.tsximport Menu from "@components/Menu"; import Modal from "@components/Modal"; import CreateChannelModal from "@components/CreateChannelModal"; 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 [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 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); }, []); if(!userData) { return <Redirect to="/login"/> } const toggleWorkspaceModal = useCallback(()=> { setShowWorkspaceModal((prev) => !(prev)); },[]); const onClickAddChannel = useCallback(() => { setShowCreateChannelModal(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={onClickAddChannel}>채널 만들기</button> <button onClick={onLogout}>로그아웃</button> </WorkspaceModal> </Menu> {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} /> </div> ) } export default Workspace
-
미해결Slack 클론 코딩[실시간 채팅 with React]
강의 초기 셋팅 문제
react를 처음 접해보는 취준생입니다.제가 강의를 보면서 따라하려는데 settings/js 디렉토리의 디렉토리명을 front로 변경해서 사용하고 back 디렉토리에서 npm start로 백앤드 서버 실행시키고 따라하라고 하신거 같아서 그렇게 따라하고 있습니다. 현재 localhost:3095로 접근하면 슬리액 페이지가 잘 나오는데 front 디렉토리에서 코드 수정해도 반영이 안고 있는데 혹시 어떤걸 잘 못 하고 있는지 알 수 있을까요?
-
미해결Slack 클론 코딩[실시간 채팅 with React]
DM이 두개씩 보내져요..
안녕하세요.우선 저는 맥북을 사용하고 있습니다.import { VFC, useCallback, useEffect, useRef } from 'react'; import { Form, MentionsTextarea, SendButton, Toolbox } from './styles'; import React from 'react'; import autosize from 'autosize'; interface Props { chat: string; onSubmitForm: (e: any) => void; onChangeChat: (e: any) => void; placeholder?: string; } const ChatBox: VFC<Props> = ({ chat, onSubmitForm, onChangeChat, placeholder }) => { // const onSubmitForm = useCallback(() => {}, []); const textareaRef = useRef(null); useEffect(() => { if (textareaRef.current) { autosize(textareaRef.current); } }, []); const onKeydownChat = useCallback( (e) => { if (e.key === 'Enter') { if (!e.shiftKey) { e.preventDefault(); onSubmitForm(e); } } }, [onSubmitForm], ); return ( <Form onSubmit={onSubmitForm}> <MentionsTextarea id="editor-chat" value={chat} onChange={onChangeChat} onKeyDown={onKeydownChat} placeholder={placeholder} ref={textareaRef} /> <Toolbox> <SendButton className={ 'c-button-unstyled c-icon_button c-icon_button--light c-icon_button--size_medium c-texty_input__button c-texty_input__button--send' + (chat?.trim() ? '' : ' c-texty_input__button--disabled') } data-qa="texty_send_button" aria-label="Send message" data-sk="tooltip_parent" type="submit" disabled={!chat?.trim()} > <i className="c-icon c-icon--paperplane-filled" aria-hidden="true"></i> </SendButton> </Toolbox> </Form> ); }; export default ChatBox; 이건 제가 작성한 ChatBox입니다.import React, { useCallback } from 'react'; import { Container, Header } from './styles'; import useSWR, { useSWRInfinite } from 'swr'; import fetcher from '@utils/fetcher'; import { useParams } from 'react-router'; import gravatar from 'gravatar'; import ChatBox from '@components/ChatBox'; import ChatList from '@components/ChatList'; import useInput from '@hooks/useInput'; import axios from 'axios'; import { IDM } from '@typings/db'; const DirectMessage = () => { const { workspace, id } = useParams<{ workspace: string; id: string }>(); const { data: userData } = useSWR(`/api/workspaces/${workspace}/users/${id}`, fetcher); const { data: myData } = useSWR('/api/users', fetcher); const [chat, onChangeChat, setChat] = useInput(''); const { data: chatData, mutate: mutateChat, revalidate, } = useSWR<IDM[]>(`/api/workspaces/${workspace}/dms/${id}/chats?perPage=20&page=1`, fetcher); const onSubmitForm = useCallback( (e) => { e.preventDefault(); if (chat?.trim()) { axios .post(`/api/workspaces/${workspace}/dms/${id}/chats`, { content: chat, }) .then(() => { revalidate(); setChat(''); console.log('submit'); }) .catch((error) => { console.log(error); }); } }, [chat], ); if (!userData || !myData) { return null; } return ( <Container> <Header> <img src={gravatar.url(userData.email, { s: '24px', d: 'retro' })} alt={userData.nickname} /> <span>{userData.nickname}</span> </Header> <ChatList chatData={chatData} /> <ChatBox chat={chat} onChangeChat={onChangeChat} onSubmitForm={onSubmitForm} /> </Container> ); }; export default DirectMessage; 이건 DirectMessage 입니다.e.preventDefault()로 기본 이벤트를 막아줬는데도 DM을 엔터로 전송하면 (한글로만 전송하면 2개씩 보내져요...!)어떨때는 2개가 보내지고 어떨때는 1개가 보내져요... 네트워크나 콘솔에도 2개씩 뜨고요.. 전송버튼을 눌렀을때는 1개만 보내집니다.