묻고 답해요
161만명의 커뮤니티!! 함께 토론해봐요.
인프런 TOP Writers
-
해결됨[리뉴얼] React로 NodeBird SNS 만들기
게시글 삭제 시, 얕은 복사를 사용하지 않는 이유가 궁금합니다.
[ 게시글 삭제 saga 작성하기 ] 수강 중 질문 드립니다.이전 댓글을 추가할때는 reudce에서 case ADD_COMMENT_SUCCESS: { const postIndex = state.mainPosts.findIndex(v => v.id === action.data.postId); const post = { ...state.mainPosts[postIndex] }; // 얕은 복사 const Comments = [action.data, ...post.Comments]; const mainPosts = [...state.mainPosts]; mainPosts[postIndex] = { ...post, Comments, }; return { ...state, mainPosts, addCommentLoading: false, addCommentDone: true, addCommentError: false, }; }이런식으로 얕은 복사를 하셨는데요. case REMOVE_POST_SUCCESS: { const mainPosts = state.mainPosts.filter(v => v.id !== action.data.id); return { ...state, mainPosts, removePostLoading: false, removePostDone: true, removePostError: false, }; }게시글을 삭제할때는 왜 'state.mainPosts'를 얕은 복사해서 조작하지 않는 건지 궁금합니다.혹시 filter가 기존 state.mainPosts 을 수정하지 않으며, 새로운 배열을 return하기에 얕은 복사가 필요없는 건가요? 답변 기다리겠습니다.감사합니다 :)
-
해결됨비전공자를 위한 진짜 입문 올인원 개발 부트캠프
fly depoly 에러
그랩님 안녕하세요 다름이아니라 depoly 관련해서 문의드립니다.현재 fly depoly 부분에서 계속 에러가 발생하고있습니다.launch 부분에서도 그랩님과는 다르게 지역 선택이후로는 물어보는게 없이 바로 진행됐습니다.오류 내역 첨부드립니다ㅠㅠ다른 수강생분들에게도 문제 해결에 도움을 줄 수 있도록 좋은 질문을 남겨봅시다 :) 1. 질문은 문제 상황을 최대한 표현해주세요.2. 구체적이고 최대한 맥락을 알려줄 수 있도록 질문을 남겨 주실수록 좋습니다. 그렇지 않으면 답변을 얻는데 시간이 오래걸릴 수 있습니다 ㅠㅠex) A라는 상황에서 B라는 문제가 있었고 이에 C라는 시도를 해봤는데 되지 않았다!3. 먼저 유사한 질문이 있었는지 꼭 검색해주세요!
-
해결됨[리뉴얼] React로 NodeBird SNS 만들기
CSR에서 로그인 유지시 깜빡임을 없애는 방법이 있나요?
강좌에서는 SSR 적용전 CSR 만으로 로그인 유지를 할 때 초기상태가 로그인 유지가 안되있는 상태기 때문에 로그인정보 부분에 깜빡임이 발생하는데요. 혹시 CSR에서도 깜빡임 없이 로그인 유지를 할 수 있는 방법이 있나요? 혹시 리액트 라이프 사이클쪽을 건드려야 하는건가요?
-
미해결처음 만난 리액트(React)
코드 질문
위에 컴포넌트에서 최상위의 컴포넌트는 상위에서 받을 props가 없어서 안적어도 될 것 같은데 적는데는 다른 의미가 있나요?
-
해결됨처음 만난 리덕스(Redux)
action creator에 관해서 질문이 있습니다!
const ACTION_TYPE_ADD_TODO = "ADD_TODO"; const ACTION_TYPE_REMOVE_TODO = "REMOVE_TODO"; const ACTION_TYPE_REMOVE_ALL = "REMOVE_ALL"; function addTodoActionCreator(text) { return { type: ACTION_TYPE_ADD_TODO, text, }; } function removeTodoActionCreator() { return { type: ACTION_TYPE_REMOVE_TODO, }; } function removeAllActionCreator() { return { type: ACTION_TYPE_REMOVE_ALL, }; }위와 같은 actionCreator들을 하나의 함수로 묶어서 쓰는 경우는 잘 없나요? 예를 들어 아래처럼요!function actionCreator(type, payload = {}) { return { type, ...payload, }; }정답은 없겠지만 뭔가 위에 3개로 분리되어있는 것도 하나로 표현하면 좋지 않을까 싶어서 여쭤봅니다!
-
미해결[리뉴얼] React로 NodeBird SNS 만들기
redux toolkit으로 action.error에 원하는 메시지 넣는 방
if (exUser) { return res.status(403).send("이미 사용중인 아이디입니다." ); }이렇게 send를 보내고redux toolkit에서는 이 메시지를 error.message에 넣는 방법을 잘 모르겠는데 어떻게 하면 되나요?위에 사진은 signup/rejected의 Action안에 있는 정보입니다
-
미해결한 입 크기로 잘라 먹는 리액트(React.js) : 기초부터 실전까지
context api 사용시 ( ) 안에
createContext( ) 괄호 안에 데이터를 입력하면 어디에 쓰이나요?value에 기본값으로 해당 데이터가 들어 가는건가요?예를 들어 export const DiaryStateContext = React.createContext("HELLO"); 이런 식이면 저 HELLO를 어디서 꺼내서 쓸수 있을까요? const hello = useContext(DiaryStateContext);이런 식으로 해도 데이터가 들어 오는거 같진 않네요 ㅜㅜ
-
해결됨[코드캠프] 부트캠프에서 만든 고농축 프론트엔드 코스
Ant design + React hook form 사용법
게시물 포토폴리오 리펙토링 하는 과정에서 ant design icon 사용해 만든 별점 기능에 다른 form ( wrtier, password, contents)과 동일하게 react hook form 을 사용하려고 하는데 어떻게 사용해야 할지 모르겠습니다. 도와주세여 ㅠㅠ
-
미해결따라하며 배우는 노드, 리액트 시리즈 - 레딧 사이트 만들기(NextJS)(Pages Router)
context를 두개로 나눠쓰는 이유에 대해 궁금합니다.
StateContext안에 value로 state와 dispatch 두개를 전달하면 하나의 ContextProvider만 쓰면되는데, context를 두개로 나눈 이유가 무엇인지 궁금합니다!
-
미해결처음 만난 리액트(React)
npm install --save react-router-dom 설치 질문
해당 코드를 터미널에서 작성했는데 설치가 되지 않습니다.. 무슨 문제일까요...
-
해결됨[코드캠프] 부트캠프에서 만든 고농축 프론트엔드 코스
포트폴리오 만드는중에 GraphQL 질문
mutation으로 게시글 삭제 구현했는데비밀번호 확인할수있는 방법이 없어서 확인절차없이 전부 삭제가 됩니다API가 바뀌어야 할것같은데 확인부탁드려요
-
미해결이미지 관리 풀스택(feat. Node.js, React, MongoDB, AWS)
node js를 이용하고 mysql에 이미지 경로 저장
nodejs, multer를 통해서 이미지를 업로드했습니다db에 해당 파일명uuid()를 통해서 저장을 했는데 이미지 주소복사 해서 검색해서 들어가면 이미지가 나오는데 검색해도 못들어가게 하려면 어떻게해야하나요? 또는 관리자만 들어갈수있게하려는데 방법이 있을까요
-
해결됨파이썬/장고 웹서비스 개발 완벽 가이드 with 리액트
two form on a page 관련 질문
안녕하세요!한 페이지당 2개의 form을 사용해서 페이지를 만들려고 합니다.2개의 form을 만든 사례가 구글링을 해도 많이 보이지 않아서 직접 View class를 상속받아서만 만들어봤습니다.그래서 view를 다음과 같이 만들었지만 에러가 발생되어 구글링을 하거나 print문으로 찍어봐도 어디서 해결할지를 몰라서 여쭤봅니다.Traceback (most recent call last): File "/Users/jehakim/.pyenv/versions/assignment/lib/python3.10/site-packages/django/core/handlers/exception.py", line 55, in inner response = get_response(request) File "/Users/jehakim/.pyenv/versions/assignment/lib/python3.10/site-packages/django/utils/deprecation.py", line 136, in call response = self.process_response(request, response) File "/Users/jehakim/.pyenv/versions/assignment/lib/python3.10/site-packages/django/middleware/clickjacking.py", line 34, in process_response response.headers["X-Frame-Options"] = self.get_xframe_options_value(AttributeError: 'dict' object has no attribute 'headers'위 에러는 django middleware 단에서 발생했습니다. views.py한 화면에 버튼이 2개가 있고, 이 버튼에 따라 전송되는 form이 다른 상황입니다. 이런 상황일 때 버튼을 구분하는 방법을 구글링해보니 button tag에 name 속성으로 구분하면 된다고 하여"refusal-btn" 과 "approval-btn" 으로 구분했습니다.FBV로 먼저 만든 후 코드가 너무 지저분해 보여서 CBV로 바꿨습니다.views.py from django.shortcuts import render, redirect, get_object_or_404 from django.contrib.auth.decorators import login_required from django.utils.decorators import method_decorator from django.views.generic import FormView, ListView, View from django.views.generic.base import ContextMixin from django.contrib.auth import login, logout from django.urls import reverse_lazy from django.utils import timezone from django.db.models import Q from config.settings.base import COUNTS_PER_PAGE from accounts.utils import ( authorization_filter_on_employee_list, authorization_filter_on_signup_list, ) from accounts.forms import ( ResignationForm, EmployeeForm, SignUpForm, LoginForm, UserForm, ) from accounts.models import User, Employee @method_decorator(login_required(login_url=reverse_lazy("login")), name="get") class SignupUserDetailView(View, ContextMixin): user_object = None user_form = None employee_form = None context = dict() def post(self, request, **kwargs): self.user_form = UserForm(request.POST, instance=self.user_object) if self.user_form.is_valid(): state = self.user_form.cleaned_data.get("state") self.user_object.state = state if "refusal-btn" in request.POST: reason_for_refusal = self.user_form.cleaned_data.get( "reason_for_refusal" ) rejected_at = timezone.now() self.user_object.reason_for_refusal = reason_for_refusal self.user_object.rejected_at = rejected_at self.user_object.save() else: # approval-btn 일 때 self.employee_form = EmployeeForm(request.POST) if self.employee_form.is_valid(): cleaned_data = self.employee_form.cleaned_data grade = self.employee_form.data.get("grade") authorizations = { "authorization_grade": grade, "signup_approval_authorization": cleaned_data.get( "signup_approval_authorization" ), "list_read_authorization": cleaned_data.get( "list_read_authorization" ), "update_authorization": cleaned_data.get( "update_authorization" ), "resign_authorization": cleaned_data.get( "resign_authorization" ), } employee = Employee.objects.create( user_id=self.user_object.id, **authorizations ) employee.save() self.user_object.save() return render(request, "detail.html", self.get_context_data()) return render(request, "detail.html", self.get_context_data()) def get(self, request, user_id): self.user_object = get_object_or_404(User, pk=user_id) self.user_form = UserForm(instance=self.user_object) self.employee_form = EmployeeForm() return render(request, "detail.html", self.get_context_data()) def get_context_data(self, **kwargs): self.context["user_form"] = self.user_form self.context["employee_form"] = self.employee_form self.context["signup_list"] = True return self.context제일 하단에 있는 signup_list 는 하나의 템플릿 .html을 사용하는데 두 개의 view에서 이를 공유하기 때문에 이를 구분하고자 만든 flag 변수입니다.제가 알기로는 render 함수는 HttpResponse를 반환하는데 그러면 header가 당연히 있지 않나? 라는 생각이라서 부딪혔습니다.제 생각에는 기존에 다른 View 종류들을 상속받으면 해결해주는 거를 순수히 View만 상속받아서 어느 부분을 놓친 것 같은데, 이러면 어느 클래스를 상속받으면 될지, 아니면 직접 뭔가를 해야할지 모르겠습니다.읽어주셔서 감사합니다.advice 해주시면 감사하겠습니다 ㅠㅠ
-
해결됨[리뉴얼] React로 NodeBird SNS 만들기
post에 게시글 추가시 오류
로그인 후 post를 추가하면 아래와 같이 오류가 납니다 ㅠㅠ!화면에러내용Unhandled Runtime Error TypeError: Cannot read properties of undefined (reading '0') Source components\PostCard.js (47:27) @ PostCard 45 | <Card 46 | // post안에 Images가 있는 경우 > 47 | cover={post.Images[0] && <PostImages images={post.Images} />} | ^ 48 | actions={[ 49 | <RetweetOutlined key="retweet" />, 50 | liked ? (콘솔 추가 에러내용react-dom.development.js:19527 The above error occurred in the <PostCard> component: in PostCard (at pages/index.js:24) in div (created by Context.Consumer) in Col (at AppLayout.js:57) in div (created by Context.Consumer) in Row (at AppLayout.js:53) in div (at AppLayout.js:35) in AppLayout (at pages/index.js:21) in Home (at _app.js:13) in NodeBird (created by withRedux(NodeBird)) in Provider (created by withRedux(NodeBird)) in withRedux(NodeBird) in ErrorBoundary (created by ReactDevOverlay) in ReactDevOverlay (created by Container) in Container (created by AppContainer) in AppContainer in Root page/index.jsimport { all, fork } from "redux-saga/effects"; import postSaga from "./post"; import userSaga from "./user"; export default function* rootSaga() { yield all([fork(postSaga), fork(userSaga)]); } reducer/post.jsimport shortId from "shortid"; import produce from "immer"; import immer from "immer"; export const initailState = { mainPosts: [ { id: 1, User: { id: 1, nickname: "행복", }, content: "첫 번째 게시글 #해시태그 #해시태그2", Images: [ { src: "https://bookthumb-phinf.pstatic.net/cover/137/995/13799585.jpg?udate=20180726", }, { src: "https://gimg.gilbut.co.kr/book/BN001958/rn_view_BN001958.jpg", }, { src: "https://gimg.gilbut.co.kr/book/BN001998/rn_view_BN001998.jpg", }, ], Comments: [ { User: { nickname: "nero", }, content: "우와 개정판이 나왔군요~", }, { User: { nickname: "hero", }, content: "얼른 사고싶어요~", }, ], }, ], imagePaths: [], addPostLoading: false, addPostDone: false, addPostError: null, removePostLoading: false, removePostDone: false, removePostError: null, addCommentLoading: false, addCommentDone: false, addCommentError: null, }; export const ADD_POST_REQUEST = "ADD_POST_REQUEST"; export const ADD_POST_SUCCESS = "ADD_POST_SUCCESS"; export const ADD_POST_FAILURE = "ADD_POST_FAILURE"; export const REMOVE_POST_REQUEST = "REMOVE_POST_REQUEST"; export const REMOVE_POST_SUCCESS = "REMOVE_POST_SUCCESS"; export const REMOVE_POST_FAILURE = "REMOVE_POST_FAILURE"; export const ADD_COMMENT_REQUEST = "ADD_COMMENT_REQUEST"; export const ADD_COMMENT_SUCCESS = "ADD_COMMENT_SUCCESS"; export const ADD_COMMENT_FAILURE = "ADD_COMMENT_FAILURE"; export const addPost = (data) => { return { type: ADD_POST_REQUEST, data, }; }; export const addComment = (data) => { return { type: ADD_COMMENT_REQUEST, data, }; }; const dummyPost = (data) => ({ id: data.id, content: data.content, User: { id: 1, nickname: "행복", }, }); const dummyComment = (data) => ({ id: shortId.generate(), content: data, User: { id: 1, nickname: "행복", }, Images: [], Comments: [], }); const reducer = (state = initailState, action) => { return produce(state, (draft) => { switch (action.type) { case ADD_POST_REQUEST: draft.addPostLoading = true; draft.addPostDone = false; draft.addPostError = null; break; case ADD_POST_SUCCESS: draft.mainPosts.unshift(dummyPost(action.data)); draft.addPostLoading = false; draft.addPostDone = true; break; case ADD_POST_FAILURE: draft.addPostLoading = false; draft.addPostError = action.error; break; case REMOVE_POST_REQUEST: draft.removePostLoading = true; draft.removePostDone = false; draft.removePostError = null; draft.mainPosts = state.main; break; case REMOVE_POST_SUCCESS: // draft.mainPosts = state.mainPosts.filter((v) => v.id !== action.data); draft.mainPosts = draft.mainPosts.filter((v) => v.id !== action.data); draft.removePostLoading = false; draft.removePostDone = true; break; case REMOVE_POST_FAILURE: draft.removePostLoading = false; draft.removePostError = action.error; break; case ADD_COMMENT_REQUEST: draft.addCommentLoading = true; draft.addCommentDone = false; draft.addCommentError = null; break; case ADD_COMMENT_SUCCESS: { // 게시글을 찾음 const post = draft.mainPosts.find((v) => v.id === action.data.postId); // 그 게시글 댓글에다가 새로운 댓글 추가 post.Comments.unshift(dummyComment(action.data.content)); draft.addCommentLoading = false; draft.addCommentDone = true; break; // const postIndex = state.mainPosts.findIndex( // (v) => v.id === action.data.postId // ); // const post = { ...state.mainPosts[postIndex] }; // post.Comments = [dummyComment(action.data.content), ...post.Comments]; // Comments 얕은 복사 후 dummyComment 추가 // const mainPosts = [...state.mainPosts]; // mainPosts 새로운객체 생성 // mainPosts[postIndex] = post; // return { // ...state, // mainPosts, // addCommentLoading: false, // addCommentDone: true, // }; } case ADD_COMMENT_FAILURE: draft.addCommentLoading = false; draft.addCommentError = action.error; break; default: break; } }); }; export default reducer; //기본 구조 // const initailState = {}; // const reducer = (state = initailState, action) => { // switch (action.type) { // default: // return state; // } // }; // export default reducer; // 데이터를 구성하고 그에따라 액션구성후 reducer작성 그 후 화면 구성 sagas/post.jsimport { all, fork, delay, put, takeLatest } from "redux-saga/effects"; // import axios from "axios"; import { ADD_POST_REQUEST, ADD_POST_SUCCESS, ADD_POST_FAILURE, ADD_COMMENT_REQUEST, ADD_COMMENT_SUCCESS, ADD_COMMENT_FAILURE, REMOVE_POST_REQUEST, REMOVE_POST_SUCCESS, REMOVE_POST_FAILURE, } from "../reducers/post"; import { REMOVE_POST_OF_ME } from "../reducers/user"; import { ADD_POST_TO_ME } from "../reducers/user"; import shortId from "shortid"; function addPostAPI(data) { return axios.post("/api/post", data); // 서버에 요청 } function* addPost(action) { try { // const result = yield call(addPostAPI, action.data); yield delay(1000); const id = shortId.generate(); yield put({ type: ADD_POST_SUCCESS, data: { id, content: action.data, }, }); yield put({ type: ADD_POST_TO_ME, data: id, }); } catch (err) { yield put({ type: ADD_POST_FAILURE, error: err.response.data, }); } } function removePostAPI(data) { return axios.delete("/api/post", data); // 서버에 요청 } function* removePost(action) { try { yield delay(1000); yield put({ type: REMOVE_POST_SUCCESS, data: action.data, }); yield put({ type: REMOVE_POST_OF_ME, data: action.data, }); } catch (err) { yield put({ type: REMOVE_POST_FAILURE, error: err.response.data, }); } } function addcommentAPI(data) { return axios.post(`/api/post/${data.postId}/comment`, data); // 서버에 요청 } function* addComment(action) { try { // const result = yield call(addCommentAPI, action.data); yield delay(1000); yield put({ type: ADD_COMMENT_SUCCESS, data: action.data, }); } catch (err) { yield put({ type: ADD_COMMENT_FAILURE, error: err.response.data, }); } } function* watchAddPost() { yield takeLatest(ADD_POST_REQUEST, addPost); } function* watchRemovePost() { yield takeLatest(REMOVE_POST_REQUEST, removePost); } function* watchAddComment() { yield takeLatest(ADD_COMMENT_REQUEST, addComment); } export default function* postSaga() { yield all([fork(watchAddPost), fork(watchRemovePost), fork(watchAddComment)]); } components/PosdCard.jsimport React, { useState, useCallback } from "react"; import { Card, Popover, Button, Avatar, Comment, List } from "antd"; import PropTypes from "prop-types"; import CommentForm from "./CommentForm"; import { RetweetOutlined, HeartOutlined, MessageOutlined, EllipsisOutlined, HeartTwoTone, } from "@ant-design/icons"; import { useDispatch, useSelector } from "react-redux"; import PostImages from "./PostImages"; import PostCardContent from "./PostCardContent"; import { REMOVE_POST_REQUEST } from "../reducers/post"; const PostCard = ({ post }) => { const dispatch = useDispatch(); const { removePostLoading } = useSelector((state) => state.post); const [liked, setLiked] = useState(false); const [commentFormOpened, setCommentFormOpened] = useState(false); const onToggleLike = useCallback(() => { setLiked((prev) => !prev); }, []); const onToggleComment = useCallback(() => { setCommentFormOpened((prev) => !prev); }, []); const id = useSelector((state) => state.user.me?.id); // = const id = useSelector((state) => state.user.me && state.user.me.id); //const { me } = useSelector((state) => state.user); //const id = me?.id; // 로그인을 한 상태이고 아이디가 있으면 으로 해석해서 설계해보자 // const id = me && me.id; const onRemovePost = useCallback(() => { dispatch({ type: REMOVE_POST_REQUEST, data: post.id, }); }, []); return ( <div style={{ marginBottom: 20 }}> <Card // post안에 Images가 있는 경우 cover={post.Images[0] && <PostImages images={post.Images} />} actions={[ <RetweetOutlined key="retweet" />, liked ? ( <HeartTwoTone twoToneColor="#eb2f96" key="heart" onClick={onToggleLike} /> ) : ( <HeartOutlined key="heart" onClick={onToggleLike} /> ), <MessageOutlined key="comment" onClick={onToggleComment} />, <Popover key="more" content={ <Button.Group> {/* 내아이디랑 게시글작성아디가 나랑 같으면 */} {id && post.User.id === id ? ( <> <Button>수정</Button> <Button type="danger" onClick={onRemovePost} loading={removePostLoading} > 삭제 </Button> </> ) : ( <Button>신고</Button> )} </Button.Group> } > <EllipsisOutlined /> </Popover>, ]} > <Card.Meta avatar={<Avatar>{post.User.nickname[0]}</Avatar>} title={post.User.nickname} description={<PostCardContent postData={post.content} />} /> </Card> {commentFormOpened && ( <div> {/* post를 넘겨주는 이유는? 어떤 게시글에 댓글을 달건지 게시글의 id가 필요하기 때문에 */} <CommentForm post={post} /> <List header={`${post.Comments.length}개의 댓글`} itemLayout="horizontal" dataSource={post.Comments} //post.Comments가 각각 item으로 들어감 renderItem={(item) => ( <li> <Comment author={item.User.nickname} avatar={<Avatar>{item.User.nickname[0]}</Avatar>} content={item.content} /> </li> )} /> </div> )} </div> ); }; PostCard.propTypes = { post: PropTypes.shape({ id: PropTypes.number, User: PropTypes.object, content: PropTypes.string, createdAt: PropTypes.object, Comments: PropTypes.arrayOf(PropTypes.object), Images: PropTypes.arrayOf(PropTypes.object), }).isRequired, }; export default PostCard; componets/PostForm.jsimport React, { useCallback, useRef, useEffect } from "react"; import { Form, Input, Button } from "antd"; import { useDispatch, useSelector } from "react-redux"; import { addPost } from "../reducers/post"; import useInput from "../hooks/useInput"; const PostForm = () => { const { imagePaths, addPostDone } = useSelector((state) => state.post); const dispath = useDispatch(); const [text, onChangeText, setText] = useInput(""); useEffect(() => { if (addPostDone) { setText(""); } }, [addPostDone]); const onSubmit = useCallback(() => { dispath(addPost(text)); }, [text]); const imageInput = useRef(); const onClickImageUpload = useCallback(() => { imageInput.current.click(); }, [imageInput.current]); return ( <Form onFinish={onSubmit} style={{ margin: "10px 0 20px" }} encType="multipart/form-data" > <Input.TextArea value={text} onChange={onChangeText} maxLength={140} placeholder="어떤 신기한 일이?" /> <div> <input type="file" multiple hidden ref={imageInput} /> <Button onClick={onClickImageUpload}>이미지 업로드</Button> <Button type="primary" style={{ float: "right" }} htmlType="submit"> 등록 </Button> </div> <div> {imagePaths.map((v) => ( <div key={v} style={{ display: "inline-block" }}> <img src={v} style={{ width: "200px" }} alt={v} /> <div> <Button>제거</Button> </div> </div> ))} </div> </Form> ); }; export default PostForm;
-
해결됨[코드캠프] 부트캠프에서 만든 고농축 프론트엔드 코스
훈훈한 자바스크립트 섹션8 질문
자꾸 저런 오류가 뜨는데 레퍼런스 코드를 보고 참고해도 똑같네요 무엇이 문제인가요>? ㅜscript.js: const todoInput = document.querySelector("#todo-input"); const todoList = document.querySelector("#todo-list"); const savedTodoList = JSON.parse(localStorage.getItem("saved-items")); const createTodo = function (storageData) { let todoContents = todoInput.value; if (storageData) { todoContents = storageData.contents; } const newLi = document.createElement("li"); const newSpan = document.createElement("span"); const newBtn = document.createElement("button"); newBtn.addEventListener("click", () => { newLi.classList.toggle("complete"); saveItemsFn(); }); newLi.addEventListener("dblclick", () => { newLi.remove(); saveItemsFn(); }); if (storageData?.complete) { newLi.classList.add("complete"); } newSpan.textContent = todoContents; newLi.appendChild(newBtn); newLi.appendChild(newSpan); todoList.appendChild(newLi); todoInput.value = ""; saveItemsFn(); }; const keyCodeCheck = function () { if (window.event.keyCode === 13 && todoInput.value !== "") { createTodo(); } }; const deleteAll = function () { const liList = document.querySelectorAll("li"); for (let i = 0; i < liList.length; i++) { liList[i].remove(); } saveItemsFn(); }; const saveItemsFn = function () { const saveItems = []; for (i = 0; i < todoList.children.length; i++) { const todoObj = { contents: todoList.children[i].querySelector("span").textContent, complete: todoList.children[i].classList.contains("complete"), }; saveItems.push(todoObj); } saveItems.length === 0 ? localStorage.removeItem("saved-items") : localStorage.setItem("saved-items", JSON.stringify(saveItems)); // if (saveItems.length === 0) { // localStorage.removeItem("saved-items"); // } else { // localStorage.setItem("saved-items", JSON.stringify(saveItems)); // } }; if (savedTodoList) { for (let i = 0; i < savedTodoList.length; i++) { createTodo(savedTodoList[i]); } } const weatherSearch = function (position) { console.log(position.latitude); console.log(position.longitude); fetch( `https://api.openweathermap.org/data/2.5/weather?lat=${position.latitude}&lon=${position.longitude}&appid=8980c5d6dfb72e97b5871537ee59d9c1` ) .then((res) => { return res.json(); }) .then((json) => { console.log(json.name, json.weather[0].description); }) .catch((err) => { console.error(err); }); }; const accessToGeo = function (position) { const positionObj = { latitude: position.coords.latitude, longitude: position.coords.longitude, }; weatherSearch(positionObj); }; const askForLocation = function () { navigator.geolocation.getCurrentPosition(accessToGeo, (err) => { console.log(err); }); }; askForLocation();
-
미해결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;
-
해결됨[코드캠프] 부트캠프에서 만든 고농축 프론트엔드 코스
yup schema에 따로 타입을 지정해줘야 하나요?
vscode가 아닌 웹스톰을 사용하고 있습니다.에디터 문제인지는 모르겠지만schema에 계속 타입을 요구하길래schema: yup.ObjectSchema<any>로 넘어갔는데 괜찮겠죠?그런데 any를 안에 넣고 싶지 않아서다른 방법을 강구해봤는데 잘 안되네요 ㅠ라이브러리가 업데이트 되어서 그런건가요?
-
해결됨웹 게임을 만들며 배우는 React
RSP Hooks 방식의 UseEffect에서 setInterval이 '한번만' 동작하는 이유를 알고싶습니다!
useEffect(() => { interval.current = setInterval(changeHand, 100); console.log("다시 실행"); return () => { console.log("종료"); clearInterval(interval.current); }; }, []);위 코드는 '가위바위보' hooks에서 useEffect를 사용하는 코드 중 일부입니다. 제가 궁금한 것은 위 코드에서 왜 setInterval을 해주었는데도 '주먹'에서 '가위'로 바뀌고 멈추는가 입니다. componentDidMount() { // 비동기 요청 많이 한다고 함. // 해제안해주면 메모리를 많이 먹음 this.interval = setInterval(this.changeHand, 100); }클래스로 만든 rsp 코드에서는 컴포넌트가 생성되고 setInterval을 하면 주먹-가위-보가 100ms 주기로 잘 바뀝니다.useEffect()에서 두 번째 인자로 아무것도 넣어주지 않으면 초기 렌더링 될 시에만 실행되는 것으로 알고 있는데요, 한 번 실행된 코드에 setInterval이 있기 때문에 가위로만 바뀌는 게 아닌 100ms마다 주먹-가위-보로 반복되어야 하는 거 아닌가요? 이와 별개로 어떤 유료 리액트 코스보다 좋은 강의 감사드립니다.
-
미해결따라하며 배우는 리액트 네이티브 기초
맥이 있기는 한데..
16년도 맥이라,xcode 다운이 어렵네요..혹시 이렇게 되면 네이티브 cli 부분은진행이 어려운건가요?데스크탑은 있습니다,아그리고, npx expo start를 하고 카메라를 찍어봐도접속이 계속 안되네요...
-
해결됨[코드캠프] 부트캠프에서 만든 고농축 프론트엔드 코스
강의명에 오타가 있는 것 같습니다.
강의 너무 잘듣고 있습니다.23-05-login-chech-hoc / login-check-hoc-success앞쪽에 chech라고 오타가 있는 것 같아요