inflearn logo
강의

강의

N
챌린지

챌린지

멘토링

멘토링

N
클립

클립

로드맵

로드맵

지식공유

묻고 답해요

173만명의 커뮤니티!! 함께 토론해봐요.

ts-node 대신 tsx 사용여부

미해결

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

질문: 1.ts-node로 이어나가도 괜찮을까요? 2. outdated 명령어 쳐보니 버전이 강의에서 한두개 추가설치했지만, 이제 빨간 글씨로 10개 정도 다 버전 업그레이드를 요구했습니다.제가 고쳐나가면서 나아가보면될지 궁금합니다. ㅡㅡㅡㅡㅡㅡㅡㅡ 상황: 몇주전 타입스크립트를 다른 강의로 시작했습니다.최근 환경설정에서 ts-node 사용시 오류가 계속 떠서 그 강의가 tsx 사용을 권장했고, tsx로 입문했습니다.그래서 ts-node로 잘돌아갈지 의문입니다.

  • react
  • 웹팩
  • typescript
  • socket.io
  • babel
  • 클론코딩
elecch 댓글 1 좋아요 0 조회수 398

숫자 야구 npm run build시 오류

미해결

웹 게임을 만들며 배우는 Vue

ERROR in app.js from Terser Error: error:0308010C:digital envelope routines::unsupported 구글링해보니 리액트밖에 안뜨네요.. 어떻게 해야 하나요?

  • vue.js
  • 웹팩
  • vuex
흑후추 댓글 2 좋아요 0 조회수 345

onScroll 스크롤 위치 유지가 안됩니다 ㅠ

미해결

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

