묻고 답해요
141만명의 커뮤니티!! 함께 토론해봐요.
인프런 TOP Writers
-
미해결Slack 클론 코딩[실시간 채팅 with React]
배포후 로컬에서 Index.html 열면 작동이 잘 안하는 이유
백엔드에 배포하려고 파일 넘기기 전에,,빌드가 잘 되었는지 테스트해보려고 (백엔드 먼저 켜두고)index.html을 그냥 브라우저로 열면 백엔드와 잘 연동될줄 알았는데, 첫 화면부터 보이지가 않아서요. 그 이유가 궁금합니다.이유가 혹시,,, 웹펙에서devServer: { historyApiFallback: true, // react router(원래 SPA에서는 3090/ 주소밖에 모른다. 뒤에 /login같은 경로는 우리가 가짜로 만들어낸건데 History api(History 기본함수)가 가짜주소를 만들어주는 거다.) port: 3090, devMiddleware: { publicPath: '/dist/' }, static: { directory: path.resolve(__dirname) }, proxy: { '/api/': { //프론트에서 /api/로 보내는 요청은 주소를 3095로 바꿔서 보내겠다 target: 'http://localhost:3095', changeOrigin: true, }, }, },위와 같이 dev모드일때는 3090포트 개발서버를 항상 켜두는데, 이처럼 뭔가 서버가 계속 켜있어야 프론트가 잘 작동 가능한건가요?? (그래서 배포할때 백엔드서버는 항상 켜있으니까 그냥 넘겨주면 된다는게 이해가 되는거 같아서요) 만약 그렇다면 프론트인데 빌드 결과물이 있는데 왜 계속 프론트만을 위한 서버가 켜있어야 하는지 궁금합니다,,저는 배포파일이 실행파일마냥 그냥 index.html 실행하면 (백엔드 켜져있어서 통신할 수 있다는 가정하에) 알아서 잘 되는줄 알았거든요,,
-
미해결Slack 클론 코딩[실시간 채팅 with React]
Cannot read properties of undefined (reading 'map')
제로초님, 코드를 따라친 후에 로그아웃을 하고 다시 로그인 하면 이런 에러메세지가 뜹니다.그런데 네트워크 탭을 보면 로그인이 정상적으로 된거 같아서 새로고침을 하면 에러 메세지가 사라지고 슬랙에서 로그인된 화면이 제대로 뜹니다.근데 또 여기서 워크스페이스를 생성하려고 하면 콘솔에 axioserror메세지가 떠서 어떻게 해야될지 모르겠습니다..Workspace/index.tsximport axios from "axios"; import React, { FC, useCallback, useState } from "react"; import useSWR from 'swr'; import fetcher from "@utils/fetcher"; import { Navigate, Routes, Route } from "react-router-dom"; import { AddButton, Channels, Chats, Header, LogOutButton, MenuScroll, ProfileImg, ProfileModal, RightMenu, WorkspaceButton, WorkspaceName, Workspaces, WorkspaceWrapper } from "@layouts/Workspace/style"; import gravatar from 'gravatar'; import loadable from '@loadable/component'; import Menu from "../../components/Menu"; import Modal from "../../components/Modal"; import { Link } from "react-router-dom"; import { IUser } from "@typings/db"; import { Button, Input, Label } from "@pages/SignUp/styles"; import useInput from "@hooks/useInput"; import {toast} from 'react-toastify'; const Channel = loadable(() => import('@pages/Channel')); const DirectMessage = loadable(() => import('@pages/DirectMessage')); const Workspace: FC<React.PropsWithChildren<{}>> = ({children}) => { const [showUserMenu, setShowUserMenu] = useState(false); const [showCreateWorkspaceModal, setShowCreateWorkspaceModal] = useState(false); const [newWorkspace, onChangeNewWorkspace, setNewWorkspace] = useInput(''); const [newUrl, onChangeNewUrl, setNewUrl] = useInput(''); // revalidate = 서버로 요청 다시 보내서 데이터를 다시 가져옴 // mutate = 서버에 요청 안보내고 데이터를 수정 const {data: userData, error, mutate} = useSWR<IUser | false>('/api/users', fetcher, { dedupingInterval: 2000, }); const onLogout = useCallback(() => { axios.post('/api/users/logout', null , { withCredentials: true, }) .then(() => { mutate(false, false); }) }, []); const onClickUserProfile = useCallback((e: any) => { e.stopPropagation(); setShowUserMenu((prev) => !prev); }, []) const onClickCreateWorkspace = useCallback(() => { setShowCreateWorkspaceModal(true); }, []) const onCreateWorkspace = useCallback((e: any) => { 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); }, []) if(!userData) { return <Navigate to="/login" /> } if(!userData) return null; 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.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>Sleact</WorkspaceName> <MenuScroll>menu scroll</MenuScroll> </Channels> <Chats> <Routes> <Route path="/channel" element={<Channel />} /> <Route path="/dm" element={<DirectMessage />} /> </Routes> </Chats> {/* Input이 들어있으면 별도의 컴포넌트로 빼는 것을 추천(input에 글자를 입력할 때마다 여기 있는 함수들이 다 리렌더링 되기 때문에 비효율적) */} </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>워크스페이스 이름</span> <Input id="workspace" value={newUrl} onChange={onChangeNewUrl} /> </Label> <Button type="submit">생성하기</Button> </form> </Modal> </div> ) } export default Workspace;Modal/index.tsximport React, { useCallback, FC } from "react"; import { CloseModalButton, CreateModal } from "./style"; interface Props { show: boolean; onCloseModal: () => void; children: React.ReactNode; } const Modal: FC<Props> = ({show, children, onCloseModal}) => { const stopPropagation = useCallback((e: any) => { e.stopPropagation() }, []); if(!show){ return null; } return( <CreateModal onClick={onCloseModal}> <div onClick={stopPropagation}> <CloseModalButton onClick={onCloseModal}>×</CloseModalButton> {children} </div> </CreateModal> ); }; export default Modal;swr2.0 버전, react v18, typescript v18swr을 최신버전 사용해서 revalidate대신 mutate를 사용했는데 제가 잘못 사용한건지 모르겠습니다.
-
미해결Slack 클론 코딩[실시간 채팅 with React]
Module not found
제로초님 영상을 보고 따라하는 도중 Menu의 index.tsx작성 후에 Workspace로 옮겼는데, Module을 찾을 수 없다고 뜨는데, 어디가 잘못 된건지 모르겠습니다... components/Menu/index.tsximport React, { FC } from 'react'; import { CreateMenu } from './style'; const Menu: FC<React.PropsWithChildren<{}>> = ({ children }) => { return ( <CreateMenu> <div>menu</div> {children} </CreateMenu> ); }; export default Menu; Workspace/index.tsximport React, { FC } from 'react'; import { CreateMenu } from './style'; const Menu: FC<React.PropsWithChildren<{}>> = ({ children }) => { return ( <CreateMenu> <div>menu</div> {children} </CreateMenu> ); }; export default Menu; 에러메세지입니다.혹시 몰라서 터미널에 뜬 에러메세지도 첨부하겠습니다
-
미해결Slack 클론 코딩[실시간 채팅 with React]
Request failed with status code 404
제로초님, layouts폴더에 App.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 Channel = loadable(() => import('@pages/Channel')); const App = () => { return ( <Routes> <Route path="/" element={<Navigate replace to="/login" />} /> <Route path="/login" element={<LogIn />} /> <Route path="/signup" element={<SignUp />} /> <Route path="/workspace/channel" element={<Channel />} /> </Routes> ) } export default App;Route의 4번째줄 path에 /workspace로 하면 로그아웃 할 때 제대로 작동하는데 저렇게 workspace/channel로 코드를 작성하면 로그아웃 할 때, 아래처럼 뜹니다/를 하나만 붙여야 되는건가요?나머지 코드들은 변경하지 않았습니다.Login 폴더 index.tsximport useInput from "@hooks/useInput"; import axios from "axios"; import React, { useCallback, useState } from "react"; import { Form, Label, Input, LinkContainer, Button, Header, Error} from './styles'; import {Link, Navigate} from 'react-router-dom'; import useSWR from 'swr'; import fetcher from "@utils/fetcher"; const LogIn = () => { const {data, error, mutate} = useSWR('/api/users', fetcher); const [logInError, setLogInError] = useState(false); const [email, onChangeEmail] = useInput(''); const [password, onChangePassword] = useInput(''); const onSubmit = useCallback((e: any) => { e.preventDefault(); setLogInError(false); axios .post( '/api/users/login', {email, password}, {withCredentials: true}, ) .then((response) => { mutate(response.data, false); }) .catch((error) => { setLogInError(error.response?.data?.statusCode === 401); }) }, [email, password, mutate]); if(data === undefined) { return <div>로딩중...</div> } if(data) { return <Navigate to="/workspace/channel" /> } return ( <div id="container"> <Header>Sleact</Header> <Form onSubmit={onSubmit}> <Label id="email-label"> <span>이메일 주소</span> <div> <Input type="email" id="email" name="email" value={email} onChange={onChangeEmail} /> </div> </Label> <Label id="password-label"> <span>비밀번호</span> <div> <Input type="password" id="password" name="password" value={password} onChange={onChangePassword} /> </div> {logInError && <Error>이메일과 비밀번호 조합이 일치하지 않습니다.</Error>} </Label> <Button type="submit">로그인</Button> </Form> <LinkContainer> 아직 회원이 아니신가요? <Link to="/signup">회원가입 하러가기</Link> </LinkContainer> </div> ); }; export default LogIn; workspace.tsximport axios from "axios"; import React, { FC, useCallback } from "react"; import useSWR from 'swr'; import fetcher from "@utils/fetcher"; import { Navigate } from "react-router-dom"; const Workspace: FC<React.PropsWithChildren<{}>> = ({children}) => { // revalidate = 서버로 요청 다시 보내서 데이터를 다시 가져옴 // mutate = 서버에 요청 안보내고 데이터를 수정 const {data, error, mutate} = useSWR('/api/users', fetcher); const onLogout = useCallback(() => { axios.post('api/users/logout', null , { withCredentials: true, }) .then(() => { mutate(false, false); }) }, []); if(data === false) { return <Navigate to="/login" /> } return( <div> <button onClick={onLogout}>로그아웃</button> {children} </div> ) } export default Workspace;swr은 2버전입니다.
-
미해결Slack 클론 코딩[실시간 채팅 with React]
Cannot read properties of undefined (reading 'data')
제로초님, 전 강의 듣고 할 때만 해도 잘 됐는데 오늘 쉬고 다시 해보니까 이런 오류 메세지가 뜹니다.해결해보려고 해도 잘 안되네요..Cannot read properties of undefined (reading 'data')SignUp 폴더에서 index.tsx에 문제가 있다고 떠서 코드 올립니다import useInput from "@hooks/useInput"; import React, { useCallback, useState } from "react"; import { Form, Label, Input, LinkContainer, Button, Header, Error, Success} from './styles' import axios from "axios"; import {Link} from 'react-router-dom'; // 타입스크립트는 간단하게 말하면 변수, 매개변수, 리턴 값에 타입을 붙여줌 // 변수에는 딱히 타입을 붙여주지 않아도 된다 타입스크립트가 알아서 추론하기 때문에(리턴값도 마찬가지) 그러나 매개변수에는 붙여주어야됨 const SignUp = () => { const [email, onChangeEmail] = useInput('');//useInput은 커스텀 훅 const [nickname, onChangeNickName] = useInput(''); const [password, setPassword] = useState(''); const [passwordCheck, setPasswordCheck] = useState(''); const [mismatchError, setMismathError] = useState(false); const [signUpError, setSignUpError] = useState(''); const [signUpSuccess, setSignUpSuccess] = useState(false); const onChangePassword = useCallback((e: any) => { setPassword(e.target.value); setMismathError(e.target.value !== passwordCheck); // 함수 기준으로 외부 변수만 deps에 적어줌 내부 변수는 x }, [passwordCheck]); const onChangePasswordCheck = useCallback((e: any) => { setPasswordCheck(e.target.value); setMismathError(e.target.value !== password) }, [password]); const onSubmit = useCallback( (e: any) => { e.preventDefault(); if (!mismatchError && nickname) { console.log('서버로 회원가입하기'); setSignUpError(''); setSignUpSuccess(false); axios .post('/api/users', { email, nickname, password, }) .then((response) => { console.log(response); setSignUpSuccess(true); }) .catch((error) => { console.log(error.response); //여기가 문제가 있다고 뜹니다. setSignUpError(error.response.data); }) .finally(() => {}); } }, [email, nickname, password, passwordCheck, mismatchError], ); return ( <div id="container"> <Header>Sleact</Header> <Form onSubmit={onSubmit}> <Label id="email-label"> <span>이메일 주소</span> <div> <Input type="email" id="email" name="email" value={email} onChange={onChangeEmail} /> </div> </Label> <Label id="nickname-label"> <span>닉네임</span> <div> <Input type="text" id="nickname" name="nickname" value={nickname} onChange={onChangeNickName} /> </div> </Label> <Label id="password-label"> <span>비밀번호</span> <div> <Input type="password" id="password" name="password" value={password} onChange={onChangePassword} /> </div> </Label> <Label id="password-check-label"> <span>비밀번호 확인</span> <div> <Input type="password" id="password-check" name="password-check" value={passwordCheck} onChange={onChangePasswordCheck} /> </div> {mismatchError && <Error>비밀번호가 일치하지 않습니다.</Error>} {!nickname && <Error>닉네임을 입력해주세요.</Error>} {signUpError && <Error>{signUpError}</Error>} {signUpSuccess && <Success>회원가입되었습니다! 로그인해주세요.</Success>} </Label> <Button type="submit">회원가입</Button> </Form> <LinkContainer> 이미 회원이신가요? <Link to="/login">로그인 하러가기</Link> </LinkContainer> </div> ); }; export default SignUp;
-
미해결프론트엔드 개발환경의 이해와 실습 (webpack, babel, eslint..)
node_modules/.bin/webpack 오류가 납니다
node_modules/.bin/webpack --help 하면 이러한 오류가 나는데 이건 왜그런걸까요?
-
미해결Slack 클론 코딩[실시간 채팅 with React]
초기 새팅에 어려움을 가져서 질문드립니다. config.js 파일이 없습니다
https://github.com/ZeroCho/sleact에서 code로 가서 zip파일을 다운 받았습니다.이후 git clone https://github.com/zerocho/sleact 명령어를 입력한 후, back 폴더에서 npm i까지 마쳤는데, 저는 강의 내용과 다르게 config 폴더가 비어 있습니다.어떤 부분에서 잘못 따라 하였는이해를 못해서 깃 관련 구글링을 해보다가 결국 질문 드립니다.
-
미해결Slack 클론 코딩[실시간 채팅 with React]
tsconfig.json과 webpack.config.ts의 연동구간?
tsconfig.json(ts -> js)로 바꿔주고,그 후에 webpack.config.ts가 js,css,html들을 하나로 번들링해주는 순서가 맞나요??$webpack 실행하면 웹팩 프로그램이 그냥 알아서 tsconfig.json이 있으면 먼저 적용한후 webpack.config.ts를 적용하는건가요?
-
미해결Slack 클론 코딩[실시간 채팅 with React]
npx sequelize db:seed:all 시, ERROR: Validation error 이슈
안녕하세요 강사님,강의 중 채널 생성과정에서 오류가 있어 문의를 남깁니다.먼저, 채널 데이터를 가져오지 못해 다른 문의를 찾아보니 seed 설정을 해주지 않은거 같아 npx sequelize db:create 진행하였고, 이어서npx sequelize db:seed:all 과정을 진행했습니다npx sequelize db:seed:all 을 진행하니 Validation error가 발생했습니다.다른 문의를 보니 db에 sleact table이 생성되지 않아 발생하는 문의글이 많아서 db 사진 올려드릴게요추가적으로, back 파일에서 npm run dev를 실행하면 db 연결까지 잘 됩니다.도대체 어디가 잘 못 된 건지 감이 오지 않습니다..감사합니다.
-
미해결Slack 클론 코딩[실시간 채팅 with React]
POST http://localhost:3090/api/users net::ERR_INTERNET_DISCONNECTED
제로초님, 코드는 정확히 따라 한 거 같은데, 무엇이 문제인지 판단이 안되네요...리액트 v18, 타입스크립트 v18, axios는 1.2.3 입니다. webpackimport path from 'path'; import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin'; import webpack, { Configuration as WebpackConfiguration } from "webpack"; import { Configuration as WebpackDevServerConfiguration } from "webpack-dev-server"; interface Configuration extends WebpackConfiguration { devServer?: WebpackDevServerConfiguration; } import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'; const isDevelopment = process.env.NODE_ENV !== 'production'; const config: Configuration = { name: 'sleact', mode: isDevelopment ? 'development' : 'production', devtool: !isDevelopment ? 'hidden-source-map' : 'eval', resolve: { extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'], alias: { '@hooks': path.resolve(__dirname, 'hooks'), '@components': path.resolve(__dirname, 'components'), '@layouts': path.resolve(__dirname, 'layouts'), '@pages': path.resolve(__dirname, 'pages'), '@utils': path.resolve(__dirname, 'utils'), '@typings': path.resolve(__dirname, 'typings'), }, }, entry: { app: './client', }, module: { rules: [ { test: /\.tsx?$/, loader: 'babel-loader', options: { presets: [ [ '@babel/preset-env', { targets: { browsers: ['IE 10'] }, debug: isDevelopment, }, ], '@babel/preset-react', '@babel/preset-typescript', ], env: { development: { plugins: [['@emotion', { sourceMap: true }], require.resolve('react-refresh/babel')], }, production: { plugins: ['@emotion'] } }, }, exclude: path.join(__dirname, 'node_modules'), }, { test: /\.css?$/, use: ['style-loader', 'css-loader'], }, ], }, plugins: [ new ForkTsCheckerWebpackPlugin({ async: false, // eslint: { // files: "./src/**/*", // }, }), new webpack.EnvironmentPlugin({ NODE_ENV: isDevelopment ? 'development' : 'production' }), ], output: { path: path.join(__dirname, 'dist'), filename: '[name].js', publicPath: '/dist/', }, devServer: { historyApiFallback: true, // react router port: 3090, devMiddleware: { publicPath: '/dist/' }, static: { directory: path.resolve(__dirname) }, proxy: { '/api/': { target: 'http://localhost:3095', changeOrigin: true, }, }, }, }; if (isDevelopment && config.plugins) { config.plugins.push(new webpack.HotModuleReplacementPlugin()); config.plugins.push(new ReactRefreshWebpackPlugin()); } if (!isDevelopment && config.plugins) { } export default config; index.tsximport useInput from "@hooks/useInput"; import React, { useCallback, useState } from "react"; import { Form, Label, Input, LinkContainer, Button, Header, Error} from './styles' import axios from "axios"; const SignUp = () => { const [email, onChangeEmail] = useInput('');//useInput은 커스텀 훅 const [nickname, onChangeNickName] = useInput(''); const [password, setPassword] = useState(''); const [passwordCheck, setPasswordCheck] = useState(''); const [mismatchError, setMismathError] = useState(false); const onChangePassword = useCallback((e: any) => { setPassword(e.target.value); setMismathError(e.target.value !== passwordCheck); // 함수 기준으로 외부 변수만 deps에 적어줌 내부 변수는 x }, [passwordCheck]); const onChangePasswordCheck = useCallback((e: any) => { setPasswordCheck(e.target.value); setMismathError(e.target.value !== password) }, [password]); const onSubmit = useCallback((e: React.FormEvent) => { e.preventDefault(); if(!mismatchError && nickname){ console.log('서버로 회원가입하기'); axios.post('/api/users', { email, nickname, password, }) .then((response) => { console.log(response); })//요청이 성공하면 실행 .catch((error) => { console.log(error.response); })//요청이 실패하면 실행 .finally(() => {});//성공하든 실패하든 실행시키고 싶은 것 } console.log(email, nickname, password, passwordCheck) }, [email, nickname, password, passwordCheck, mismatchError]); return ( <div id="container"> <Header>Sleact</Header> <Form onSubmit={onSubmit}> <Label id="email-label"> <span>이메일 주소</span> <div> <Input type="email" id="email" name="email" value={email} onChange={onChangeEmail} /> </div> </Label> <Label id="nickname-label"> <span>닉네임</span> <div> <Input type="text" id="nickname" name="nickname" value={nickname} onChange={onChangeNickName} /> </div> </Label> <Label id="password-label"> <span>비밀번호</span> <div> <Input type="password" id="password" name="password" value={password} onChange={onChangePassword} /> </div> </Label> <Label id="password-check-label"> <span>비밀번호 확인</span> <div> <Input type="password" id="password-check" name="password-check" value={passwordCheck} onChange={onChangePasswordCheck} /> </div> {mismatchError && <Error>비밀번호가 일치하지 않습니다.</Error>} {!nickname && <Error>닉네임을 입력해주세요.</Error>} {/* {signUpError && <Error>{signUpError}</Error>} */} {/* {signUpSuccess && <Success>회원가입되었습니다! 로그인해주세요.</Success>} */} </Label> <Button type="submit">회원가입</Button> </Form> <LinkContainer> 이미 회원이신가요? <a href="/login">로그인 하러가기</a> </LinkContainer> </div> ); }; export default SignUp;이렇게 따라 한 후에 회원 가입 누르면 아래 같은 화면이 뜹니다.
-
해결됨프론트엔드 개발환경의 이해와 실습 (webpack, babel, eslint..)
개발환경에서 assets 파일 참조관련 질문
요약개발환경에서 src/assets/.... 에 있는 이미지 파일을 제대로 참조하는 방법이 궁금합니다. 구성요소프로젝트의 구성요소는 아래와 같습니다.public[index.html, favicon.ico]src[assets[image0, image1...], index.js 등] 설치된 패키지는 아래와 같습니다. "webpack": "^5.75.0", "webpack-cli": "^5.0.1", "webpack-dev-server": "^4.11.1" // 본 강의에선 4.x.x 버전을 사용하지만... // 5 version을 공부해야해서... 죄송합니다 😥 설명dev server를 실행시켜 개발할 때,js 파일을 수정하면 바로 반영이 되는 걸 확인했습니다. 그런데 이미지 파일의 경우 다른 파일을 참조하도록 하면 해당 파일을 불러오지 못합니다. 그리고 build된 파일을 참조합니다.예로들어 정적 이미지 파일이 ./src/assets/image_0.jpg 라면,dev server로 실행시켜 확인하면 HOST/dist/assets/images/[hash][ext][query].jpg 이렇게 되어있습니다. (경로가 다름)그리고 build를 하면 분명 assets 디렉토리엔 다수의 이미지 파일이 존재함에도 불구하고 코드에서 사용된 이미지 파일만 build됩니다.그러면 만약 코드내부에서 동적으로 다른 static image 파일을 참조하게 된다면 해당 이미지가 없기 때문에 오류가 날텐데 이런건 어떻게 처리해야하나요? 코드const path = require('path'); const { BannerPlugin, DefinePlugin } = require('webpack'); const childProcess = require('child_process'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const isDevMode = (process.env.NODE_ENV || 'development').trim() === 'development'; console.log('is DEV mode?', isDevMode); console.log('__dirname: ', __dirname); module.exports = { mode: isDevMode ? 'development' : 'production', // entry: webpack 시작되는 부분이라고 생각하면 된다. entry: { main: './src/index.js', }, /** * output * entry point를 기준으로 * 모든 .js 파일을 합쳐서 하나의 bundle 파일로 만드는데, * 이걸 어디에 저장할 것인지 지정하는 option */ output: { path: path.resolve(__dirname, 'dist'), filename: isDevMode ? '[name].js' : 'main.[contenthash].js', chunkFilename: '[id].chunk.js', assetModuleFilename: 'images/[hash][ext][query]', clean: true, }, devServer: { port: 3000, hot: true, client: { overlay: { errors: true, warnings: false, }, }, // static: { // directory: path.resolve(__dirname, './src/assets/'), // }, }, /** * module * test에 설정한 파일들을 inspect 하여, * 조건에 맞는 파일들에 대해 loader 들을 실행하여 해석함 */ module: { rules: [ { test: /\.(sa|sc|c)ss$/i, exclude: [/node_modules/], use: [ // creates 'style' nodes from JS strings isDevMode ? 'style-loader' : { loader: MiniCssExtractPlugin.loader, options: { publicPath: '', }, }, // translates css into common JS 'css-loader', 'postcss-loader', // complies sass to css 'sass-loader', ], }, { test: /\.(png|svg|jpg|jpeg|gif)$/i, exclude: [/node_modules/], type: 'asset/resource', parser: { dataUrlCondition: { // 크기가 8kb 미만인 파일은 inline 모듈로 처리되고 그렇지 않으면 resource 모듈로 처리됩니다. maxSize: 4 * 1042, }, }, // generator: { // publicPath: './assets/', // outputPath: './assets/', // }, }, { test: /\.js$/, exclude: [/node_modules/], loader: 'babel-loader', }, { test: /\.(woff|woff2|eot|ttf|otf)$/i, exclude: [/node_modules/], type: 'asset/resource', }, ], }, plugins: [ /** * 개발할 때 API 서버주소, * 배포했을 때 API 서버주소를 설정하는 Plugin */ // new DefinePlugin({ // NODE_ENV: 'development', // }), new BannerPlugin({ banner: `Build Date: ${new Date().toLocaleString()} Commit Version: ${childProcess.execSync('git rev-parse --short HEAD')} Author: ${childProcess.execSync('git config user.name')}`, }), new HtmlWebpackPlugin({ template: './public/index.html', templateParameters: { env: isDevMode ? '개발용' : '배포용', }, minify: !isDevMode ? { collapseWhitespace: true, removeComments: true, } : false, }), ...(!isDevMode ? [ new MiniCssExtractPlugin({ filename: isDevMode ? '[name].css' : '[name].[contenthash].css', chunkFilename: isDevMode ? '[id].css' : '[id].[contenthash].css', }), ] : []), ], }; 결론즉 정리하자면,개발모드일 때 정적 이미지 파일을 참조하도록 설정을 어떻게 해야하나요?왜 build할 땐 이미지 파일이 코드에서 사용중인 것만 빌드 되나요? 답변 주시면 감사하겠습니다.
-
미해결Slack 클론 코딩[실시간 채팅 with React]
swr revalidate에 대해 질문드립니다.
안녕하세요 swr revalidate 가 deprecated 되어 mutate를 사용하면 된다는 답변을 확인하고 mutate를 썼는데 궁금한 점이 있습니다. mutate()를 하는 이유는 로그인 성공했을때 그 시점에 users api를 호출하기 위해서 인가요?그리고 mutate 와 무관하게 디폴트 설정에따라(화면전환등) SWR에서 userapi를 호출하고 있는것도 맞나요?1: 화면 첫 렌더링때 user api 콜로그인mutate실행으로 user api 콜화면전환했을때 다시 콜 제가 이해한게 맞는지 답변 부탁드립니다.감사합니다.
-
미해결Slack 클론 코딩[실시간 채팅 with React]
회원가입 요청이 가지않는 이슈
회원가입을 눌렀으나 회원가입이 안되고 요청이 가지 않는 것 같습니다. 에러메세지를 긁어서 확인 해보았으나 어디가 잘못된건지 모르겠고, 서버 부분의 콘솔을 확인해 보았으나 에러메세지가 나오지 않았습니다. 에러 메세지 이미지와 본문입니다.react_devtools_backend.js:4012 Error: Minified React error #31; visit https://reactjs.org/docs/error-decoder.html?invariant=31&args[]=object%20with%20keys%20%7Bsuccess%2C%20code%2C%20data%7D for the full message or use the non-minified dev environment for full errors and additional helpful warnings. at ka (react-dom.production.min.js:140:47) at react-dom.production.min.js:150:265 at Ml (react-dom.production.min.js:176:171) at Bi (react-dom.production.min.js:271:134) at Eu (react-dom.production.min.js:250:347) at wu (react-dom.production.min.js:250:278) at bu (react-dom.production.min.js:250:138) at pu (react-dom.production.min.js:243:163) at react-dom.production.min.js:123:115 at t.unstable_runWithPriority (scheduler.production.min.js:18:34 혹시나 ENV를 까먹었을까봐 다시 확인했지만 있었고,아래는 서버쪽 터미널 이미지입니다.감사합니다.
-
해결됨Slack 클론 코딩[실시간 채팅 with React]
npx sequelize db:create 오류에 대한 질문입니다.
PS C:\Projects\sleact\back> npx sequelize db:createSequelize CLI [Node: 18.12.1, CLI: 6.2.0, ORM: 6.26.0]Loaded configuration file "config\config.js".Using environment "development".ERROR: Access denied for user 'root'@'localhost' (using password: YES)현재 node version은 18.12.1npm version은 8.19.2 입니다.squelize 는 npm 을 통해서 설치하였고 cli도 설치하였습니다.config/config.js 에 string으로도 넣어보았고.env 에도 넣어보았습니다. .env에 값이 나오는 것은 console.log로 확인하였습니다.MySQL commend clinent 에 들어가 password가 맞는지도 확인했는데 맞는 password 였습니다....!그럼에도 불구하고실행이 안됐습니다.. ㅠ 도움 주시면 감사하겠습니다.
-
미해결Slack 클론 코딩[실시간 채팅 with React]
채널 생성시 channelData.map is not a function
채널생성 클릭하면 channeldata.map is not a function이라고 에러가 뜨는데channelData뿌려지는곳에 ?옵셔널도 줬고..아래처럼 잘 작성한것같은데 어딜 놓쳤는지 모르겠습니다.새로고침하면 추가된 채널명이 출력됩니다. workspaceimport fetcher from '@utils/fetcher'; import axios from 'axios'; import React, { FC, useCallback, useState } from 'react'; import { Navigate, useParams } from 'react-router-dom'; import useSWR from 'swr'; import { AddButton, Channels, Chats, Header, LogOutButton, MenuScroll, ProfileImg, ProfileModal, RightMenu, WorkspaceButton, WorkspaceModal, WorkspaceName, Workspaces, WorkspaceWrapper, } from './styles'; import gravatar from 'gravatar'; import Menu from '@components/menu'; import { Link } from 'react-router-dom'; import { IChannel, IUser, IWorkspace } from '@typings/db'; import { Button, Input, Label } from '@pages/signup/styles'; import useInput from '@hooks/useInput'; import Modal from '@components/modal'; import { toast } from 'react-toastify'; import CreateChannelModal from '@components/createChannelModal'; const Workspace: FC = ({ children }) => { const { workspace, channel } = useParams<{ workspace: string; channel: 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); if (!userData) { return <Navigate to="/login" />; } const [showUserMenu, setShowUserMenu] = useState(false); const [newWorkspace, onChangeNewWorkspace, setNewWorkspace] = useInput(''); const [newUrl, onChangeNewUrl, setNewUrl] = useInput(''); const [showWorkspaceModal, setShowWorkspaceModal] = useState(false); const [showCreateChannelModal, setShowCreateChannelModal] = useState(false); const [showCreateWorkspaceModal, setShowCreateWorkspaceModal] = useState(false); //functions const onLogout = useCallback(() => { axios .post('/api/users/logout', null, { withCredentials: true, }) .then((res) => { mutate(res.data); }); }, []); const onClickUserProfile = useCallback(() => { setShowUserMenu(!showUserMenu); }, [showUserMenu]); const onClickCreateWorkspace = useCallback(() => { setShowCreateWorkspaceModal(true); }, []); const onCreateWorkspace = useCallback( (e) => { e.preventDefault(); if (!newWorkspace || !newWorkspace.trim()) return; if (!newUrl || !newUrl.trim()) return; //trim ->띄어쓰기 하나도 통과 돼버리는걸 막는다. axios .post( '/api/workspaces', { workspace: newWorkspace, url: newUrl, }, { withCredentials: true, }, ) .then((res) => { mutate(res.data); setShowCreateWorkspaceModal(false); setNewWorkspace(''), setNewUrl(''); }) .catch((err) => { console.dir(err); toast.error(error.response?.data, { position: 'bottom-center' }); }); }, [newWorkspace, newUrl], ); const onCloseModal = useCallback(() => { setShowCreateWorkspaceModal(false); setShowCreateChannelModal(false); }, []); const toggleWorkspaceModal = useCallback(() => { setShowWorkspaceModal(!showWorkspaceModal); }, [showWorkspaceModal]); 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 }} onCloseModal={onClickUserProfile} show={showUserMenu}> <ProfileModal> <img src={gravatar.url(userData.email, { s: '28px', 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: IWorkspace) => { 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> {channelData?.map((v, idx) => ( <div key={idx}>{v.name}</div> ))} </MenuScroll> </Channels> <Chats> {children}</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} /> </div> ); }; export default Workspace; createChannelModalimport 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, VFC } from 'react'; import { useParams } from 'react-router-dom'; import { toast } from 'react-toastify'; import useSWR from 'swr'; interface Props { show: boolean; onCloseModal: () => void; setShowCreateChannelModal: (flag: boolean) => void; } const CreateChannelModal: VFC<Props> = ({ show, onCloseModal, setShowCreateChannelModal }) => { const [newChannel, onChangeNewChannel, setNewChannel] = useInput(''); const { workspace, channel } = useParams<{ workspace: string; channel: string }>(); const { data: userData } = useSWR<IUser | false>(`/api/users`, fetcher); const { data: channelData, mutate } = useSWR<IChannel[]>( userData ? `/api/workspaces/${workspace}/channels` : null, fetcher, ); const onCreateChannel = useCallback( (e) => { e.preventDefault(); axios .post( `/api/workspaces/${workspace}/channels`, { name: newChannel, }, { withCredentials: true }, ) .then((res) => { setShowCreateChannelModal(false); mutate(res.data); setNewChannel(''); }) .catch((err) => { console.dir(err); toast.error(err.response?.data, { position: 'bottom-center' }); }); }, [newChannel], ); return ( <Modal show={show} onCloseModal={onCloseModal}> <form onSubmit={onCreateChannel}> <Label id="channel-label"> <span>채널</span> <Input id="channel" value={newChannel} onChange={onChangeNewChannel} /> </Label> <Button type="submit">생성하기</Button> </form> </Modal> ); }; export default CreateChannelModal;
-
미해결Slack 클론 코딩[실시간 채팅 with React]
npx sequelize db:create 오류
ws@DESKTOP-9H6S8B6 MINGW64 ~/Desktop/sleact/back$ npx sequelize db:createSequelize CLI [Node: 16.15.0, CLI: 6.4.1, ORM: 6.21.4]Loaded configuration file "config\config.js".Using environment "development".ERROR: Access denied for user 'root'@'localhost' (using password: NO) 이런 오류가 계속 뜨고 다른 분들께서 질문하신 답변을 봐도 모르겠습니다... mysql 비밀번호는 확실하게 맞습니다.
-
미해결Slack 클론 코딩[실시간 채팅 with React]
npx sequelize db:create 입력시 에러 발생
back 폴더에 npm i 이후 npx sequelize db:create 입력시 npm ERR! could not determine executable to run npm ERR! A complete log of this run can be found in:npm ERR! /Users/eycha/.npm/_logs/2022-08-21T06_14_10_186Z-debug-0.log 라는 에러 발생합니다. mysql 과 node 정상적으로 설치했는데 관련되서 검색해도 해결책이 없어서 질문 남깁니다.
-
해결됨Slack 클론 코딩[실시간 채팅 with React]
sleact/alecture/pages/login/styles.tsx ?
깃헙에서 sleact/alecture/pages/login/styles.tsx 파일이 비워 있는데 일부러 비워 두신거죠?
-
해결됨Slack 클론 코딩[실시간 채팅 with React]
안녕하세요 웹팩 관련질문입니다.
안녕하세요! 웹팩 관련 질문이있습니다. 제꺼 빌드용량이 3MB로 엄청크더라구요. 그래서 원인을 알아보니 isDevelopment가 계속 development 모드로 빌드 되는게 이유였습니다. devtool설정은 아래와 같이했는데 development이다 보니까 계속 inline source map 으로 작동해서 파일크기가 큰것같더라고요. isDevelopment ? 'inline-source-map' : 'hidden-source-map' 차이점을 보니 npm script가 start는 webpack serve, build는 webpack만 되어있는 상태였어요. 그래서 start 에는 webpack serve --env development build에는 NODE_ENV=production webpack 를 적어주니까 production모드로 되고, 300kb로 떨어진걸 확인했어요. 그런데 start는 --env development이고, build는 NODE_ENV=production인 이유가 있나요? 둘다 --env development, --env production을 적거나 NODE_ENV=development, NODE_ENV=production으로 하는것과 차이가 있나요? 공식문서에서는 webpack dev와 prod로 파일을 다르게하는 아래방법밖에 못찾았는데, "start": "webpack serve --open --config webpack.dev.js", "build": "webpack --config webpack.prod.Js 제로초님은 어떤걸 보고 하셨는지 궁금합니다. 그리고 EnvironmentPlugin은 적지 않아도 console.log(isDevelopment)를 찍어보면 process.env.NODE_ENV 상태가 출력되고 빌드/실행도 잘되던데, 아래와같이 추가해야하는 이유가 있는지도 궁금합니다. new webpack.EnvironmentPlugin({ NODE_ENV: isDevelopment ? 'development' : 'production',
-
미해결Slack 클론 코딩[실시간 채팅 with React]
[06:26 부분] 정규표현식으로 문자열 변환하기 부분 질문할게요
정규식으로 닉네임 찾는 부분에서요 match.match(/@\[(.+?)]\((\d+?)\)/)! 여기 이부분에서느낌표(!)를 왜 붙여준건가요? str.match(정규표현식) 이런 형태로 작성하는 건줄 알았는데 느낌표가 붙여있어서 궁금해서 질문올립니다.