묻고 답해요
130만명의 커뮤니티!! 함께 토론해봐요.
인프런 TOP Writers
-
해결됨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]
로그인 화면에서 리다이렉트 시 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개만 보내집니다.
-
미해결Slack 클론 코딩[실시간 채팅 with React]
오류 문의
강의를 따라하는 도중에 npx sequelize db:create를 입력하였을때는 정상적으로 "Database sleact create."라는 메세지가 확인됬습니다. 그 이후로 yarn dev를 실행하니 아래와 같은 오류가 확인됬습니다.이유가 뭘까요?ㅜㅜ
-
미해결프론트엔드 개발환경의 이해와 실습 (webpack, babel, eslint..)
폴더를 prettier로 돌렸을 때 나오는 에러 구문에 대한 질문
npx prettier ./src --write해당 구문이 적용되나요? 첫 시도에서는 성공했는데 그 이후로 부터는 실패 구문이 아래와 같이 뜨더라구요.[error] No matching files. Patterns tried: ./src !**/node_modules/** !./node_modules/** !**/.{git,svn,hg}/** !./.{git,svn,hg}/**안내 답변 가능하신가요?
-
미해결Slack 클론 코딩[실시간 채팅 with React]
Direct Messages에 값이 없어요
안녕하세요.import React from 'react'; import { Container, Header } from './styles'; import useSWR from 'swr'; import fetcher from '@utils/fetcher'; import { useParams } from 'react-router'; import gravatar from 'gravatar'; const DirectMessage = () => { const { workspace, id } = useParams<{ workspace: string; id: string }>(); const { data: userData } = useSWR(`api/workspaces/${workspace}/members/${id}`, fetcher); const { data: myData } = useSWR('api/users', fetcher); if (!userData || !myData) { return null; } return ( <Container> <Header> <img src={gravatar.url(userData.email, { s: '24px', d: 'retor' })} alt={userData.nickname} /> <span>{userData.nickname}</span> </Header> {/* <ChatList /> <ChatBox /> */} </Container> ); }; export default DirectMessage; 이건 DirectMessage입니다.// import EachDM from '@components/EachDM'; // import useSocket from '@hooks/useSocket'; import { CollapseButton } from '@components/DMList/styles'; 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 } = 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); // }); // console.log('socket on dm', socket?.hasListeners('dm'), socket); // return () => { // 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 p-channel_sidebar__section_heading_expand--show_more_feature 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); const count = countList[member.id] || 0; <NavLink key={member.id} activeClassName="selected" to={`/workapce/${workspace}/dm/${member.id}`} onClick={resetCount(member.id)} > ; <i className={`c-icon p-channel_sidebar__presence_icon p-channel_sidebar__presence_icon--dim_enabled 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 className={count > 0 ? 'bold' : undefined}>{member.nickname}</span> {member.id === userData?.id && <span> (나)</span>} {count > 0 && <span className="count">{count}</span>} </NavLink>; // return <EachDM key={member.id} member={member} isOnline={isOnline} />; })} </div> </> ); }; export default DMList; 이건 DMList입니다. 현재 DM페이지 만들기 강의를 듣고 있는데, DM리스트에 사용자가 하나도 표시가 되지 않는데 원래 지금 강의까지는 표시가 되지 않는게 맞나요? 워크스페이스 초대, 채널 멤버 초대해도 에러는 발생하지 않는데 DM리스트에 추가는 다음 강의에서 진행하나요? 아니면 지금도 되야하는게 정상인가요..?
-
미해결Slack 클론 코딩[실시간 채팅 with React]
사용자 초대 모달에서 에러가 발생했습니다.
import fetcher from '@utils/fetcher'; import axios from 'axios'; import React, { VFC, useCallback, useState } from 'react'; import { Redirect, Route, Switch, useParams } from 'react-router'; import useSWR from 'swr'; import { Header, ProfileImg, RightMenu, WorkspaceWrapper, WorkspaceName, Workspaces, Channels, Chats, MenuScroll, ProfileModal, LogOutButton, WorkspaceButton, AddButton, WorkspaceModal, } from './styles'; import gravatar from 'gravatar'; import loadable from '@loadable/component'; import Menu from '@components/Menu'; const Channel = loadable(() => import('@pages/Channel')); const DirectMessage = loadable(() => import('@pages/DirectMessage')); 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 InviteWorkspaceModal from '@components/InviteWorkspaceModal'; import InviteChannelModal from '@components/InviteChannelModal'; // import ChannelList from '@components/ChannelList'; import DMList from '@components/DMList'; import ChannelList from '@components/ChannelList'; // FC타입안에 children이 알아서 들어있음 // children 안쓸거면 VFC const Workspace: VFC = () => { const [showUserMenu, setShowUserMenu] = useState(false); const [showCreateWorkspaceModal, setShowCreateWorkspaceModal] = useState(false); const [showInviteWorkspaceModal, setShowInviteWorkspaceModal] = useState(false); const [showInviteChannelModal, setShowInviteChannelModal] = useState(false); const [newWorkspace, onChangeNewWorkspace, setNewWorkspace] = useInput(''); const [newUrl, onChangeNewUrl, setNewUrl] = useInput(''); const [showWorkspaceModal, setShowWorkspaceModal] = useState(false); const [ShowCreateChannelModal, setShowCreateChannelModal] = useState(false); const { workspace } = useParams<{ workspace: string }>(); // 사용자 데이터 가져오기 const { data: userData, error, revalidate, mutate, } = useSWR<IUser | false>('/api/users', fetcher, { dedupingInterval: 2000, }); // channel 데이터 가져오기 const { data: channelData } = useSWR<IChannel[]>(userData ? `/api/workspaces/${workspace}/channels` : null, fetcher); // 워크스페이스에 있는 멤버 데이터 const { mutate: revalidateMembers } = useSWR<IUser[]>( userData ? `/api/workspaces/${workspace}/members` : null, fetcher, ); // 로그아웃 const onLogout = useCallback(() => { axios .post('/api/users/logout', null, { withCredentials: true, }) .then((response) => { mutate(response.data); // 기존에 받은 데이터를 mutate의 data에 담음 }) .catch((error) => { console.log(error); }); }, []); // 프로필 누르면 메뉴 보이기 const onClickUserProfile = useCallback(() => { setShowUserMenu((prev) => !prev); }, []); // 프로필 닫기 const onCloseUserProfile = useCallback((e) => { e.stopPropagation(); setShowUserMenu(false); }, []); // 워크스페이스 모달 열기 const onClickCreateWorkspace = useCallback(() => { setShowCreateWorkspaceModal(true); }, []); // 워크스페이스 모달 닫기 const onCloseModal = useCallback(() => { setShowCreateWorkspaceModal(false); setShowCreateChannelModal(false); setShowInviteChannelModal(false); setShowInviteWorkspaceModal(false); }, []); // 워크스페이스 생성 const onCreateWorkspace = useCallback( (e) => { e.preventDefault(); // trim() 안넣으면 띄어쓰기 넣으면 걍 통과됨 if (!newWorkspace || !newWorkspace.trim()) return; if (!newUrl || !newUrl.trim()) return; axios .post( '/api/workspaces', { workspace: newWorkspace, url: newUrl, }, { withCredentials: true, }, ) .then(() => { revalidate(); setShowCreateWorkspaceModal(false); setNewWorkspace(''); setNewUrl(''); }) .catch((error) => { console.log(error); toast.error(error.response?.data, { position: 'bottom-center' }); }); }, [newWorkspace, newUrl], ); // 워크스페이스 사용자 초대 const onClickInviteWorkspace = useCallback(() => { setShowInviteWorkspaceModal(true); }, []); // Channel // 토글 const toggleWorkspaceModal = useCallback(() => { setShowWorkspaceModal((prev) => !prev); }, []); // 채널 만들기 const onClickAddChannel = useCallback(() => { setShowCreateChannelModal(true); }, []); if (!userData) { return <Redirect to="/login" />; } return ( <div> <Header> <RightMenu> <span onClick={onClickUserProfile}> <ProfileImg src={gravatar.url(userData.nickname, { s: '28px', d: 'retro' })} alt={userData.nickname} /> {showUserMenu && ( <Menu style={{ right: 0, top: 38 }} show={showUserMenu} onCloseModal={onCloseUserProfile}> <ProfileModal> <img src={gravatar.url(userData.nickname, { 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" 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; 이건 제가 작성한 WorkSpace 입니다.import Modal from '@components/Modal'; import useInput from '@hooks/useInput'; import { Button, Input, Label } from '@pages/SignUp/styles'; import { IUser } from '@typings/db'; import fetcher from '@utils/fetcher'; import axios from 'axios'; import React, { FC, useCallback } from 'react'; import { useParams } from 'react-router'; import { toast } from 'react-toastify'; import useSWR from 'swr'; interface Props { show: boolean; onCloseModal: () => void; setShowInviteChannelModal: (flag: boolean) => void; } const InviteChannelModal: FC<Props> = ({ show, onCloseModal, setShowInviteChannelModal }) => { const { workspace, channel } = useParams<{ workspace: string; channel: string }>(); const [newMember, onChangeNewMember, setNewMember] = useInput(''); const { data: userData } = useSWR<IUser>('/api/users', fetcher); const { revalidate: revalidateMembers } = useSWR<IUser[]>( userData ? `/api/workspaces/${workspace}/channels/${channel}/members` : null, fetcher, ); console.dir(channel); const onInviteMember = useCallback( (e) => { e.preventDefault(); if (!newMember || !newMember.trim()) { return; } axios .post(`/api/workspaces/${workspace}/channels/${channel}/members`, { email: newMember, }) .then(() => { revalidateMembers(); setShowInviteChannelModal(false); setNewMember(''); }) .catch((error) => { console.dir(error); toast.error(error.response?.data, { position: 'bottom-center' }); }); }, [channel, newMember, revalidateMembers, setNewMember, setShowInviteChannelModal, workspace], ); return ( <Modal show={show} onCloseModal={onCloseModal}> <form onSubmit={onInviteMember}> <Label id="member-label"> <span>채널 멤버 초대</span> <Input id="member" value={newMember} onChange={onChangeNewMember} /> </Label> <Button type="submit">초대하기</Button> </form> </Modal> ); }; export default InviteChannelModal; 이건 제가 작성한 InviteChannelModal입니다.xhr.js:210 GET http://localhost:3090/api/workspaces/sleact/channels/undefined/members 404 (Not Found) dispatchXhrRequest @ xhr.js:210 xhrAdapter @ xhr.js:15 dispatchRequest @ dispatchRequest.js:58 request @ Axios.js:108 Axios.<computed> @ Axios.js:129 wrap @ bind.js:9 fetcher @ fetcher.ts:18 eval @ use-swr.js:392 step @ use-swr.js:43 eval @ use-swr.js:24 eval @ use-swr.js:18 __awaiter @ use-swr.js:14 eval @ use-swr.js:344 softRevalidate @ use-swr.js:532 onFocus @ use-swr.js:550 revalidate_1 @ use-swr.js:73 eval @ use-swr.js:77 eval @ web-preset.js:29그런데 위와 같은 에러가 발생합니다.콘솔에 channel을 출력해도 undefined가 뜹니다.. 그런데 url을 보면 http://localhost:3090/workspace/sleact/channel/테스트채널 이렇게 채널 명이 표시가 되는데 도대체 뭐 때문에 undefined라고 뜨는지 모르겠습니다... 아무리 찾아봐도 알 수가 없어서 글 남깁니다... 분명 WorkSpace의 route에도 오타가 없고 url도 표시가 잘되는데 왜 undefined일까요...
-
미해결Slack 클론 코딩[실시간 채팅 with React]
원래 깃허브에 올려주신 파일과 강의 파일이 다른가요...?
안녕하세요.먼저 제로초님 강의 보면서 공부 열심히 하고 있습니다. 감사합니다!. 현재 DMList 만드는 중인데 깃허브에 올려주신 front의 DMList와 제로초님이 강의하면서 작성하시는 DMList의 코드가 서로 다른 부분이 꽤 있던데 원래 그런가요...?영상을 정지해가면서 수정하고 있지만 안보이는 코드가 있어서 많이 헷갈리네요 ㅠㅠ
-
미해결프론트엔드 개발환경의 이해와 실습 (webpack, babel, eslint..)
해쉬값과 캐쉬 갱신
'웹팩 기본편 - 자주 사용하는 로더'를 학습하고 있습니다.로더들 중에, file-loader에 대해 학습하다가 질문이 생겼습니다.웹팩이 .png파일을 발견하면 file-loader를 실행할 것이고, 로더가 동작하고 나면 아웃풋에 설정한 경로로 이미지 파일을 복사합니다. 그리고 파일명이 해쉬코드로 변경 됩니다. 수업시간에 강사님께서 쿼리스트링이 매번 바뀌는 해시값으로 설정된다고 하셨는데, 그러면 빌드를 할때마다 해시값이 매번 바뀌는 건가요?그리고 이게 캐시 관점에서 어떤 도움을 주는지 궁금합니다. 파일명이 해쉬코드로 변경되는게 캐쉬 갱신을 위한 처리로 보인다고 하셨는데, 정확히 어떤점에서 캐쉬 갱신에 도움을 주는지 이해가 잘 안가서요ㅠㅠ알려주시면 감사하겠습니다!
-
미해결프론트엔드 개발환경의 이해와 실습 (webpack, babel, eslint..)
Hash 에러 발생
안녕하세요 강사님.제가 npm run build로 빌드를 하려고 할때마다 다음과 같은 에러가 생기더라구요..[에러 내용]$ npm run build > sample@1.0.0 build > webpack node:internal/crypto/hash:69 this[kHandle] = new _Hash(algorithm, xofLen); ^ Error: error:0308010C:digital envelope routines::unsupported at new Hash (node:internal/crypto/hash:69:19) at Object.createHash (node:crypto:138:10) at module.exports (C:\Users\power\OneDrive\inflearn\devEnv\sample\node_modules\webpack\lib\util\createHash.js:135:53) at NormalModule._initBuildHash (C:\Users\power\OneDrive\inflearn\devEnv\sample\node_modules\webpack\lib\NormalModule.js:417:16) at handleParseError (C:\Users\power\OneDrive\inflearn\devEnv\sample\node_modules\webpack\lib\NormalModule.js:471:10) at C:\Users\power\OneDrive\inflearn\devEnv\sample\node_modules\webpack\lib\NormalModule.js:503:5 at C:\Users\power\OneDrive\inflearn\devEnv\sample\node_modules\webpack\lib\NormalModule.js:358:12 at C:\Users\power\OneDrive\inflearn\devEnv\sample\node_modules\loader-runner\lib\LoaderRunner.js:373:3 at iterateNormalLoaders (C:\Users\power\OneDrive\inflearn\devEnv\sample\node_modules\loader-runner\lib\LoaderRunner.js:214:10) at Array.<anonymous> (C:\Users\power\OneDrive\inflearn\devEnv\sample\node_modules\loader-runner\lib\LoaderRunner.js:205:4) at Storage.finished (C:\Users\power\OneDrive\inflearn\devEnv\sample\node_modules\enhanced-resolve\lib\CachedInputFileSystem.js:55:16) at C:\Users\power\OneDrive\inflearn\devEnv\sample\node_modules\enhanced-resolve\lib\CachedInputFileSystem.js:91:9 at C:\Users\power\OneDrive\inflearn\devEnv\sample\node_modules\graceful-fs\graceful-fs.js:123:16 at FSReqCallback.readFileAfterClose [as oncomplete] (node:internal/fs/read_file_context:68:3) { opensslErrorStack: [ 'error:03000086:digital envelope routines::initialization error' ], library: 'digital envelope routines', reason: 'unsupported', code: 'ERR_OSSL_EVP_UNSUPPORTED' } Node.js v20.3.0위의 에러가 왜 발생했는지 찾아봤고 원인과 해결방법을 다음 링크에 정리했습니다.[정리 링크] https://github.com/SeoMiYoung/Learning/issues/6결론적으로, npm run build를 하기전에export NODE_OPTIONS=--openssl-legacy-provider위의 명령어를 한번 쳐주고, npm run build를 하면, 제대로 빌드가 작동되더라구요..!그런데 VSCode를 다시 껐다 켜면, 또 다시 위의 명령어를 입력해준 다음에 빌드를 해야되더라구요..아마, 위의 명령어는 Node.js실행시, OpenSSL의 레거시 프로바이더를 사용하도록 환경변수를 설정해주는 명령어인데, 해당 명령어는 임시적으로 현재 세션에서만 레거시 프로바이더를 사용하도록 해주는 명령어이다보니, VSCode를 껐다 켜는것처럼, 재부팅하거나 새로운 세션을 시작하는 경우에는 환경변수가 초기화되어 다시 설정해줘야되는 것 같아서 그런 것 같더라구요..!재부팅할때마다 빌드하기전에 해당 명령어를 치고 시작하는게 번거롭습니다..혹시 해결 방법이 없을까요??[참고: 버전 정보]{ "name": "sample", "version": "1.0.0", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "webpack" }, "author": "", "license": "ISC", "description": "", "dependencies": { "css-loader": "^4.3.0", "file-loader": "^6.2.0", "react": "^18.2.0", "style-loader": "^1.3.0" }, "devDependencies": { "webpack": "^4.46.0", "webpack-cli": "^3.3.12" } }
-
미해결프론트엔드 개발자를 위한 웹팩
바벨없이 import 구문을 빌드한 의도파악하기
안녕하세요 판교님, 강의 섹션에서 질문하신 *다른 분의 글(es6와 es5...)을 보고 글을 남겨요.3:27 부분에서 index.js 파일에서는 ES6 구문을 쓰고,import _ from 'lodash'webpack.config.js 파일에서는 ES5(commonJS) 구문을 var path = require('path')쓰셨더라구요, 모든 브라우저에서 호환 가능하려면 바벨은 필수인데 최신 브라우저는 네이티브 모듈을 지원하니까 별도의 빌드 도구 없이 모듈을 로드할 수 있으니 편의상 진행하신거라고 이해하면 될까요?
-
해결됨프론트엔드 개발환경의 이해와 실습 (webpack, babel, eslint..)
webpack.config.js에서의 CommonJS방식에 대한 질문
안녕하세요!수업시간에 보면 webpack.config.js 웹팩 설정파일을 CommonJS방식으로 작성하였는데요,웹팩은 Node.js에서 동작하는 프로그램이고,Node.js가 사용하는 모듈시스템이 CommonJS이므로 웹팩 설정파일을 CommonJS로 작성한 건 이해했습니다.그런데 문득 드는 궁금증이,CommonJS 모듈 시스템을 채택했던 NodeJS 환경에서 ES Module을 사용하려면 Babel과 같은 트랜스파일러(transpiler)를 사용했어야 했는데요. NodeJS 버전 13.2부터 ES모듈 시스템에 대한 정식 지원이 시작됨에 따라 다른 도구 없이 NodeJS에서 손쉽게 ES Module을 사용할 수 있게 되었잖아요..?그렇다면 노드버전 13.2부터는 webpack.config.js 를 CommonJS가 아닌, ES Module방식으로 작성해도 상관없는건가요?
-
해결됨프론트엔드 개발환경의 이해와 실습 (webpack, babel, eslint..)
웹팩 버전
안녕하세요.현재 웹팩의 가장 최신버전이 5버전으로 알고 있는데,해당 강의에서는 4버전으로 진행하더라고요.제가 잘 몰라서 그런데.. 4버전으로 공부해도 상관은 없겠죠?
-
해결됨프론트엔드 개발환경의 이해와 실습 (webpack, babel, eslint..)
웹팩-cli 버전을 명시하지 않으면 왜 에러가 날까요?
안녕하세요? 웹팩 cli를 처음 깔아보는 수강생입니다..npm install -D webpack@4 webpack-cli위의 코드처럼 그냥 webpack-cli를 깔면, 에러가 발생하는데요...그래서수업시간에 webpack-cli를 버전3을 사용한다는 글을 보고, 다음과 같이 명령어를 바꿔서 쳐보았더니npm install -D webpack@4 webpack-cli@3정상적으로 설치가 되었습니다. 왜 버전을 명시해주지 않으면 에러가 발생하나요?
-
해결됨프론트엔드 개발환경의 이해와 실습 (webpack, babel, eslint..)
import해오는 두가지 방법에 대해 제가 잘 이해한건지 확인받고 싶습니다!!
강사님께서, 수업중에 import해오는 방법을 두가지를 언급하셨는데요,import * as math from './math.js'; // [방법1] 모든 export를 math라는 객체로 가져오기 import { sum } from './math.js'; // [방법2] sum함수만 가져오기방법1로 import할 경우, sum함수를 사용하기 위해서 math.sum(,) 이렇게 써야하지만,방법2로 import할 경우, 그냥 바로 sum(,)으로 사용하면 되나요? 저는 일단 그렇게 이해하고 있는데그러면 import * as math1 from './math1.js'; import * as math2 from './math2.js';이렇게 math1.js와 math2.js에서 각각 sum함수를 만들고, 그걸 app.js에서 위와 같이 import해서 사용했을 때,math1.sum(,) 그리고 math2.sum(,) 이렇게 쓰면 되니깐, 이름이 sum으로 같아도 충돌이 안나겠네요? 그러나 방법2방식으로 import { sum } from './math1.js'; import { sum } from './math2.js';이렇게 가져온다면, 둘다 sum(,)으로 호출하게 되니깐, 이름 충돌이 발생하겠네요?제가 이해한것이 맞는지 확인부탁드려요ㅠㅠ
-
미해결Slack 클론 코딩[실시간 채팅 with React]
useSelector가 초기값만 읽고, 상태변화를 읽지못함
안녕하세요. 선생님늦은 시간인데도 답변해주셔서 감사합니다(꾸벅)선생님께서 말씀해주신대로 useSelector를 써봤는데요.useSelector가 초기값만 읽고, updateReducer에 의해 상태가 업데이트되면,업데이트 된 값을 읽지 못합니다....1. console.log(title)이라고 썼는데, 아무것도 뜨지 않습니다2. Modal 컴포넌트에 제목을 입력을 해도 상태 변화를 읽지를 못합니다.어디서 잘못 된걸까요??const TodoItem = () =>{ const [state, dispatch] = useReducer(checkReducer, todoItem) const id = useSelector((state)=> state.update.id); const title = useSelector((state)=> state.update.title) const content = useSelector((state)=> state.update.content); const isDone = useSelector((state)=> state.update.isDone) const handleCheck = (id) =>{ dispatch(checkBox(id)); }; console.log(title) return( <SeveralItemContainer> <TodoItemContainer key={id}> <TitleContainer> <img className ="dot" src={dot} alt="icon"></img> <div className="title">{title}</div> <img onClick={ () => {handleCheck(id)}} className="noncheck" src = {isDone ? check: noncheck} alt="icon"></img> </TitleContainer> <div className="contentcontainer"> <div className="content">{content}</div> </div> </TodoItemContainer> </SeveralItemContainer> ) }; export default TodoItem; export const updateReducer = (state=initialState, action)=>{ switch(action.type){ case 'UPDATE_TITLE': return{...state, title: action.payload}; case 'UPDATE_STARTDATE': return{...state, startDate: action.payload}; case 'UPDATE_ENDDATE': return{...state, endDate:action.payload}; case 'UPDATE_CONTENT': return {...state, content: action.payload}; case 'ADD_ITEM': return {...state, id: Number(state.id)+1} //여기서 todoItem을 바로 쓸 수 없다. state나 action관련된 변수만 넣어야 한다 // 배열은 가능하지만, 객체는 전개연산자(...)를 쓸 수 없다 default: return state; } }; const rootReducer = combineReducers({ update: updateReducer, check: checkReducer, }); export default rootReducer; const Modal = ({onClick}) =>{ const [state, dispatch] = useReducer(updateReducer, initialState) const handleModalClick = (e) => { e.stopPropagation(); // 이벤트 버블링 막기 }; //사용자가 입력한 값을 추적하는 함수 const handleInputChange = (e) =>{ const {name, value} = e.target; dispatch({type: `UPDATE_${name.toUpperCase()}`, payload: value}) }; //클릭으로 적은 모든 내용을 한번에 전달 const addItem = () =>{ dispatch({type:'ADD_ITEM'}); }; return( <ModalBackGround onClick={onClick}> <ModalView onClick={handleModalClick} > <div className = "titleContainer"> <img className="dot" src = {dot} alt="icon"></img> <input value ={state.title} name = "title" className="title" placeholder="메모 제목" onChange={handleInputChange}></input> <img onClick={addItem} className="edit" src={edit} alt="icon"></img> </div> <div className="date"> <div className='일자'>일자</div> <input value ={state.startDate} name ="startDate" className ="start" placeholder="시작일" onChange={handleInputChange}></input> <div>~</div> <input value ={state.endDate} name= "endDate" className ="end" placeholder="종료일" onChange={handleInputChange}></input> </div> <div className='contentContainer'> <textarea value ={state.content} name ="content" className ="content" placeholder="메모" onChange={handleInputChange}> </textarea> </div> </ModalView> </ModalBackGround> ) }; export default Modal;