25%
66,000원
다른 수강생들이 자주 물어보는 질문이 궁금하신가요?
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
next 12 적용 관련질문
안녕하세요. 오늘 next.js 12버전 적용해봤는데요 데이터 변경하고 router.push()로 serversideprops 화면 이동할 때, 변경된 데이터가 바로 적용되지 않고 새로고침 해야 변경되는 현상이 있더라구요. 특이한게 최초진입 할 때는 바로 적용되는데 2번이상 동일한 액션을 취하면 새로고침이 필요했어요. 찾아보니까 next.js api 사이트에 페이지가 아니라 컴포넌트에 넣어야된다는 설명이 초반부에 나오는데요 제가 이해한게 맞는건가요? 한 page에 serversideprops가 필요한 컴포넌트가 많을 경우, 각 컴포넌트 별 로 넣어주는게 맞는지 질문드립니다.
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
리덕스 구현 중에 에러가 났습니다.
리덕스연결을 시키고 실행을 했는데 위와 같은 메세지가 발생했습니다. ㅜㅜ 답변해주시면 감사하겠습니다.!! applayout import 코드는 아래와 같습니다. _app.js import 코드
- 해결됨[리뉴얼] React로 NodeBird SNS 만들기
passport module에 대해 질문이 있습니다.
안녕하세요 passport-local를 사용할 때 username만 받게 되면 원래 안되는건가요? 제가 프로젝트를 진행하는데 메타마스크에서 지갑주소만 프론트에서 전달해주면 자동으로 로그인 되게끔 할려고 하는데 passwordField에 입력을 아예 안하면 401 에러만 자꾸 떠서 어쩔 수 없이 넣긴하였는데 형식상 nickname에 임의로 넣어서 postman에서 잘 동작 되는 것을 확인했습니다. module.exports = () => { passport.use(new LocalStrategy({ usernameField: `wallet_address`, passwordField: `nickname` }, async (wallet_address,nickname, done) => { try { const user = await User.findOne({ where : { wallet_address: wallet_address} }); if (user.wallet_address === wallet_address) { return done(null, user) } else { return done(null, false, { message: '가입되지 않은 회원입니다.' }); } } catch (error) { console.error(error); return done(error); } })); }; 그런데 logout을 강의처럼 코딩해서 했는데 Executing (default): SELECT `id`, `wallet_address`, `nickname`, `description`, `img_src`, `createdAt`, `updatedAt` FROM `users` AS `User` WHERE `User`.`id` = 1; POST /user/logout - - ms - - 이런식으로 떠서 동작이 잘 안되는거 같아서 이곳 저곳 뒤져봤는데 해결책을 마땅히 찾을 수 없어서 의심되는 부분이 password를 빼먹어서 그런가 싶어서 여쭈어보고 저는 비번을 딱히 사용을 원치 않는데 이것이 문제라면 혹시 이런 필수 파라미터를 제약 받지 않는 다른 모듈도 있는건가요?
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
안녕하세요 제로초님 프론트 앱 실행 시 에러가 발생해서 질문드립니다.
node_modules 삭제 후 다시 npm i 설치해도 같은 오류가 발생하고있습니다. 어떤 부분에서 문제가 발생하는지 찾기가 어려워서 질문 남깁니다. 강의 항상 잘보고있습니다 감사합니다
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
오류 발생 후 지속적 alert 발생 방지를 위해 error 상태 초기화 코드 작성 후 오류 질문 드립니다.
안녕하세요 아래 리렌더링 시 지속적으로 alert발생으로 인해 error 후 초기화 코드를 작성을 했습니다. 하지만, 제가 상태를 바꿔주지 않은 속성도 바뀌는 현상이 있습니다. 코드는 아래와 같습니다. 스위치 문 내에서는 상태의 변경이 없으나, 리덕스 데브툴에서는 아래처럼 제가 설정하지 않은 loginLoading 값이 변경되어 나옵니다.
- 해결됨[리뉴얼] React로 NodeBird SNS 만들기
faker사용시 이미지가 동일하게 출력되는데 오류가 있는걸까요?
안녕하세요 제로초님 강의 잘 듣고있습니다. faker를 사용해서 더미데이터를 출력해봤는데 제로초님과는 다르게 이미지가 같은 이미지로 출력되더라고요 제가 코드를 잘못작성해서인건지, 아니면 faker라이브러리의 문제인건지 혹시 이유를 알 수 있을까요? // pages/index.js import React, { useCallback, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { Button } from 'antd'; import { PlusCircleOutlined } from '@ant-design/icons'; import Link from 'next/link'; import { HomeWrapper, HomeLogoHeader, HomeLogoText, PageMainText, PageSubText, HomeInputWrapper } from '../pagestyles'; import { LOAD_POSTS_REQUEST } from '../reducers/post'; import AppLayout from '../components/AppLayout'; import PostList from '../components/HomePost/PostList'; const Home = () => { const dispatch = useDispatch(); const { me } = useSelector((state) => state.user); const { mainPosts, hasMorePosts, loadPostsLoading } = useSelector((state) => state.post); const onSearch = useCallback((value) => { console.log(value); }, []); useEffect(() => { dispatch({ type: LOAD_POSTS_REQUEST }); }, []); useEffect(() => { function onScroll() { console.log(window.scrollY, document.documentElement.clientHeight, document.documentElement.scrollHeight); if (window.scrollY + document.documentElement.clientHeight > document.documentElement.scrollHeight - 300) { if (hasMorePosts && !loadPostsLoading) { dispatch({ type: LOAD_POSTS_REQUEST }); } } }; window.addEventListener('scroll', onScroll); return () => { window.removeEventListener('scroll', onScroll); } }, [hasMorePosts, loadPostsLoading]); return ( <AppLayout> <HomeWrapper> <HomeLogoHeader> <HomeLogoText> <PageMainText>Recipe.io</PageMainText> <PageSubText>Have a delicious meal today</PageSubText> </HomeLogoText> {me && <Link href='/posting'><a><Button type='primary' size='large' icon={<PlusCircleOutlined />} >Create Recipe</Button></a></Link>} </HomeLogoHeader> <HomeInputWrapper placeholder="Search for Recipe" size='large' allowClear="true" enterButton onSearch={onSearch} /> <PostList mainPosts={mainPosts} /> </HomeWrapper> </AppLayout> ) }; export default Home; // reducers/post.js import produce from 'immer'; import shortId from 'shortid'; import { faker } from '@faker-js/faker'; export const initialState = { mainPosts: [], imagePaths: [], hasMorePosts: true, loadPostsLoading: false, loadPostsDone: false, loadPostsError: null, addPostLoading: false, addPostDone: false, addPostError: null, removePostLoading: false, removePostDone: false, removePostError: null, addCommentLoading: false, addCommentDone: false, addCommentError: null, removeCommentLoading: false, removeCommentDone: false, removeCommentError: null, }; export const generateDummyPost = (num) => Array(num).fill().map(() => ({ id: shortId.generate(), User: { id: shortId.generate(), nickname: faker.name.findName(), }, title: faker.lorem.slug(), desc: faker.lorem.sentence(), content: [{ ingredient: faker.lorem.paragraphs(), }, { recipes: faker.lorem.paragraphs(), }, { tips: faker.lorem.paragraphs(), }], Images: [{ id: shortId.generate(), src: faker.image.food(), }, { id: shortId.generate(), src: faker.image.food(), }, { id: shortId.generate(), src: faker.image.food(), }], tag: '맛있을것같아요 #hashtag1 짱짱맨!! #hashtag2 ##hashtag3', Comments: [{ id: shortId.generate(), User: { id: shortId.generate(), nickname: faker.name.findName(), }, content: faker.lorem.sentence(), }, { id: shortId.generate(), User: { id: shortId.generate(), nickname: faker.name.findName(), }, content: faker.lorem.sentence(), }] })); export const dummyPost = (data) => ({ id: data.id, User: { id: 2, nickname: 'Mirrer', }, title: data.content.title, desc: data.content.desc, content: [{ ingredient: data.content.ingredient, }, { recipes: data.content.recipes, }, { tips: data.content.tips, }], tag: data.content.tags, Images: [{ id: shortId.generate(), src: faker.image.food(), }, { id: shortId.generate(), src: faker.image.food(), }], Comments: [{ id: shortId.generate(), User: { id: shortId.generate(), nickname: faker.name.findName(), }, content: faker.lorem.sentence(), }], }); const dummyComment = (data) => ({ id: data.id, User: { id: 2, nickname: 'Mirrer', }, content: data.commentText, }); export const LOAD_POSTS_REQUEST = 'LOAD_POSTS_REQUEST'; export const LOAD_POSTS_SUCCESS = 'LOAD_POSTS_SUCCESS'; export const LOAD_POSTS_FAILURE = 'LOAD_POSTS_FAILURE'; 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 REMOVE_COMMENT_REQUEST = 'REMOVE_COMMENT_REQUEST'; export const REMOVE_COMMENT_SUCCESS = 'REMOVE_COMMENT_SUCCESS'; export const REMOVE_COMMENT_FAILURE = 'REMOVE_COMMENT_FAILURE'; export const addPostRequestAction = (data) => { return { type: ADD_POST_REQUEST, data, } }; export const removePostRequestAction = (data) => { return { type: REMOVE_POST_REQUEST, data, } }; export const addCommentRequestAction = (data) => { return { type: ADD_COMMENT_REQUEST, data, } }; export const removeCommentRequestAction = (data) => { return { type: REMOVE_COMMENT_REQUEST, data, } }; const reducer = (state = initialState, action) => { return produce(state, (draft) => { switch (action.type) { case LOAD_POSTS_REQUEST: draft.loadPostsLoading = true; draft.loadPostsDone = false; draft.loadPostsError = null; break; case LOAD_POSTS_SUCCESS: draft.loadPostsLoading = false; draft.loadPostsDone = true; draft.mainPosts = action.data.concat(draft.mainPosts); draft.hasMorePosts = draft.mainPosts.length < 30; break; case LOAD_POSTS_FAILURE: draft.loadPostsLoading = false; draft.loadPostsError = action.error; break; case ADD_POST_REQUEST: draft.addPostLoading = true; draft.addPostDone = false; draft.addPostError = null; break; case ADD_POST_SUCCESS: draft.addPostLoading = false; draft.addPostDone = true; draft.mainPosts.unshift(dummyPost(action.data)); 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; break; case REMOVE_POST_SUCCESS: 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)); draft.addCommentLoading = false; draft.addCommentDone = true; break; } case ADD_COMMENT_FAILURE: draft.addCommentLoading = false; draft.addCommentError = action.error; break; case REMOVE_COMMENT_REQUEST: draft.removeCommentLoading = true; draft.removeCommentDone = false; draft.removeCommentError = null; break; case REMOVE_COMMENT_SUCCESS: { // action.data = { postId: 1, commentId: '댓글id'} const post = draft.mainPosts.find((v) => v.id === action.data.postId); draft.removeCommentLoading = false; draft.removeCommentDone = true; break; } case REMOVE_COMMENT_FAILURE: draft.removeCommentLoading = false; draft.removeCommentError = action.error; break; default: break; } }); }; export default reducer; // sagas/post.js import { all, fork, delay, put, takeLatest } from 'redux-saga/effects'; import shortId from 'shortid'; // import axios from 'axios'; import { LOAD_POSTS_REQUEST, LOAD_POSTS_SUCCESS, LOAD_POSTS_FAILURE, generateDummyPost, ADD_POST_REQUEST, ADD_POST_SUCCESS, ADD_POST_FAILURE, REMOVE_POST_REQUEST, REMOVE_POST_SUCCESS, REMOVE_POST_FAILURE, ADD_COMMENT_REQUEST, ADD_COMMENT_SUCCESS, ADD_COMMENT_FAILURE, REMOVE_COMMENT_REQUEST, REMOVE_COMMENT_SUCCESS, REMOVE_COMMENT_FAILURE, } from '../reducers/post'; import { BOARD_ADD_POST_TO_ME, BOARD_REMOVE_POST_OF_ME } from '../reducers/user'; // function addPostAPI(data) { // return axios.post('/api/post', data); // } function* loadPosts(action) { try { // const result = yield call(addPostAPI, action.data); yield delay(1000); yield put({ type: LOAD_POSTS_SUCCESS, data: generateDummyPost(10) }) } catch(err) { yield put({ type: LOAD_POSTS_FAILURE, data: err.response.data }) } } // function addPostAPI(data) { // return axios.post('/api/post', data); // } function* addPost(action) { try { // const result = yield call(addPostAPI, action.data); const id = shortId.generate(); yield delay(1000); yield put({ type: ADD_POST_SUCCESS, data: { id, content: action.data, } }) yield put({ type: BOARD_ADD_POST_TO_ME, data: { id, content: action.data, } }) } catch(err) { yield put({ type: ADD_POST_FAILURE, data: err.response.data }) } } // function addPostAPI(data) { // return axios.post('/api/post', data); // } function* removePost(action) { try { // const result = yield call(addPostAPI, action.data); yield delay(1000); yield put({ type: REMOVE_POST_SUCCESS, data: action.data, }) yield put({ type: BOARD_REMOVE_POST_OF_ME, data: action.data, }) } catch(err) { yield put({ type: REMOVE_POST_FAILURE, data: err.response.data }) } } // function addCommentAPI(data) { // return axios.post(`/api/post/${data.postId}/comment`, data); // } function* addComment(action) { const id = shortId.generate(); try { // const result = yield call(addCommentAPI, action.data); yield delay(1000); yield put({ type: ADD_COMMENT_SUCCESS, data: { id, postId: action.data.postId, commentText: action.data.commentText, } }) } catch(err) { yield put({ type: ADD_COMMENT_FAILURE, data: err.response.data }) } } // function addPostAPI(data) { // return axios.post('/api/post', data); // } function* removeComment(action) { try { // const result = yield call(addPostAPI, action.data); yield delay(1000); yield put({ type: REMOVE_COMMENT_SUCCESS, data: { postId: action.data.postId, commentId: action.data.commentId, }, }) } catch(err) { yield put({ type: REMOVE_COMMENT_FAILURE, data: err.response.data }) } } function* watchLoadPosts() { yield takeLatest(LOAD_POSTS_REQUEST, loadPosts); } function* watchAddPost() { yield takeLatest(ADD_POST_REQUEST, addPost); } function* watchRemovePost() { yield takeLatest(REMOVE_POST_REQUEST, removePost); } function* watchAddComment() { yield takeLatest(ADD_COMMENT_REQUEST, addComment); } function* watchRemoveComment() { yield takeLatest(REMOVE_COMMENT_REQUEST, removeComment); } export default function* postSaga() { yield all([ fork(watchLoadPosts), fork(watchAddPost), fork(watchRemovePost), fork(watchAddComment), fork(watchRemoveComment), ]); }
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
Toolkit 사용 시 질문드립니다.
안녕하세요 제로초님,강의로 배운 코드를 Redux-Toolkit으로 전환하여 실행시켜보았는데요. context.store.dispatch(END); await context.store.sagaTask.toPromise(); SSR 코드에 위 코드가 없어진 것을 제외하면 크게 다른 부분은 없는 것 같은데 에러가 납니다. 에러는 처음 실행 시 Hydration failed because the initial UI does not match what was rendered on the server. 이렇게 나타나는데 에러창을 끄면 일단 초기 10개의 게시물은 불러와집니다. 이후 스크롤을 내려 dispatch(loadPosts({lastId}))가 실행되면 404 에러가 나면서 rejected됩니다.(그리고 콘솔에서는 404 에러가 나오는데 에디터 터미널에서는 처음 성공한 200코드 이후에 반응이 없습니다.) 검색해보니 위 에러코드는 SSR로 렌더링된 UI와 클라이언트 UI가 일치하지 않아서라고 하는데Toolkit 적용하기 전의 코드는 잘되어서 어느 부분을 체크해보아야 할 지 모르겠습니다. Toolkit적용 시 SSR을 위해 따로 고려해야하는 부분이 있을까요?
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
antd Menu toggle device toolbar 질문입니다.
메뉴를 구현하고 모바일버전에서도 확인을 하고싶어서 개발자도구에서 toggle device toolbar를 클릭하고 메뉴의 버튼들을 눌렀는데 이러한 에러가 뜹니다. 그리고 모바일로 접속해서 클릭을 해봐도 동일한 오류가 뜹니다. 이러면은 antd-mobile로 메뉴를 두개로 구성을 해야하나요? 아래는 제 코드입니다. import PropTypes from "prop-types"; import React, { useCallback } from "react"; import Link from "next/link" import {Menu, Input, Row, Col} from "antd"; import UserProfile from "./UserProfile"; import LoginForm from "./LoginForm"; import styled from "styled-components"; import {useSelector} from "react-redux"; import Router from "next/router"; import useInput from "../hooks/useInput"; const InputSearch = styled(Input.Search)` vertical-align: middle; `; const AppLayout = ({ children }) => { const {me } = useSelector((state) => state.user); const [search, changeSearch] = useInput(''); const searchHashtag = useCallback(() => { Router.push(`/hashtag/${search}`); }, [search]); return ( <div> <Menu mode="horizontal"> <Menu.Item key="home"> <Link href={"/"}><a>home</a></Link> </Menu.Item> <Menu.Item key="profile"> <Link href={"/profile"}><a>profile</a></Link> </Menu.Item> <Menu.Item key="search"> <InputSearch enterButton value={search} onChange={changeSearch} onSearch={searchHashtag}/> </Menu.Item> <Menu.Item key="signup"> <Link href={"/signup"}><a>signup</a></Link> </Menu.Item> </Menu> <Row gutter={8}> <Col xs={24} md={6}> {me ? <UserProfile/> : <LoginForm />} </Col> <Col xs={24} md={12}>{children}</Col> <Col xs={24} md={6}><a href="https://velog.io/@mayrang" target="_blank" rel="noreferrer noopener">Made by Mayrang</a> </Col> </Row> </div> ); }; AppLayout.propTypes = { children: PropTypes.node.isRequired, }; export default AppLayout;
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
async/await에 대한 이해가 맞는지 확인 해주 실 수 있으실까요?
안녕하세요 ~ 강의 잘 듣고 있습니다 async / await을 파면서 궁금한점이 생겼는데 혹시 알려 주실 수 있으시면 정말 감사하겠습니다 첫번째 예시 reader.onload = async e =>{ array.push(e.target.result) let copyPreview = [...preview] await copyPreview.push(...array); setPreview(copyPreview) } } } Copy 이렇게 코드가 있다는 가정 (제가 아는한에서 설명하겠습니다) 1. async안의 코드들은 전부 비동기로 작동한다 2. await 붙은 코드는 .then느낌으로 맨 마지막에 작동한다 // 이 정의가 맞을까요? 두번째 예시 (공부하면서 퍼온겁니다) async function showAvatar() { // JSON 읽기 let response = await fetch('/article/promise-chaining/user.json'); let user = await response.json(); // github 사용자 정보 읽기 let githubResponse = await fetch(`https://api.github.com/users/${user.name}`); let githubUser = await githubResponse.json(); } Copy let response = await fetch('/article/promise-chaining/user.json'); 라는 것은 fetch즉 백엔드에서 데이터를 불러오는 것이 비동기 이기 때문에 await을 안붙여주면 let response에 데이터르 다 불러오기전엔 undefined가 뜨고 다 불러온 후에야 response에 데이터가 담기기 때문에 await을 쓴건가요?
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
상태관리에 대해 질문드립니다.
안녕하세요 제로초님, 정말 기초적인 질문인 것 같은데 검색해보아도 긴가민가하여 명확하게 알고싶어 고민끝에 질문드립니다. 강의에서 배운 redux, redux-saga는 어느정도 이해가 되어 다른 상태관리 라이브러리로 대체해서 적용해보고자 공식문서와 깃허브에 올려주신 코드들을 보면서 먼저 어떤 차이가 있는 지 알아보았는데요. 다양한 라이브러리가 있지만 우선 깃허브로 소개해주신 라이브러리만 보았을 때 SWR이나 React Query - 서버 상태관리 라이브러리 Redux Toolkit - 전역 상태관리 라이브러리 로 나누어 이해하였습니다. 그런데 깃허브에 올려주신 React Query 코드를 보면 상태관리가 리덕스나 리덕스툴킷없이 리액트쿼리로만 작성되어 있습니다. Redux Toolkit을 적용한 코드에는 중간중간 SWR이 적용되어 있었구요. 여기서 궁금한 점은 Redux Toolkit은 전역 상태관리이기때문에 SWR이나 React Query 없이 단독으로 전체 관리가 가능하다고 생각했는데 React Query만으로 작성된 코드를 보니 조금 헷갈리는데요. 노드버드 프로젝트에서는 클라이언트 상태관리가 필요없이 모두 비동기로 이루어져 가능했던 것인지 아니면 다른 이유가 있는 것인지 궁금합니다. 현재 깃허브에 올라와 있는 React Query 코드에 기능 추가로 Redux Toolkit을 함께 쓰게 된다면 깃허브 Redux Toolkit 코드처럼 store와 reducers를 만들어서 그 기능에 대해서만 state 변경 적용을 해주면 되는 것일까요?
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
앱에서 까지 이용하고 싶은데 궁금한 부분이 있습니다
더 공부하기 위해 강좌를 통해 만든 서비스를 이용하여 아주 간단하게 앱까지 제작해보려 하고 있습니다. 리액트 네이티브에서 웹뷰 형태로 웹을 불러오려 하는데 반응형 퍼블리싱을 어떻게 해야할지 궁금해졌습니다. 웹은 일반적으로 미디어 쿼리를 통해 임의의 분기점 마다 스타일 값을 바꿔서 자연스럽게 보이게 합니다. 패딩을 예로 들면, 분기점이 320, 760, 1080 일 경우 350px과 400px 에서는 같은 패딩 값을 갖게 됩니다. 이때 같은 8px 이라 가정한다면, 두 경우에 패딩이 화면을 차지하는 비율이 다르게 됩니다. 따라서 이렇게 px로 값을 준 요소들은 디바이스 마다 차지하는 비율이 조금씩 다르게 되고, 화면이 전체적으로 다르게 느껴질겁니다. 웹에서도 상대 값인 % 단위로 값을 주는 부분도 있지만, 대부분 분기점에서 어떻게 바뀌어야 하는지를 더 신경쓰고 그 사이에서는 같은 px 로 주는 경우도 많다고 느꼈습니다. 그런데 앱에서 쓰일 웹뷰의 경우도 디자인과 크게 다르지 않다면 분기점 사이의 차이는 넘어가고디바이스 별로 화면이, 요소의 비율이 조금씩 다른건 신경쓰지 않아도 되는지 궁금합니다. 그래서 다른 예시를 찾아보니 어떤 앱 프로젝트에서는 마진, 패딩, 심지어 폰트 사이즈까지 상대 값인 %로 계산하여 값을 정한 코드도 보았습니다. 보통 앱에서/웹뷰에서 반응형 퍼블리싱을 어떻게 하는지, 앱개발을 해보신 제로초님께서는 어떻게 하시는지 궁금합니다.
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
대댓글 작성시 오류 발생
안녕하세요~ 강의 잘 듣고 있습니다. 대댓글 작성시 아래와 같은 오류가 발생하는데 ㅠ 원인을 잘 모르겠습니다. postcard 컴포넌트에서 발생하는거 같은데 다시 영상보고 코드를 봐도 수정이 안되서 다음 진도를 못 나가고 있습니다. 확인 부탁드리겠습니다.
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
pages폴더에서
혹시 pages폴더에 있는 파일 끼리 데이터 가져오고싶을떄는 어떻게해야할까요?
- 해결됨[리뉴얼] React로 NodeBird SNS 만들기
안녕하세요 제로초님 RTK-QUERY 관련해서 질문드립니다.
안녕하세요 제로초님 블로그하나를 만들고 있는데 react query 를 써보려 했더니 redux-toolkit에는 RTK-query 가 따로 있다 하더라구요 그래서 예제코드들을 쭉 둘러봤는데 대부분 provider에 감싸는 형태로 작성되더라구요. 하지만 next-redux-wrapper를 사용했는데 그에 대한 예제 코드들이라던가 사례들이 잘 보이지 않더라구요(제가 검색을 잘 못한건지..) 혹시 rtk-query를 사용 한다면 next-redux-wrapper를 걷어 내야할까요?
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
cors문제에 관한 질문입니다
안녕하세요 제로초님 react + spring boot로 팀프로젝트 진행중입니다. 이렇게 cors문제가 나오고 있습니다! 프론트에서 제가 프록시를 이용하지는 않고 있고 제 로컬에서 배포된 서버주소로 요청을 하면 이런식으로 오류가 뜨고 있는데, 혹시 어떻게 해야할지 궁금합니다...react에서 axios.defaults.headers["Access-Control-Allow-Origin"] = "*"; axios.defaults.withCredentials = true; 등 해봤는데 오류는 계속 같습니다. backend에서 cors문제가 정확히 안되어있는 것인지 궁금합니다..!!
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
Image모델 질문드립니다.
안녕하세요. Image모델 질문드립니다. 이미지 같은 경우는 포스트(Post)에 속해있기 때문에 포스트 모델 안에서 src: { type: DataTypes.STRING(300) } 로 만들어 줄 수도 있었을 거 같은데 따로 하나의 모델로 분리해서 만드신 이유가 포스트 안에 하나의 이미지가 아닌 여러 개의 이미지가 들어갈 수 있고, 그렇다면 배열 형태로 들어가야 하기 때문에 분리하신건가요?
- 해결됨[리뉴얼] React로 NodeBird SNS 만들기
ADD_COMMENT_SUCCESS acrtion이 실행되지 않는 질문드립니다.
ADD_COMMENT_REQUEST action은 아래와 같이 정상적으로 data도 전달하고 동작하는데 ADD_COMMENT_SUCCESS action이 실행되면 아래와 같은 오류가 발생해서 action이 실행되지 않는건 물론이고 다른 action도 동작하지 않는 문제가 발생해서 질문드립니다. // action 발생 컴포넌트 import React, { useCallback } from 'react'; import PropTypes from 'prop-types'; import { Button, Form, Input, Row } from 'antd'; import { useSelector, useDispatch } from 'react-redux'; import { addCommentRequestAction } from '../../reducers/post'; const CommentForm = ({ mainPosts }) => { const dispatch = useDispatch(); const { addCommentLoading } = useSelector((state) => state.post); const id = useSelector((state) => state.user.me?.id); const [form] = Form.useForm(); const onSubmitComment = useCallback((value) => { console.log(value.comment, mainPosts.id, id); dispatch(addCommentRequestAction({commentText: value.comment, postId: mainPosts.id, userId: id})); form.resetFields(); }, []); return ( <> <Form form={form} name="writeComment" onFinish={onSubmitComment} > <Form.Item name="comment" rules={[ { type: 'text', }, ]} > <Input.TextArea placeholder='댓글을 입력하세요.' showCount maxLength={100} rows={2} /> </Form.Item> <Row align='end'> <Button type='primary' htmlType='submit' loading={addCommentLoading} > 등록 </Button> </Row> </Form> </> ) }; CommentForm.propTypes = { mainPosts: PropTypes.shape({ id: PropTypes.string, User: PropTypes.object, title: PropTypes.string, desc: PropTypes.string, content: PropTypes.arrayOf(PropTypes.object), Images: PropTypes.arrayOf(PropTypes.object), tag: PropTypes.string, Comments: PropTypes.arrayOf(PropTypes.object), }) }; export default CommentForm; // reducers/post.js import produce from 'immer'; import shortId from 'shortid'; export const initialState = { mainPosts: [{ id: 1, User: { id: 1, nickname: 'ZeroCho' }, title: '돼지고기 갈비찜', desc: '한국인이 좋아하는 대표 고기 요리 갈비찜 레시피!!', content: [{ ingredient: '주 재료 = 갈비 600g, 당근 20g, 은행 10알, 밤 10개, 파 1대, 양파 50g, 양념장 재료 = 간장 3큰술, 설탕 2큰술, 육수 12큰술, 다진 생강 1작은술, 깨소금 2큰술, 청주 ¼컵, 다진 마늘 3큰술, 참기름 1큰술, 후춧가루 약간', }, { recipes: '갈비는 사방 5㎝ 크기로 썰어 기름기를 제거한다., 기름기를 없앤 갈빗살에 칼집을 낸 다음 찬물에 30분~한 시간쯤 담가 핏물을 빼주고, 혹시 모를 절단 과정에서 섞인 뼛가루나 뼛조각을 제거해준다. 이 핏물 빼는 과정을 속성으로 하고 싶으면, 한 번 끓여 데치는 거로 대체해도 되긴 된다, 끓는 물에 핏물을 뺀 갈비와 토막 낸 양파·파를 넣어 속까지 익을 때까지 삶아낸다. 중간에 젓가락으로 고기를 찔러보아 핏물이 나오는지 확인한다. 핏물이 나오면 고기가 덜 익은 것., 고기가 익으면 체에 받친다. 이 국물은 걸러서 지저분한 것을 제거하고 양념장의 육수로 이용한다. 갈비는 사방 5㎝ 크기로 썰어 기름기를 제거한다., 기름기를 없앤 갈빗살에 칼집을 낸 다음 찬물에 30분~한 시간쯤 담가 핏물을 빼주고, 혹시 모를 절단 과정에서 섞인 뼛가루나 뼛조각을 제거해준다. 이 핏물 빼는 과정을 속성으로 하고 싶으면, 한 번 끓여 데치는 거로 대체해도 되긴 된다, 끓는 물에 핏물을 뺀 갈비와 토막 낸 양파·파를 넣어 속까지 익을 때까지 삶아낸다. 중간에 젓가락으로 고기를 찔러보아 핏물이 나오는지 확인한다. 핏물이 나오면 고기가 덜 익은 것., 고기가 익으면 체에 받친다. 이 국물은 걸러서 지저분한 것을 제거하고 양념장의 육수로 이용한다.' }, { tips: '갈비를 한 번 끓여서 핏물이나 기름기를 어느 정도 빼준 후, 재료들을 압력밥솥에 싹 때려넣고 그대로 푹 익혀버리면 질긴 고기가 녹아드는 수준이 되어 부드럽게 먹을 수 있다.' }], Images: [{ src: 'https://recipe1.ezmember.co.kr/cache/recipe/2015/06/03/f6551b241deba537266c7dfe668e09821.jpg', }, { src: 'https://recipe1.ezmember.co.kr/cache/recipe/2017/09/15/af5ed61b01ce6d0c8ded20d59f0d15e31.jpg', }, { src: 'https://www.cj.co.kr/images/theKitchen/PHON/0000002320/0000009726/0000002320.jpg', }], tag: '맛있을것같아요 #hashtag1 짱짱맨!! #hashtag2 ##hashtag3', Comments: [{ User: { nickname: 'AppleLover', }, content: '우와 정말 맛있을것 같아요 ㅎㅎ', }, { User: { nickname: 'Nightmare', }, content: '오늘 저녁은 갈비찜이다!!', }] }], imagePaths: [], addPostLoading: false, addPostDone: false, addPostError: null, addCommentLoading: false, addCommentDone: false, addCommentError: null, }; const dummyPost = (data) => ({ id: shortId.generate(), User: { id: 2, nickname: 'Mirrer', }, title: data.title, desc: data.desc, content: [{ ingredient: data.ingredient, }, { recipes: data.recipes, }, { tips: data.tips, }], tag: data.tags, Images: [{ src: 'https://www.hongcheon.go.kr/site/tour/images/contents/cts1927_img1.jpg' }, { src: 'https://t1.daumcdn.net/cfile/tistory/1837BE1A4BF1BA073F' }], Comments: [{ User: { nickname: 'Korean', }, content: '역시 한국인은 밥심이죠 ㅎㅎ', }], }); const dummyComment = (data) => ({ User: { id: 2, nickname: 'Mirrer', }, content: data, }); 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 ADD_COMMENT_REQUEST = 'ADD_COMMENT_REQUEST'; export const ADD_COMMENT_SUCCESS = 'ADD_COMMENT_SUCCESS'; export const ADD_COMMENT_FAILURE = 'ADD_COMMENT_FAILURE'; export const addPostRequestAction = (data) => { console.log('reducer'); return { type: ADD_POST_REQUEST, data, } }; export const addCommentRequestAction = (data) => { console.log('reducer'); return { type: ADD_COMMENT_REQUEST, data, } }; const reducer = (state = initialState, 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 ADD_COMMENT_REQUEST: draft.addCommentLoading = true; draft.addCommentDone = false; draft.addCommentError = null; break; case ADD_COMMENT_SUCCESS: { const post = mainPosts.find((v) => v.id === action.data.postId); post.Comments.unshift(dummyComment(action.data.commentText)); draft.addCommentLoading = false; draft.addCommentDone = true; break; } case ADD_COMMENT_FAILURE: draft.addCommentLoading = false; draft.addCommentError = action.error; break; default: return state; } }); }; export default reducer; // sagas/post.js import { 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 } from '../reducers/post'; // function addPostAPI(data) { // return axios.post('/api/post', data); // } function* addPost(action) { try { // const result = yield call(addPostAPI, action.data); yield delay(1000); yield put({ type: ADD_POST_SUCCESS, data: action.data, }) } catch(err) { yield put({ type: ADD_POST_FAILURE, data: err.response.data }) } } // function addCommentAPI(data) { // return axios.post(`/api/post/${data.postId}/comment`, data); // } function* addComment(action) { console.log(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, data: err.response.data }) } } function* watchAddPost() { yield takeLatest(ADD_POST_REQUEST, addPost); } function* watchAddComment() { yield takeLatest(ADD_COMMENT_REQUEST, addComment); } export default function* postSaga() { yield all([ fork(watchAddPost), fork(watchAddComment), ]); }
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
unfollow_success질문드립니다.
안녕하세요. reducers/user.js의 unfollow_success 부분 질문드립니다. 불변성을 안 지키기 위해서는 splice를 사용하는 것이 맞다고 하셨는데 그럼 filter보다는 splice를 사용해서 배열에서 요소를 제거하는 것이 더 나은 방법인가요? 아무래도 filter를 사용하게 되면 새로운 배열을 생성하게 되기 때문에 splice를 사용하는 것이 더 나은 방법인지 아니면 크게 상관없는지 궁금합니다.
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
팔로워/팔로우 정보 불러오기에 대해 질문드립니다.
안녕하세요 제로초님, Profile 페이지에서 팔로워/팔로잉 목록을 불러오는 부분에서 질문이 있습니다. SWR을 적용하기 전, LOAD_FOLLOWERS_REQUEST / LOAD_FOLLOWINGS_REQUEST를 이용해 데이터를 가져오면서 백단의 코드에서 limit에 3을 주게 되면 프로필 페이지에 처음 접근했을 때, 아래처럼 UserProfile 부분인 팔로워/팔로우 갯수(me.Followers.length / me.Followings.length)가 3으로 뜹니다. (총 갯수는 각각 4개이구요) [더보기] 버튼을 눌러 데이터를 불러오면 그만큼 늘어납니다. 우선 프로필 페이지에 접근했을 때, 데이터를 불러오기 위한 REQUEST 요청이 갔고, 응답 받은 팔로워/팔로잉 데이터는 limit 설정으로 인해 3개뿐이기때문에 리듀서에서 SUCCESS 응답으로 3개의 데이터가 state에 저장되어 UserProfile 컴포넌트에서는 3으로 뜨는 것으로 이해했습니다. 여기서 궁금한 점은 1. SWR을 사용하지 않았을 때, 어떻게 설정해야 [더보기]버튼을 누르기 전에도 UserProfile 부분에 전체 팔로워/팔로잉 수를 가져올 수 있을까요? 2. attributes에 id와 nickname만 적었는데 follow 정보가 함께 들어오는데 이 정보는 제외하지못하는 것인지, nickname정보가 자꾸 나타났다 사라졌다 하는데 이를 해결하기 위해 확인해볼만한 부분이 있는 지 궁금합니다!
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
빌드시 bundle-analyzer 가 정상적으로 만들어지지 않습니다.
도저히 모르겠네요. 분명 다른예제들이랑 다를게 없는데 ㅠㅠ