import Chat from '@components/Chat'; import { ChatZone, Section, StickyHeader } from '@components/ChatList/style'; import { IChat, IDM } from '@typings/db'; import React, { FC, RefObject, VFC, forwardRef, useCallback, useRef } from 'react'; import { Scrollbars } from 'react-custom-scrollbars'; interface Props { scrollRef: RefObject<Scrollbars>; chatSections: { [key: string]: IDM[] }; setSize: (f: (index: number) => number) => Promise<IDM[][] | undefined>; isEmpty: boolean; isReachingEnd: boolean; } const ChatList: VFC<Props> = (({ chatSections, setSize, isEmpty, scrollRef, isReachingEnd}) => { const onScroll = useCallback((values) => { // 끝에 도달하면 불러오지 않기 if(values.scrollTop === 0 && !isReachingEnd){ console.log('가장 위'); setSize((prevSize) => prevSize + 1).then(()=>{ // 스크롤 위치 유지 if(scrollRef?.current){ scrollRef.current?.scrollTop(scrollRef.current?.getScrollHeight() - values.scrollHeight); } }); } }, []); return ( <ChatZone> <Scrollbars autoHide ref={scrollRef} onScrollFrame={onScroll}> {Object.entries(chatSections).map(([date, chats]) => { return ( <Section className={`section-${date}`} key={date}> <StickyHeader> <button>{date}</button> </StickyHeader> {chats.map((chat) => ( <Chat key={chat.id} data={chat} /> ))} </Section> ); })} </Scrollbars> </ChatZone> ); }); export default ChatList; 이쪽 코드는 문제가 없는것같은데 희한하게 위치가 유지가 되지않고 원래처럼 쭉 올라가버립니다.. ref쪽이 문제인가요..? 혹시몰라 DirectMessage 컴포넌트도 아래에 첨부하겠습니다. import React, { useCallback, useEffect, useRef } from 'react'; import gravator from 'gravatar'; import useSWR, { mutate } from 'swr'; // swr 인피니티스크롤링 전용 메서드 import useSWRInfinite from 'swr/infinite'; import { IDM, IUser } from '@typings/db'; import fetcher from '@utils/fetcher'; import { useParams } from 'react-router'; import ChatBox from '@components/ChatBox'; import { Container, Header } from '@pages/DirectMessage/style'; import ChatList from '@components/ChatList'; import useInput from '@hooks/useInput'; import axios from 'axios'; import makeSection from '@utils/makeSection'; import Scrollbars from 'react-custom-scrollbars'; const DirectMessage = () => { const { workspace, id } = useParams<{ workspace: string; id: string }>(); const { data: userData } = useSWR(`http://localhost:3095/api/workspaces/${workspace}/users/${id}`, fetcher); // 내정보 const { data: myData } = useSWR(`http://localhost:3095/api/users`, fetcher); const [chat, onChangeChat, setChat] = useInput(''); // 과거 채팅리스트에서 채팅을 치면 최신목록으로 바로 스크롤을 내려줄려면 ref를 // 이 컴포넌트에서 props로 내려줘야하기 때문에 forwardRef를 사용해서 props로 넘겨준다 // 💡 HTML 엘리먼트가 아닌 React 컴포넌트에서 ref prop을 사용하려면 React에서 제공하는 forwardRef()라는 함수를 사용해야 합니다 const scrollbarRef = useRef<Scrollbars>(null); // 채팅 받아오는곳 (setSize : 페이지수를 바꿔줌) // useSWRInfinite를 쓰면 [{id:1},{id:2},{id:3},{id:4}] 1차원배열이 [[{id:1},{id:2}],[{id:3},{id:4}]] 2차원배열이 된다. const { data: chatData, mutate: mutateChat, setSize, } = useSWRInfinite<IDM[]>( (index) => `http://localhost:3095/api/workspaces/${workspace}/dms/${id}/chats?perPage=20&page=${index + 1}`, fetcher, ); // 데이터 40 개중에 20개씩 사져오면 첫번째페이지부터 20 + 20 + 0 세번째 페이지 0 이되면 isEmpty, isReachingEnd는 true가 됨 // 반대의 상황에서 데이터가 45개면 20 + 20 + 5 isEmpty는 0이 아니라서 false isReachingEnd는 여전히 데이터 가져옴 const isEmpty = chatData?.[0]?.length === 0; const isReachingEnd = isEmpty || (chatData && chatData[chatData.length - 1]?.length < 20) || false; const onSubmitForm = useCallback( (e) => { e.preventDefault(); if (chat?.trim() && chatData) { const savedChat = chat; // 💡 옵티미스틱 UI // 서버쪽에 다녀오지 않아도 성공해서 데이터가 있는거처럼 보이게 미리 만듦 mutateChat((prevChatData) => { // infinite 스크롤링은 2차원 배열이다. prevChatData?.[0].unshift({ // unshift : 앞쪽에 추가 id: (chatData[0][0]?.id || 0) + 1, content: savedChat, SenderId: myData.id, Sender: myData, ReceiverId: userData.id, Receiver: userData, createdAt : new Date(), }); return prevChatData; },false) // 옵티미스틱 UI 할땐 이부분이 항상 false .then(()=>{ setChat(''); // 버튼클릭 시 기존 채팅지우기 scrollbarRef.current?.scrollToBottom(); // 채팅 첬을때 맨 아래로 }) axios .post( `http://localhost:3095/api/workspaces/${workspace}/dms/${id}/chats`, { content: chat, }, { withCredentials: true, }, ) .then(() => { mutateChat(); // SWR에서 데이터를 다시 불러와서 캐시를 갱신하는 역할을 합니다. }) .catch(() => { console.error; }); } }, [chat, chatData, myData, userData, workspace, id], ); // (채팅이 최신것을 아래에 두기 위함) = 기존것 데이터를두고 새 데이터를 뒤집어서 출력 / flat() 배열을 1차원 배열로 만들어줌 const chatSections = makeSection(chatData ? [...chatData].flat().reverse() : []); // 로딩 시 스크롤바 제일 아래로 useEffect(()=>{ if(chatData?.length === 1){ // 채팅 데이터가 있어서 불러온 경우 scrollbarRef.current?.scrollToBottom(); // 가장 아래쪽으로 내려줌 } },[chatData]) // 로딩 if (!userData || !myData) { return null; } return ( <Container> <Header> <img src={gravator.url(userData.email, { s: '24px', d: 'retro' })} alt={userData.nickname}></img> <span>{userData.nickname}</span> </Header> {/* 컴포넌트 위치를 미리 지정해도 좋다. */} {/* 전역 상태관리 라이브러리를 사용해도 컴포넌트상황에따라 props 로 넘겨줌*/} <ChatList scrollRef={scrollbarRef} chatSections={chatSections} setSize={setSize} isEmpty={isEmpty} isReachingEnd={isReachingEnd} /> <ChatBox chat={chat} onChangeChat={onChangeChat} onSubmitForm={onSubmitForm} /> </Container> ); }; export default DirectMessage;

  • react
  • 웹팩
  • typescript
  • socket.io
  • babel
  • 클론코딩
복실묘 댓글 2 좋아요 0 조회수 533

str.toLowerCase is not a function

미해결

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

예상하지 못한 부분에서 에러가 나와서 질문 남겨드립니다 ! ChatBox.tsx import React, { useCallback, useEffect, useRef, VFC } from 'react'; import { ChatArea, EachMention, Form, MentionsTextarea, SendButton, Toolbox } from './styles'; import autosize from 'autosize'; import { Mention, SuggestionDataItem } from 'react-mentions'; import { useParams } from 'react-router'; import useSWR from 'swr'; import { IUser } from '@typings/db'; import fetcher from '@utils/fetcher'; import gravatar from 'gravatar'; interface Props { chat: string; onSubmitForm: (e: any) => void; onChangeChat: (e: any) => void; placeholder?: string; } const ChatBox: VFC<Props> = ({ chat, onSubmitForm, onChangeChat, placeholder }) => { const { workspace } = useParams<{ workspace: string }>(); const { data: userData, error, revalidate, mutate, } = useSWR<IUser | false>('/api/users', fetcher, { dedupingInterval: 2000, // 2초 }); const { data: memberData } = useSWR<IUser[]>(userData ? `/api/workspaces/${workspace}/members` : null, fetcher); const textareaRef = useRef<HTMLTextAreaElement>(null); useEffect(() => { if (textareaRef.current) { autosize(textareaRef.current); } }, []); const onKeydownChat = useCallback( (e) => { if (e.key === 'Enter') { if (!e.shiftKey) { e.preventDefault(); onSubmitForm(e); } } }, [onSubmitForm], ); const renderSuggestion = useCallback( ( suggestion: SuggestionDataItem, search: string, highlightedDisplay: React.ReactNode, index: number, focus: boolean, ): React.ReactNode => { if (!memberData) return; return ( <EachMention focus={focus}> <img src={gravatar.url(memberData[index].email, { s: '20px', d: 'retro' })} alt={memberData[index].nickname} /> <span>{highlightedDisplay}</span> </EachMention> ); }, [memberData], ); return ( <ChatArea> <Form onSubmit={onSubmitForm}> <MentionsTextarea id="editor-chat" value={chat} onChange={onChangeChat} onKeyPress={onKeydownChat} placeholder={placeholder} inputRef={textareaRef} allowSuggestionsAboveCursor > <Mention appendSpaceOnAdd trigger="@" data={memberData?.map((v) => ({ id: v.id, display: v.nickname })) || []} renderSuggestion={renderSuggestion} /> </MentionsTextarea> <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" /> </SendButton> </Toolbox> </Form> </ChatArea> ); }; export default ChatBox; 혼자서 해결해보려다가 못찾고 있어서 질문 남겨드려요 ㅠㅠ

  • react
  • 웹팩
  • typescript
  • socket.io
  • babel
  • 클론코딩
유니크한 고슴도치 댓글 2 좋아요 0 조회수 835

[공유] react-mention 항상 커서 위에 나오게 수정

미해결

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

이전 질문을 보면 react-mention 추천이 커서 아래로 나온다고 해서 공식 문서를 확인해봤습니다. 결론적으로는 forceSuggestionsAboveCursor을 사용하면 됩니다. 해당 내용은 제로초님 sleact 레파지토리에 pull request 하였습니다. allowSuggestionsAboveCursor는 아래 공간이 부족하면 위에 배치가 '가능'하도록, 즉 워크스페이스 사람이 적으면 아래로 배치 forceSuggestionsAboveCursor는 항상 위로 배치 allowSuggestionsAboveCursor: Renders the SuggestionList above the cursor if there is not enough space below forceSuggestionsAboveCursor: Forces the SuggestionList to be rendered above the cursor

  • 웹팩
  • Socket.io
  • babel
  • typescript
  • react
  • 클론코딩
alvinlog 댓글 3 좋아요 2 조회수 920

webpack설정 시 json 파일 에러

미해결

웹 게임을 만들며 배우는 React

안녕하세요 제로초님, 제가 웹팩을 이용해서 리액트를 사용하면서 json 파일을 로컬에서 사용하고 싶어서 import를 해봤는데 세미콜론 에러가 나서 json-loader를 추가해보니 또 다른 에러가 나서.. 혹시 이 부분에 대한 해결 방법이나 방향을 알고 계신가요? import data from "./info.json"; // 에러 => json loader 추가 후 webpack.config.js const path = require("path"); const RefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin"); module.exports = { name: "react-test-setting", mode: "development", devtool: "eval", resolve: { extensions: [".jsx", ".js"], }, entry: { app: ["./client"], }, // input module: { rules: [ { test: /\.jsx?/, loader: "babel-loader", options: { presets: ["@babel/preset-env", "@babel/preset-react"], plugins: ["react-refresh/babel"], }, }, { test: /\.json$/, loader: "json-loader", }, ], }, plugins: [new RefreshWebpackPlugin()], output: { path: path.join(__dirname, "/dist"), filename: "app.js", }, // output devServer: { devMiddleware: { publicPath: "/dist" }, static: { directory: path.resolve(__dirname) }, hot: true, }, }; info.json { "items": [ { "name": "kim", "age": 21, "address": "seoul" }, { "name": "lee", "age": 23, "address": "seoul" }, { "name": "park", "age": 31, "address": "seoul" } ] }

  • json
  • react
  • 웹팩
김성현 댓글 1 좋아요 0 조회수 1144

안녕하세요.

해결됨

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

안녕하세요. 질문 있습니다. 강의에서는 SWR을 쓰시는데 공부할때는 react-query를 쓰려고 합니다. 이유는 생태계가 더 큰 라이브러리를 사용할 목적이었는데 알려주신 npm trends에 검색해보니 둘 다 비슷비슷하더라고요. SWR도 아직 많이 쓰이는 추세인가요??? 궁금한 이유는 react-query 외에도 추가로 하나의 라이브러리를 동시에 더 써볼 생각이라 추천해주시면 감사합니다. 추가로 수업 외 질문으로 유틸리티 라이브러리?를 하나 써보려고 합니다. 기존에는 fxjs를 사용 중이었습니다. npm trends를 보니 가장 많이 사용하는 라이브러리는 lodash, rxjs인 것 같고 lamda도 많이 쓰는거 같고 타입스크립트로 된 fp-ts도 있는거 같은데 fp-ts를 제외한 것들 중 추천해주시는게 있을까요? 감사합니다!!

  • 웹팩
  • babel
  • Socket.io
  • socket.io
  • typescript
  • react
  • 클론코딩
  • 클론코딩
Milkyway 댓글 1 좋아요 0 조회수 471

Cannot read properties of undefined (reading 'map')

미해결

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

제로초님, 코드를 따라친 후에 로그아웃을 하고 다시 로그인 하면 이런 에러메세지가 뜹니다. 그런데 네트워크 탭을 보면 로그인이 정상적으로 된거 같아서 새로고침을 하면 에러 메세지가 사라지고 슬랙에서 로그인된 화면이 제대로 뜹니다. 근데 또 여기서 워크스페이스를 생성하려고 하면 콘솔에 axioserror메세지가 떠서 어떻게 해야될지 모르겠습니다.. Workspace/index.tsx import 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.tsx import 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}>&times;</CloseModalButton> {children} </div> </CreateModal> ); }; export default Modal; swr2.0 버전, react v18, typescript v18 swr을 최신버전 사용해서 revalidate대신 mutate를 사용했는데 제가 잘못 사용한건지 모르겠습니다.

  • 클론코딩
  • babel
  • typescript
  • socket.io
  • 웹팩
  • 클론코딩
  • react
  • Socket.io
성창수 댓글 2 좋아요 0 조회수 1890

개발환경에서 assets 파일 참조관련 질문

해결됨

프론트엔드 개발환경의 이해와 실습 (webpack, babel, eslint..)

요약 개발환경에서 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할 땐 이미지 파일이 코드에서 사용중인 것만 빌드 되나요? 답변 주시면 감사하겠습니다.

  • 웹팩
  • webpack5
  • 웹팩
  • nodejs
  • eslint
  • babel
taylous 댓글 1 좋아요 0 조회수 1190

swr revalidate에 대해 질문드립니다.

미해결

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

안녕하세요 swr revalidate 가 deprecated 되어 mutate를 사용하면 된다는 답변을 확인하고 mutate를 썼는데 궁금한 점이 있습니다. mutate()를 하는 이유는 로그인 성공했을때 그 시점에 users api를 호출하기 위해서 인가요? 그리고 mutate 와 무관하게 디폴트 설정에따라(화면전환등) SWR에서 userapi를 호출하고 있는것도 맞나요? 1: 화면 첫 렌더링때 user api 콜 로그인 mutate실행으로 user api 콜 화면전환했을때 다시 콜 제가 이해한게 맞는지 답변 부탁드립니다. 감사합니다.

  • Socket.io
  • 웹팩
  • react
  • typescript
  • babel
  • 클론코딩
나래 댓글 1 좋아요 2 조회수 765

회원가입 요청이 가지않는 이슈

미해결

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를 까먹었을까봐 다시 확인했지만 있었고, 아래는 서버쪽 터미널 이미지입니다. 감사합니다.

  • 웹팩
  • Socket.io
  • babel
  • typescript
  • react
  • 클론코딩
퀀텀에듀 댓글 1 좋아요 0 조회수 607

스크롤탑 동기처리 이슈.

미해결

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

안녕하세요 제로초님. 인피니티 스크롤 로딩 후 추가 데이터를 로딩하고 스크롤 위치를 유지하는 로직에 문제가 있어서 문의드립니다. 아래 코드에서는 setSize의 then문 안에 setTimeout으로 한 번 더 감싸주었는데 setTimeout이 없는경우 데이터가 불러와지기전에(스크롤의 높이가 늘어나지 않은 상태에서) setTimeout내부의 로직이 실행됩니다. 그로인해 스크롤의 높이는 기존높이 - 기존높이 로 scrollTop(0) 과 같은 상태가 되어버리는데요. setTimeout으로 0초의 딜레이를 주면 또 순서대로 동작을 합니다... 이런경우에 좋은 해결책이 있을까요? const onScroll = useCallback((values: positionValues) => { if (values.scrollTop === 0 && !isReachingEnd) { setSize((prevSize) => prevSize + 1).then(() => { setTimeout(() => { if (typeof scrollbarRef !== "function" && scrollbarRef?.current) { scrollbarRef.current.scrollTop(scrollbarRef.current.getScrollHeight() - values.scrollHeight); } }, 0); }); } }, []);

  • babel
  • Socket.io
  • 웹팩
  • react
  • typescript
  • 클론코딩
김영욱 댓글 2 좋아요 0 조회수 465

npx sequelize db:create 오류에 대한 질문입니다.

해결됨

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

PS C:\Projects\sleact\back> npx sequelize db:create Sequelize 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.1 npm version은 8.19.2 입니다. squelize 는 npm 을 통해서 설치하였고 cli도 설치하였습니다. config/config.js 에 string으로도 넣어보았고 .env 에도 넣어보았습니다. .env에 값이 나오는 것은 console.log로 확인하였습니다. MySQL commend clinent 에 들어가 password가 맞는지도 확인했는데 맞는 password 였습니다....! 그럼에도 불구하고 실행이 안됐습니다.. ㅠ 도움 주시면 감사하겠습니다.

  • squelize
  • react
  • typescript
  • 웹팩
  • babel
  • Socket.io
  • 클론코딩
퀀텀에듀 댓글 1 좋아요 0 조회수 523

__dirname 질문입니당

미해결

프론트엔드 개발자를 위한 웹팩

선생님, output:{ fieldname : 'main.js', path: path.resolve( __dirname, 'dist') } 여기에서요, __dirname 대신에 fieldname 을 적어줘야 할 것 같은데 __dirname 를 적은 이유가 뭐에욤?

  • 웹팩
Oh Chocho 댓글 2 좋아요 1 조회수 700

채널 생성시 channelData.map is not a function

미해결

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

채널생성 클릭하면 channeldata.map is not a function이라고 에러가 뜨는데 channelData뿌려지는곳에 ?옵셔널도 줬고.. 아래처럼 잘 작성한것같은데 어딜 놓쳤는지 모르겠습니다. 새로고침하면 추가된 채널명이 출력됩니다. workspace import 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; createChannelModal import Modal from '@components/modal'; import useInput from '@hooks/useInput'; import { Button, Input, Label } from '@pages/signup/styles'; import { IChannel, IUser } from '@typings/db'; import fetcher from '@utils/fetcher'; import axios from 'axios'; import React, { useCallback, 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;

  • 웹팩
  • typescript
  • react
  • Socket.io
  • babel
  • 클론코딩
soh308 댓글 3 좋아요 0 조회수 617

안녕하세요. invailhost header 질문

미해결

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

현재 슬랙클론 수강중인 학생입니다. 저는 군대에서 독학으로 공부중입니다. 현재 제로초님 깃허브에서 git clone 하여 그대로 데모버전을 사용하고 있습니다. 제가 사용하는 에디터는 구름ide를 사용하고 있는데 구름 ide 에서는 localhost를 제공해주지 않고, 제가 port를 설정하면 구름에서 제공해주는 사이트 주소를 사용하는 식으로 운영됩니다. 제가 본 영상에서와 같이 dependency까지 그대로 다운받고, npm run dev 명령어를 실행하면 당연히 localhost가 없기때문에 사이트 접속이 힘듭니다.... 그래서 port 3090에 맞게 url에 접속하면 invaild host header 에러가 나옵니다.. 전에 위 에러를 만났을 때는 항상 cra로 개발했기 때문에 금방 구글링으로 해결을 했는데 현재는 어떻게 해결해야 할지 모르는 상황입니다..

  • babel
  • Socket.io
  • typescript
  • react
  • 웹팩
  • 클론코딩
댓글 2 좋아요 0 조회수 263

npx sequelize db:create 오류

미해결

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

ws@DESKTOP-9H6S8B6 MINGW64 ~/Desktop/sleact/back $ npx sequelize db:create Sequelize 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 비밀번호는 확실하게 맞습니다.

  • Socket.io
  • babel
  • react
  • 웹팩
  • typescript
  • 클론코딩
wsws 댓글 1 좋아요 1 조회수 632

500에러 질문

미해결

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

제로초님 깃헙코드 보고 리액트쿼리로 공부하다가 DM 보내는 로직에서 500에러가 떳습니다~!! api 문서대로 요청보냈고, 분명 서버쪽은 문제가 없을텐데 싶어서 며칠 째 고민하다 질문드려요!!! NaN으로 뜨는 부분이 백엔드 코드에서 콘솔 찍어보니까 req.query.perPage 이게 언디파인드로 전달 되더라구요. 근데 저는 perPage를 제로초님처럼 20으로 고정해서 전달하고 있는데 언디파인드로 뜨는게 이상하더라구요.ㅜㅜ +) 그리고 뮤테이션 쿼리키를 ["workspace", workspace, "dm", id, "chat"] 이렇게 주신 이유가 궁금해요!! 제가 공식문서 읽고 이해한게 배열로 줄 경우 첫번째 값이 캐싱할 때 쓰이는 이름이고 그 위에있는 애들은 mutation 안에서 사용될 외부 값을 넣어준다고 이해했었거든요!! 근데 dm 이나 chat은 사용되지 않는 것 같아서요!!

  • typescript
  • 웹팩
  • react
  • babel
  • Socket.io
  • 클론코딩
댓글 1 좋아요 0 조회수 369

npx sequelize db:create 입력시 에러 발생

미해결

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

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 정상적으로 설치했는데 관련되서 검색해도 해결책이 없어서 질문 남깁니다.

  • react
  • Socket.io
  • babel
  • typescript
  • 웹팩
  • 클론코딩
차은엽 댓글 5 좋아요 0 조회수 2265

app.vue안에 있는 nav를 따로 분리하고 싶은데 잘안됩니다..

해결됨

웹 게임을 만들며 배우는 Vue

안녕하세요. 현재 강의보고하다가 요즘은 책을 보고 공부중인데 app.vue안에 있는 nav를 따로 분리 하고 싶은데 방법이 있을까요? 이유는 특정 서브페이지에서는 nav가 안보여야하는데 그 특정 서브페이지에서도 nav가 보여서요 ㅠ css로 처리했더니 다시 돌아가면 그페이지들도 nav가 사라지더라구요 ㅠㅠ 혹시 방법이 잇을까요?

  • app.vue
  • vuejs
  • nav
  • 웹팩
  • vuex
댓글 2 좋아요 1 조회수 452

인기 태그

인프런 TOP Writers

주간 인기글