월 17,600원
5개월 할부 시다른 수강생들이 자주 물어보는 질문이 궁금하신가요?
- 해결됨[리뉴얼] React로 NodeBird SNS 만들기
Next 14 강의
안녕하세요,슬랙 채널에서 Next 14 강의 업로드 예정이시라는 글을 봤습니다. 해당 강좌의 리뉴얼 버전이라고 보면 될까요? 차이점이 궁금합니다.감사합니다.
- 해결됨[리뉴얼] React로 NodeBird SNS 만들기
미리보기가 보이지 않습니다
//app.js app.use('/', express.static(path.join(__dirname, 'uploads'))); //PostForm.js import { Button, Form, Input } from 'antd'; import React, { useCallback, useEffect, useRef } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components'; import useInput from '../hooks/useInput'; import { ADD_POST_REQUEST, REMOVE_IMAGE, UPLOAD_IMAGES_REQUEST, addPost, } from '../reducers/post'; const FormWrapper = styled(Form)` margin: 10px 0 20px; `; const ButtonStyle = styled(Button)` float: 'right'; `; const PostForm = () => { const { imagePaths, addPostLoading, addPostDone } = useSelector( (state) => state.post ); const dispatch = useDispatch(); const imageInput = useRef(); const [text, onChangeText, setText] = useInput(''); useEffect(() => { if (addPostDone) { setText(''); } }, [addPostDone]); const onSubmitForm = useCallback(() => { if (!text || !text.trim()) { return alert('게시글을 작성하세요.'); } const formData = new FormData(); imagePaths.forEach((p) => { formData.append('image', p); }); formData.append('content', text); return dispatch({ type: ADD_POST_REQUEST, data: formData, }); }, [text, imagePaths]); const onClickImageUpload = useCallback(() => { imageInput.current.click(); }, [imageInput.current]); const onChangeImages = useCallback((e) => { console.log('images', e.target.files); //배열모양을 띄는 객체 const imageFormData = new FormData(); [].forEach.call(e.target.files, (f) => { imageFormData.append('image', f); }); dispatch({ type: UPLOAD_IMAGES_REQUEST, data: imageFormData, }); }, []); const onRemoveImage = useCallback( (index) => () => { dispatch({ type: REMOVE_IMAGE, data: index, }); }, [] ); return ( <FormWrapper encType='multipart/form-data' onFinish={onSubmitForm}> <Input.TextArea value={text} onChange={onChangeText} maxLength={140} placeholder='어떤 신기한 일이 있었나요?' /> <div> <input onChange={onChangeImages} type='file' name='image' hidden multiple ref={imageInput} /> <Button onClick={onClickImageUpload}>이미지 업로드</Button> <ButtonStyle type='primary' htmlType='submit'> Twit </ButtonStyle> </div> <div> {imagePaths.map((item, i) => ( <div key={item} style={{ display: 'inline-block' }}> <img src={`http://localhost:3065/${item}`} style={{ width: '200px' }} alt={item} /> <div> <Button onClick={onRemoveImage(i)}>제거</Button> </div> </div> ))} </div> </FormWrapper> ); }; export default PostForm; //post.js const multer = require('multer'); const path = require('path'); const fs = require('fs'); const { Post, Image, Comment, User } = require('../models'); const { isLoggedIn } = require('./middlewares'); const router = express.Router(); try { fs.accessSync('uploads'); } catch (error) { console.log('uploads 폴더가 없으므로 생성합니다.'); fs.mkdirSync('uploads'); } //이미지나 동영상처리는 웬만하면 프론트에서 클라우드로 바로 올리는게 좋다. const upload = multer({ storage: multer.diskStorage({ destination(req, file, done) { done(null, 'uploads'); }, //배포때는 s3로 , 개발에는 드라이브에 filename(req, file, done) { const ext = path.extname(file.originalname); //확장자 추출 const basename = path.basename(file.originalname, ext); done(null, basename + '_' + new Date().getTime() + ext); //이나당151817842.png }, }), limits: { fileSize: 20 * 1024 * 1024 }, //20MB }); router.post('/', isLoggedIn, upload.none(), async (req, res, next) => { // POST /post try { const post = await Post.create({ content: req.body.content, UserId: req.user.id, }); if (req.body.image) { if (Array.isArray(req.body.image)) { const images = await Promise.all( req.body.image.map((image) => Image.create({ src: image })) ); await post.addImages(images); } else { const image = await Image.create({ src: req.body.image }); await post.addImages(image); } } console.log('POST', post); const fullPost = await Post.findOne({ where: post.id, include: [ { model: Image, }, { model: Comment, include: [{ model: User, attributes: ['id', 'nickname'] }], }, { model: User, //작성자 attributes: ['id', 'nickname'], }, { model: User, //좋아요 누른 사람 as: 'Likers', attributes: ['id'], }, ], }); res.status(201).json(fullPost); } catch (error) { console.error(error); next(error); } }); router.post( '/images', isLoggedIn, upload.array('image'), async (req, res, next) => { //POST /post/images try { console.log(req.files); res.json(req.files.map((v) => v.filename)); } catch (error) { console.error(error); next(error); } } ); 안녕하세요 제로초님! 질문이 있습니다.. 파일은 제대로 올라가서 uploads 폴더안에 있는데 화면에서 이미지를 가져오지 못하고있습니다.. 프론트-백 api통신도 잘 되는데, 왜 개발자도구-네트워크에서 이미지를 가져올 수 없는지 이유를 모르겠습니다 ㅠㅠ
- 해결됨[리뉴얼] React로 NodeBird SNS 만들기
[공유] Next.js 12버전 redirect
import { useRouter } from 'next/router'; const Profile = () => { ... const router = useRouter(); if (!me) { router.push('/'); return null; } ... }이런식으로 하면 잘 동작해요~
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
retweet 관련 질문이 있습니다.
다름이 아니라 리트윗빼고는 문제가 없습니다.하지만 리트윗을 하게 되면 데이터베이스에 userid가 null로 들어가면서 게시글을 불러올때 userid가 없기때문에 오류가 나는 것으로 보입니다... 해결해보려고 노력하는 중입니다만 어디가 문제인지 잘 모르겠습니다.const express = require("express"); const multer = require("multer"); const path = require("path"); const fs = require("fs"); const { Post, Image, Comment, User, Hashtag } = require("../models"); const { isLoggedIn } = require("./middlewares"); const router = express.Router(); try { fs.accessSync("uploads"); } catch (error) { console.log("uploads폴더가 없으므로 생성합니다."); fs.mkdirSync("uploads"); } const upload = multer({ storage: multer.diskStorage({ //어디에 저장할지 diskStorage => 하드디스크에 저장 destination(req, file, done) { done(null, "uploads"); }, filename(req, file, done) { // 제로초.png const ext = path.extname(file.originalname); // 확장자 추출(.png) const basename = path.basename(file.originalname, ext); // 제로초 done(null, basename + "_" + new Date().getTime() + ext); // 제로초15184712891.png }, }), limits: { fileSize: 20 * 1024 * 1024 }, // 20MB }); router.post("/", isLoggedIn, upload.none(), async (req, res, next) => { // 보기에는 "/"로 되어있지만 실제로는 "/post"로 되어있다. try { const hashtags = req.body.content.match(/#[^\s#]+/g); //hashtag 정규식 const post = await Post.create({ content: req.body.content, UserId: req.user.id, }); if (hashtags) { const result = await Promise.all( hashtags.map( (tag) => Hashtag.findOrCreate({ //있으면 가져오고 없으면 추가해라 where: { name: tag.slice(1).toLowerCase() }, }) //[[노드, true], [리액트, true]] 이런식으로 나옴 //slice는 글자만 떼기 위해 ex ) #react인 경우 react만 꺼냄 ) ); await post.addHashtags(result.map((v) => v[0])); //위에 같은 형식이기 때문에 0번째만 꺼내야함 } if (req.body.image) { if (Array.isArray(req.body.image)) { //여러개의 이미지를 올린 경우 const images = await Promise.all( req.body.image.map((image) => Image.create({ src: image })) ); await post.addImages(images); } else { const image = await Image.create({ src: req.body.image }); await post.addImages(image); } } const fullPost = await Post.findOne({ where: { id: post.id }, include: [ { model: Image, }, { model: Comment, include: [ { model: User, //댓글 작성자 attributes: ["id", "nickname"], }, ], }, { model: User, //게시글 작성자 attributes: ["id", "nickname"], }, { model: User, //좋아요 누른 사람 as: "Likers", attributes: ["id"], }, ], }); res.status(201).json(fullPost); //다시 프론트로 돌려주기 } catch (error) { console.error(error); next(error); } }); router.post("/:postId/comment", isLoggedIn, async (req, res, next) => { //:postId는 동적으로 바뀐다. //POST /post/comment // 보기에는 "/"로 되어있지만 실제로는 "/post"로 되어있다. try { const post = await Post.findOne({ //이 게시물이 진짜 있는지. where: { id: req.params.postId }, }); if (!post) { return res.status(403).send("존재하지 않는 게시글입니다."); } const comment = await Comment.create({ content: req.body.content, PostId: parseInt(req.params.postId, 10), //문자열로 넘어가기 때문에 int형으로 바꿔줘야한다. UserId: req.user.id, }); const fullComment = await Comment.findOne({ where: { id: comment.id }, include: [ { model: User, attributes: ["id", "nickname"], }, ], }); res.status(201).json(fullComment); } catch (error) { console.error(error); next(error); } }); router.patch("/:postId/like", isLoggedIn, async (req, res, next) => { //PATCH /post/1/like\ try { const post = await Post.findOne({ where: { id: req.params.postId } }); if (!post) { return res.status(403).send("게시글이 존재하지 않습니다."); } await post.addLikers(req.user.id); res.json({ PostId: post.id, UserId: req.user.id }); } catch (error) { console.error(error); next(error); } }); router.delete("/:postId/like", isLoggedIn, async (req, res, next) => { //DELETE /post/1/like try { const post = await Post.findOne({ where: { id: req.params.postId } }); if (!post) { return res.status(403).send("게시글이 존재하지 않습니다."); } await post.removeLikers(req.user.id); res.json({ PostId: post.id, UserId: req.user.id }); } catch (error) { console.error(error); next(error); } }); router.delete("/:postId", isLoggedIn, async (req, res, next) => { // DELETE /post/10 try { await Post.destroy({ where: { id: req.params.postId, UserId: req.user.id, }, }); res.status(200).json({ PostId: parseInt(req.params.postId, 10) }); } catch (error) { console.error(error); next(error); } }); router.post( "/images", isLoggedIn, upload.array("image"), //PostForm.js에서input에 올린 이미지가 배열로 들어감 (이미지를 여러장 올릴 수 있게 하기 위해서) async (req, res, next) => { // POST /post/images/ //이곳은 이미지 업로드 후 실행되는 부분, 업로드는 위에 upload에서 이미 다 올라감 console.log(req.files); res.json(req.files.map((v) => v.filename)); //프론트로 보내줌 } ); router.post("/:postId/retweet", isLoggedIn, async (req, res, next) => { // POST /post/1/retweet try { const post = await Post.findOne({ where: { id: req.params.postId }, include: [ { model: Post, as: "Retweet", }, ], }); if (!post) { return res.status(403).send("존재하지 않는 게시글입니다."); } if ( req.user.id === post.UserId || (post.Retweet && post.Retweet.UserId === req.user.id) ) { return res.status(403).send("자신의 글은 리트윗할 수 없습니다."); } const retweetTargetId = post.RetweetId || post.id; const exPost = await Post.findOne({ where: { UserId: req.user.id, RetweetId: retweetTargetId, }, }); if (exPost) { return res.status(403).send("이미 리트윗했습니다."); } const retweet = await Post.create({ UserId: req.user.id, RetweetId: retweetTargetId, content: "retweet", }); const retweetWithPrevPost = await Post.findOne({ where: { id: retweet.id }, include: [ { model: Post, as: "Retweet", include: [ { model: User, attributes: ["id", "nickname"], }, { model: Image, }, ], }, { model: User, attributes: ["id", "nickname"], }, { model: Image, }, { model: Comment, include: [ { model: User, attributes: ["id", "nickname"], }, ], }, ], }); res.status(201).json(retweetWithPrevPost); } catch (error) { console.error(error); next(error); } }); module.exports = router; //node에서는 import와 export defau lt를 사용하지 않고 require를 사용한다. //리트윗 case RETWEET_REQUEST: draft.retweetDone = false; draft.retweetLoading = true; draft.retweetError = null; break; case RETWEET_SUCCESS: draft.retweetLoading = false; draft.retweetDone = true; draft.mainPosts.unshift(action.data); break; case RETWEET_FAILURE: draft.retweetLoading = false; draft.retweetError = action.error; break; default: break; } }); export default reducer;//리트윗 function retweetAPI(data) { return axios.post(`/post/${data}/retweet`); //data는 formdata다 } function* retweet(action) { try { const result = yield call(retweetAPI, action.data); yield put({ type: RETWEET_SUCCESS, data: result.data, }); } catch (err) { console.error(err); yield put({ type: RETWEET_FAILURE, error: err.response.data }); } }
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
[공유] input 속성에 hidden 이 적용 안될 때
<input type='file' multiple style={{ display: 'none' }} />display: 'none'으로 줘보세요!
- 해결됨[리뉴얼] React로 NodeBird SNS 만들기
faker 적용 후 이미지가 깨지는 오류, 게시글 이미지 클릭 시 이미지 확대 화면의 fill-rule(fillRule) 에러에 관한 질문
안녕하세요! React로 NodeBird SNS 만들기섹션3 Redux-saga 연동까지 모두 수강한 수강생입니다!사전에 제로초님 강의와 트위터 클론 깃허브를 확인하였고, 구글링 및 에러를 번역했습니다!항상 강의 잘 보고 있습니다! 제로초님 항상 감사합니다!(1) 아래 질문 글과 동일하게 faker 이미지가 깨지는 현상이 발생했습니다!https://www.inflearn.com/course/lecture?courseSlug=%EB%85%B8%EB%93%9C%EB%B2%84%EB%93%9C-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EB%A6%AC%EB%89%B4%EC%96%BC&unitId=48831&category=questionDetail&tab=community&q=451330lorempixel 사이트 고장이 원인이라 다른 대체 사이트(placeimg)를 찾아봤지만 해결이 되지 않아 질문 올립니다!옆의 에러를 번역해보니 사이트 자체에서 이미지 찾을 수 없다고 합니다!+++(2) Warning: Invalid DOM property fill-rule. Did you mean fillRule? 에러게시글 이미지 클릭 시 콘솔에 fill-rule 에러가 발생합니다!faker 적용 전, 게시글 더미이미지가 있었을 때도 똑같은 에러가 발생하였습니다!더미이미지 삭제 전에도 진행 자체에 문제가 없고, 콘솔에 오류만 출력 되었는데 그냥 넘어가도 되는 에러인지 궁금합니다!(svg 관련 이미지는 적용한 적이 없습니다!)에러를 번역해보니 다음과 같았습니다.
- 해결됨[리뉴얼] React로 NodeBird SNS 만들기
로그인 실패 에러 (500 Internal Server Error)
회원가입 시 MySQL 테이블에 이메일, 비밀번호 저장되는 것을 확인하고 로그인을 시도했는데 계속 실패해서 질문드립니다 MySQL 워크벤치로 확인한 user 테이블을 보면 회원가입은 잘 되었는데 id: 2 의 메일, 비밀번호로 로그인이 안 됩니다 console.log로 확인해보니까 패스포트 로그인 시도에서 에러가 나는 것 같기는 한데 정확한 원인을 못 찾겠습니다.. 터미널에 뜨는 메시지입니다개발자도구_네트워크 화면입니다개발자도구_콘솔 화면입니다 의심스러운 코드입니다!routes/user.js 코드const express = require('express'); const bcrypt = require('bcrypt'); const passport = require('passport'); const { User } = require('../models'); const router = express.Router(); router.post('/login', (req, res, next) => { passport.authenticate('local', (err, user, info) => { if (err) { //서버쪽 에러 console.error(err); console.log('routes/user_server err') return next(err); } if (info) { //클라이언트쪽 에러 return res.status(401).send(info.reason); } return req.login(user, async (loginErr) => { if (loginErr) { //패스포트 로그인 에러 console.error(loginErr); console.log('routes/user_loginErr') return next(loginErr); } // res.setHeader('Cookie', 'cxlhy..랜덤토큰') return res.status(200).json(user); }); })(req, res, next); }); router.post('/', async (req, res, next) => { // POST /user try { const exUser = await User.findOne({ where: { email: req.body.email, } }); if (exUser) { return res.status(403).send('이미 가입된 메일입니다.'); } const hashedPassword = await bcrypt.hash(req.body.password, 10); //10~13 await User.create({ email: req.body.email, nickname: req.body.nickname, password: hashedPassword, }); res.status(201).send('OK'); } catch (error) { console.error(error); next(error); //next로 에러 처리 (한방에), status 500 } }); module.exports = router; app.js 코드const express = require('express'); const cors = require('cors'); const session = require('express-session'); const cookieParser = require('cookie-parser'); const passport = require('passport'); const dotenv = require('dotenv'); const postRouter = require('./routes/post'); const userRouter = require('./routes/user'); const db = require('./models'); const passportConfig = require('./passport'); dotenv.config(); const app = express(); db.sequelize.sync() .then(() => { console.log('db 연결 성공'); }) .catch(console.error); passportConfig(); app.use(cors({ origin: 'http://localhost:3000', credentials: false, })); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(cookieParser(process.env.COOKIE_SECRET)); app.use(session({ saveUninitialized: false, resave: false, secret: process.env.COOKIE_SECRET, })); app.use(passport.initialize()); app.use(passport.session()); ~이하생략~ passport/index.js 코드const passport = require('passport'); const local = require('./local'); const { User } = require('../models'); module.exports = () => { passport.serializeUser((user, done) => { done(null, user.id); }); passport.deserializeUser(async (id, done) => { try { const user = await User.findOne({ where: { id } }); done(null, user); //req.user } catch (error) { console.error(error); done(error); } }); local(); }; passport/local.js 코드const passport = require('passport'); const { Strategy: LocalStrategy } = require('passport-local'); const bcrypt = require('bcrypt'); const { User } = require('../models'); module.exports = () => { passport.use(new LocalStrategy({ usernameField: 'email', //id칸 passwordField: 'password', //비밀번호칸 }, async (email, password, done) => { //done으로 결과 판단 try { const user = await User.findOne({ //가입된 이메일이 있는지 검사 where: { email } }); if (!user) { return done(null, false, { reason: '존재하지 않는 이메일입니다.' }); } const result = await bcrypt.compare(password, user.password) if (result) { return done(null, user); } return done(null, false, { reason: '비밀번호가 일치하지 않습니다.' }); } catch (error) { console.error(error); return done(error); } })); };
- 해결됨[리뉴얼] React로 NodeBird SNS 만들기
서버사이드 렌더링 적용 후, css가 풀렸다가 다시 적용되는것 같아요.
안녕하세요!서버사이드 렌더링 적용후에 css가 풀렸다가 다시적용되는 것 같은데, 해결 방법이 있을까요?로그인 후 새로고침을 하면 잠깐 첫번째 화면이 보였다가 두번째 화면을 보여줍니다. 세번째 화면은 리덕스 상태입니다.서버사이드 렌더링을 해서 user와 post의 데이터는 잘 받아오고 있는것 같습니다.
- 해결됨[리뉴얼] React로 NodeBird SNS 만들기
faker 사용 후 postData.split is not a function 에러 질문
이전에는 잘 되었는데 faker로 더미데이터 사용 후에 다음과 같은 에러가 뜹니다!faker 버전 이슈를 보고 삭제 후 npm i -D faker@5 로 재설치했는데 오류가 해결되지 않았습니다ㅠ console.log 찍어본 postData의 타입이랑 에러메시지 입니다!4. WrappedApp created new store with withRedux(NodeBird) { initialState: undefined, initialStateFromGSPorGSSR: undefined }type : stringtype : functionTypeError: postData.split is not a functionat PostCardContent (C:\Users\pc\react-nodebird\prepare\front\.next\server\pages\index.js:1131:13)at processChild (C:\Users\pc\react-nodebird\prepare\front\node_modules\react-dom\cjs\react-dom-server.node.development.js:3043:14)at resolve (C:\Users\pc\react-nodebird\prepare\front\node_modules\react-dom\cjs\react-dom-server.node.development.js:2960:5)at ReactDOMServerRenderer.render (C:\Users\pc\react-nodebird\prepare\front\node_modules\react-dom\cjs\react-dom-server.node.development.js:3435:22)at ReactDOMServerRenderer.read (C:\Users\pc\react-nodebird\prepare\front\node_modules\react-dom\cjs\react-dom-server.node.development.js:3373:29)at renderToString (C:\Users\pc\react-nodebird\prepare\front\node_modules\react-dom\cjs\react-dom-server.node.development.js:3988:27)at Object.renderPage (C:\Users\pc\react-nodebird\prepare\front\node_modules\next\dist\next-server\server\render.js:50:851)at Document.getInitialProps (C:\Users\pc\react-nodebird\prepare\front\.next\server\pages\_document.js:264:19)at loadGetInitialProps (C:\Users\pc\react-nodebird\prepare\front\node_modules\next\dist\next-server\lib\utils.js:5:101)at renderToHTML (C:\Users\pc\react-nodebird\prepare\front\node_modules\next\dist\next-server\server\render.js:50:1142) PostCardContent.js 코드 중 의심스러운 부분입니다const PostCardContent = ({ postData }) => ( <div> {postData.split(/(#[^\s#]+)/g).map((v, i) => { if (v.match(/(#[^\s#]+)/)) { return <Link href={`/hashtag/${v.slice(1)}`} key={i}><a>{v}</a></Link>; } return v; })} </div> ); PostCardContent.propTypes = { postData: PropTypes.string.isRequired, };
- 해결됨[리뉴얼] React로 NodeBird SNS 만들기
slick에 이미지가 안 뜨는 오류
상세이미지에서 전체화면까지는 되었는데, 이미지가 안 떠서 질문드립니다 밑에 이미지 Indicator를 보면 스크롤하면 다음으로 넘어가는 기능은 잘 작동하는 것 같은데이미지 불러오기가 안 되는 것 같습니다만 이유를 모르겠습니다.. 컴파일도 잘 되었구요ㅠ ImagesZoom/index.js 코드 첨부합니다const ImagesZoom = ({ images, onClose }) => { const [currentSlide, setCurrentSlide] = useState(0); return ( <Overlay> <Global /> <Header> <h1>상세 이미지</h1> <CloseBtn onClick={onClose}>X</CloseBtn> </Header> <SlickWrapper> <div> <Slick initialSlide={0} beforeChange={(slide) => setCurrentSlide(slide)} infinite arrows={false} slidesToShow={1} slidesToScroll={1} > {images.map((v) => ( <ImgWrapper key={v.src}> <img src={v.src} alt={v.src} /> </ImgWrapper> ))} </Slick> <Indicator> <div> {currentSlide + 1} {' '} / {images.length} </div> </Indicator> </div> </SlickWrapper> </Overlay> ); }; PostImages.js 코드입니다const PostImages = ({ images }) => { const [showImagesZoom, setShowImagesZoom] = useState(false); const onZoom = useCallback(() => { setShowImagesZoom(true); }, []); const onClose = useCallback(() => { setShowImagesZoom(false); }, []); if (images.length === 1) { return ( <> <img role="presentation" src={images[0].src} alt={images[0].src} onClick={onZoom} /> {showImagesZoom && <ImagesZoom images={images} onClose={onClose} />} </> ); } if (images.length === 2) { return ( <> <div> <img style={{ width: "50%", display: 'inline-block' }} role="presentation" src={images[0].src} alt={images[0].src} onClick={onZoom} /> <img style={{ width: "50%", display: 'inline-block' }} role="presentation" src={images[1].src} alt={images[1].src} onClick={onZoom} /> </div> {showImagesZoom && <ImagesZoom images={images} onClose={onClose} />} </> ); } return ( <> <div> <img style={{ width: "50%" }} role="presentation" src={images[0].src} alt={images[0].src} onClick={onZoom} /> <div role="presentation" style={{ display: 'inline-block', width: '50%', textAlign: 'center', verticalAlign: 'middle' }} onClick={onZoom} > <PlusOutlined /> <br /> {images.length - 1}개의 사진 더보기 </div> </div> {showImagesZoom && <ImagesZoom images={images} onClose={onClose} />} </> ); };
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
안녕하세요 제로초님 이미지 업로드 관련 질문이 있습니다.
이미지 업로드를 위한 multer 강의까지 수강을 하였습니다.이미지 업로드를 할때 uploads 폴더에도 이미지가 잘 들어가고UPLOAD_IMAGES_SUCCESS도 잘 나오는 상황입니다.하지만 제로초님의 화면은 제거 버튼과 비록 깨지지만 이미지가 올라간 화면이 보이는데 저는 그 부분이 나오지 않아서 이 점이 궁금하여 질문 드립니다.import React, { useState } from "react"; import { Button, Card, Popover, Avatar, Image, List, Comment } from "antd"; import { RetweetOutlined, HeartOutlined, MessageOutlined, EllipsisOutlined, HeartTwoTone, } from "@ant-design/icons"; import { useCallback } from "react"; import { useSelector, useDispatch } from "react-redux"; import PropTypes from "prop-types"; import PostImages from "./PostImages"; import CommentForm from "./CommentForm"; import PostCardContent from "./PostCardContent"; import { REMOVE_POST_REQUEST, LIKE_POST_REQUEST, UNLIKE_POST_REQUEST, } from "../reducers/post"; import FollowButton from "./FollowButton"; const PostCard = ({ post }) => { //pages/index.js에서 mainPosts에서 하나씩 뜯어서 보내줌 const dispatch = useDispatch(); const [commentFormOpened, setCommentFormOpened] = useState(false); //댓글창 열지 말지 const onLike = useCallback(() => { dispatch({ type: LIKE_POST_REQUEST, data: post.id, }); }, []); //좋아요 const onUnlike = useCallback(() => { dispatch({ type: UNLIKE_POST_REQUEST, data: post.id, }); }, []); //좋아요 취소 const onToggleComment = useCallback(() => { setCommentFormOpened((prev) => !prev); }, []); //폼 버튼 한번 더 누르면 폼 닫기 const onRemovePost = useCallback(() => { return dispatch({ type: REMOVE_POST_REQUEST, data: post.id, }); }, [id]); const id = useSelector((state) => state.user.me?.id); const { removePostloading } = useSelector((state) => state.post); const liked = post.Likers.find((v) => v.id === id); //게시글 좋아요 누른 사람 중에 내가 있는지. return ( <div style={{ marginBottom: 20 }}> <Card cover={post.Images?.[0] && <PostImages images={post.Images} />} //이미지가 존재한다면 PostImages를 출력 actions={[ //카드 아래에 존재하는 것들 <RetweetOutlined key="retweet" />, liked ? ( <HeartTwoTone twoToneColor="red" onClick={onUnlike} /> ) : ( <HeartOutlined key="heart" onClick={onLike} /> ), <MessageOutlined onClick={onToggleComment} key="comment" />, <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>, ]} extra={id && <FollowButton post={post} />} > <Card.Meta //프로필과 내용 등 avatar={<Avatar>{post.User.nickname[0]}</Avatar>} title={post.User.nickname} description={<PostCardContent postData={post.content} />} /> </Card> {commentFormOpened && ( //commentFormOpened가 true이면 열어라 <div> {/* 어떤 게시글에 댓글을 남기는지.. */} <CommentForm post={post} /> <List header={`${post.Comments.length}개의 댓글`} itemLayout="horizontal" dataSource={post.Comments} //데이터는 여기서 가져와서 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.string, Comment: PropTypes.arrayOf(PropTypes.object), Images: PropTypes.arrayOf(PropTypes.object), Likers: PropTypes.arrayOf(PropTypes.object), }).isRequired, }; export default PostCard;저의 PostCard 코드입니다 이 코드에서 cover={post.Images?.[0] && <PostImages images={post.Images} />}이 부분이 이미지들을 출력해주는 부분이 아닌가요?? 저의 화면에는 아래처럼 나오지 않습니다.
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
게시글 작성 오류
안녕하세요 제로초님!게시글을 작성하게되면 아래와 같이 성공했다고 응답도 잘 도착하지만 id가 undefined이라고 오류가 납니다.제가 어느 부분을 놓치고 있는건가요??
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
로그인 오류
안녕하세요 제로초님 middleware.js를 사용하여로그인했을때와 하지않았을 때의 경우를 나눠놓으셨자나요!그 강좌를 듣고 코드를 그대로 작성하고 로그인을 진행해보는데올바른 이메일과 비밀번호를 입력해도 이 알림이 뜹니다.. 로그인도 실패로 응답하구요.. 어떤 부분이 문제일 가능성이 높을까요??
- 해결됨[리뉴얼] React로 NodeBird SNS 만들기
특정 페이지의 비로그인 차단
강좌에서처럼 me(내 정보 불러오는 상태변수) 변수를 통해 로그인 유지를 하고특정 페이지에서 비로그인 사용자를 차단할경우useEffect(() => { if(!me){ redirects(); } },[])이런식으로 로직을 짤수가 있는데문제가 브라우저 url로 접속할 경우 초기 me 의 상태가 null 이기 때문에 로그인을 한 상태더라도리다이렉트가 되는 문제가 있었습니다. 물론 ssr을 이용하면 이 문제가 해결은 되지만csr만 이용하는 선에서 리다이렉트가 정상적으로 작동하도록 하고 싶은데 어떻게 하면 좋을까요?
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
안녕하세요 제로초님 로그인 관련 redux 질문있습니다.
다름이 아니라 제로초님의 로그인 문제 해결하기 챕터를 보다가Posts.length관련 오류 해결하는 강의를 보는데제로초님의 redux화면이랑 저의 화면이랑 구성이 좀 다른 것 같아서 문의드립니다.제로초님의 화면은 위 사진처럼 me라는 객체로 나와서 확인하기 쉬운데제 화면은 아래처럼 그냥 데이터 라고 나오고 있습니다..어느 부분을 수정해야하는지 잘 모르겠어서 질문드립니다.
- 해결됨[리뉴얼] React로 NodeBird SNS 만들기
글 작성이 안 되는 오류
컴파일은 오류 없이 잘 되는데, 폼에서 짹짹(게시)버튼을 눌러도 글 작성이 되지 않습니다개발자도구에서 나타나는 오류입니Incorrect use of <label for=FORM_ELEMENT>The label's for attribute doesn't match any element id. This might prevent the browser from correctly autofilling the form and accessibility tools from working correctly.To fix this issue, make sure the label's for attribute references the correct id of a form field. PostCard가 작성되지 않는데 코드에서 오류를 못 찾겠어서 질문합니다! 의심스러운 부분들만 올려봅니다..post.js 코드입니다const ADD_POST = 'ADD_POST'; export const addPost = { type: ADD_POST } const reducer = (state = initialState, action) => { switch (action.type) { case ADD_POST: { return { ...state, mainPosts: [dummyPost, ...state.mainPosts], postAdded: true, }; }; default: { return { ...state, }; }; } }; PostForm.js 코드입니다const PostForm = () => { const { imagePaths } = useSelector((state) => state.post); const dispatch = useDispatch(); const imageInput = useRef(); const [text, setText] = useState(''); const onChangeText = useCallback((e) => { setText(e.target.value); }, []); const onSubmit = useCallback(() => { dispatch(addPost); setText('') }, []); return ( <Form style={{ margin: '10px 0 20px' }} encType="multipart/form-data" onFinish={onSubmit}> <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 scr={v} style={{ width: '200px' }} alt={v} /> <div> <Button>제거</Button> </div> </div> ))} </div> </Form> ) }; pages/index.js 코드입니다const Home = () => { const { isLoggedIn } = useSelector((state) => state.user); const { mainPosts } = useSelector((state) => state.post); return ( <AppLayout> {isLoggedIn && <PostForm />} {mainPosts.map((post) => <PostCard key={post.id} post={post} />)} </AppLayout> );
- 해결됨[리뉴얼] React로 NodeBird SNS 만들기
로그아웃시 req logout의 콜백함수 에러가 납니다...
req.logout을 호출할때 콜백함수가 필요하다 라고 나옵니다.req#logout requires a callback function이런식으로요 제 코드는 제로초님 코드처럼 적어놨는데 제가 이해하는 콜백함수라는게 라우터 부분이라고 생각이 드는데 어떻게 수정을 해야할지 감이 안잡힙니다 ㅜㅜ const express = require('express'); const bcrypt = require('bcrypt'); const passport = require('passport'); const { isLoggedIn, isNotLoggedIn } = require('./middlwares') const { User, Post } = require('../models'); const router = express.Router(); router.post("/login", isNotLoggedIn, (req, res, next) => { passport.authenticate("local", (err, user, info) => { if (err) { // 서버에러 부분 console.error(err) return next(err) } if(info){// 클라이언트 에러 부분 return res.status(401).send(info.reason) } // 성공시 return req.login(user,async(loginErr)=>{ // 패스포트 에러날시 if(loginErr){ console.error(loginErr) return next(loginErr) } // 유저의 모든 정보 const fullUserWithoutPassword = await User.findOne({ where:{id:user.id}, // 비밀번호 제외 // 받고 싶은 정보만 적을경우 // attributes:["id","nickname","email"], attributes: { exclude:["password"] }, // 나머지 정보 include:[{ model:Post }, { model: User, as: "Followings", }, { model: User, as: "Followers", }] }) // post 팔로워 팔로잉 정보 비밀번호 제거 return res.status(200).json(fullUserWithoutPassword) }) })(req,res,next) }) router.post("/", isNotLoggedIn, async (req, res, next) => { try { const exUser = await User.findOne({// 중복이 됫는지아닌지. where:{ email:req.body.email, } }) if(exUser){ return res.status(403).send("이미 사용중인 아이디입니다.") } const hashedPassword = await bcrypt.hash(req.body.password, 12) await User.create({ email:req.body.email, nickname: req.body.nickname, password: hashedPassword, }) // res.setHeader("Access-Control-Allow-Origin","http://localhost:3000") res.status(201).send("회원가입이 완료되셨습니다.") // 201은 생성이 잘됫다. } catch (error) { console.error(error) next(error)// 에러가 난거를 브라우저에 알려준다. statsus:500 } }) router.post('/logout', (req, res) => { req.logout(); req.session.destroy(); res.send('ok'); }); module.exports = router
- 미해결[리뉴얼] React로 NodeBird SNS 만들기
post 추가 시 오류
안녕하세요 제로초님다름이 아니라 로그인 후 포스트를 추가할 때 에러가 발생하는데혼자 해결하려고 노력해봤지만 해결을 아직 하지 못하여 질문드립니다.reducers/post.jsimport shortId from "shortid"; import { produce } from "immer"; import faker from "faker"; export const initialValue = { mainPosts: [], //이미지업로드 할떄 이미지경로들이 여기 들어간다. imagePaths: [], hasMorePost: true, //true면 가져올 시도를 해라. (스크롤 했을 때) loadPostsLoading: false, loadPostsDone: false, loadPostsError: null, //게시글 추가가 완료되었을때 TRue addPostLoading: false, addPostDone: false, addPostError: null, removePostLoading: false, removePostDone: false, removePostError: null, addCommentLoading: false, addCommentDone: false, addCommentError: null, }; //가짜 데이터 export const generateDummyPost = (number) => Array(number) .fill() .map(() => ({ id: shortId.generate(), User: { id: shortId.generate(), nickname: faker.name.findName(), }, content: faker.lorem.paragraph(), //아무 문장, Images: [ { src: "https://image.utoimage.com/preview/cp872722/2022/12/202212008462_500.jpg", }, ], Comments: [ { User: { id: shortId.generate(), nickname: faker.name.findName(), }, content: faker.lorem.sentence(), }, ], })); 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 addPost = (data) => ({ type: ADD_POST_REQUEST, data, }); export const addComment = (data) => ({ type: ADD_COMMENT_REQUEST, data, }); //리듀서란 이전 상태를 액션을 통해 다음 상태로 만들어내는 함수(단, 불변성은 지키면서) //draft는 불변성 상관없이 바꾸면 immer가 알아서 불변성있게 만들어준다. const reducer = (state = initialValue, action) => 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.addPostLoading = false; draft.addPostDone = true; draft.mainPosts.unshift(action.data); draft.imagePaths = []; //unshift란 배열의 맨 앞에다가 추가하는 함수 break; case ADD_POST_FAILURE: draft.addPostLoading = false; draft.addPostError = action.error; break; //게시글 불러오기 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); //concat은 두개 이상의 배열을 합칠 때 사용 //action.data에는 더미데이터들이 들어있고 draft.mainPosts는 원래 데이터 draft.hasMorePost = draft.mainPosts.length < 50; //50개보다 적으면 불러와야함 break; case LOAD_POSTS_FAILURE: draft.loadPostsLoading = false; draft.loadPostsError = action.error; break; //댓글 추가 case ADD_COMMENT_REQUEST: draft.addCommentLoading = true; draft.addCommentDone = false; draft.addCommentError = null; break; case ADD_COMMENT_SUCCESS: //immer버전 (너무 간단함) 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; //immer를 안 쓴 부분 // const postIndex = state.mainPosts.findIndex( // (v) => v.id === action.data.postId // ); // const post = { ...state.mainPosts[postIndex] }; // post.Comments = [ // dummyComment(action.data.content), // ...post.Comments, // ]; //얕은 복사 // const mainPosts = [...state.mainPosts]; // mainPosts[postIndex] = post; //댓글 추가하는 부분 너무어려움.. // //불변성을 지키다 보니 가독성이 너무 안좋음 // return { // ...state, // mainPosts, // addCommentLoading: false, // addCommentDone: true, // }; case ADD_COMMENT_FAILURE: draft.addCommentLoading = false; draft.addCommentError = action.error; break; //게시글 삭제 case REMOVE_POST_REQUEST: draft.removePostDone = false; draft.removePostLoading = true; draft.removePostError = null; break; case REMOVE_POST_SUCCESS: draft.removePostLoading = false; draft.removePostDone = true; draft.mainPosts = draft.mainPosts.filter( (v) => v.id !== action.data ); break; case REMOVE_POST_FAILURE: draft.removePostLoading = false; draft.removePostError = action.error; break; default: break; } }); export default reducer;여기서 Images안에 아무것도 들어있지 않아서 나타나는 오류같은데 잘 모르겠습니다.
- 해결됨[리뉴얼] React로 NodeBird SNS 만들기
<StopOutLined> 이 exported 되지 않는 오류
import { List, Button, Card } from 'antd';import { StopOutLined } from '@ant-design/icons';로 임포트를 했는데도 불구하고 프로필창을 열면 다음과 같은 에러가 납니다터미널에는 에러메시지도 뜨고요error - ./components/FollowList.jsAttempted import error: 'StopOutLined' is not exported from '@ant-design/icons'. npm i antd antd 와 npm install @ant-design/icons --save-dev로 다시 설치해봤는데 해결되지 않았습니다 package.json 에서 확인한 버전입니다 "antd": "^5.10.2", "@ant-design/icons": "^5.2.6", FollowList.js 에서 의심스러운 부분입니다import { List, Button, Card } from 'antd'; import { StopOutLined } from '@ant-design/icons'; const FollowList = ({header, data}) => { return ( <List renderItem={(item) => ( <List.Item style={{ marginTop: 20 }}> <Card actions={[<StopOutLined key="stop" />]}> <Card.Meta description={item.nickname} /> </Card> </List.Item> )} /> ) };
- 해결됨[리뉴얼] React로 NodeBird SNS 만들기
포스트 트윗 시 게시글이 추가되지 않고, 요청 무한로딩에 걸립니다. TypeError: Cannot read properties of undefined (reading 'data')
안녕하세요 제로초님현재 섹션3 redux-saga 연동하기 - 게시글, 댓글 saga 작성하기 영상의 Warning: Encountered two children with the same key, 2. 에러 해결까지 수강한 학생입니다!로그인 후 포스트 폼에 글을 입력 후 트윗하기 버튼을 누르는 순간 콘솔 창에 에러가 발생했습니다.redux-devtools로 포스트 추가 액션의 변화를 살펴보니ADD_POST_SUCCESS 포스트 추가 성공 액션이 실행되고 있지 않았습니다! 콘솔에 있는 에러 메시지 번역은 다음과 같았습니다.번역을 읽고 data가 undefined이며, addPost에 이상이 있음을 확인했고,컴포넌트 폴더의 PostForm.js와 reducers/post.js, sagas/post.js를 코드를 확인했습니다.https://github.com/ZeroCho/react-nodebird/blob/master/ch4/front/components/PostForm.js제로초님의 깃허브 챕터4와 구글 검색을 참고 후 가장 의심되는 코드 부분을 올립니다!※ 관련 없는 코드는 . . . 으로 요약 표시하였습니다. components/PostForm.js가독성을 위해 setText는 setPostText로, text는 postText로 바꾸어 코딩했습니다.// reducer 포스트 추가 요청 액션 불러오기 import { addPost } from '../reducers/post'; // 포스트 폼 컴포넌트(사용자 정의 태그) const PostForm = () => { const dispatch = useDispatch(); const [postText, onChangePostText, setPostText] = useInput(''); const { imagePaths, addPostLoading, addPostDone } = useSelector((state) => state.post); /* ----- 포스트 추가 완료 시 포스트 폼 글자 지우기 ----- */ useEffect(() => { if (addPostDone) { setPostText(''); } }, [addPostDone]); /* ----- 포스트 폼 제출 시 포스트 카드 추가 ----- */ const onSubmitForm = useCallback(() => { dispatch(addPost(postText)); }, [postText]); // const onSubmitForm = useCallback(() => { // dispatch({ // type: ADD_POST_REQUEST, // data: addPost, // }); // }, [postText]); return ( <Form . . . /> <Input.TextArea id="post-form" value={postText} onChange={onChangePostText} maxLength={140} placeholder="어떤 신기한 일이 있었나요?" /> . . . {/* ---------- 포스트 작성 버튼 ---------- */} <Button type="primary" style={{ float: 'right' }} htmlType="submit" loading={addPostLoading} > 트윗하기 </Button> </div> </Form> ); }; reducers/post.js// ShortId 라이브러리 불러오기 import shortId from 'shortid'; // 중앙 데이터 저장소(기본 state) export const initialState = { /* ---------- 메인 포스트 더미 데이터 ---------- */ mainPosts: [{ . . . }], /* ---------- 이미지 업로드 시 경로 저장 ---------- */ imagePaths: [], /* ---------- 포스트 추가 시도 중, 완료, 에러 ---------- */ addPostLoading: false, addPostDone: false, addPostError: 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'; // 포스트 추가 요청 액션 생성함수(action creator) export const addPost = (data) => ({ type: ADD_POST_REQUEST, data, }); // 포스트 더미 데이터 const dummyPost = (data) => ({ id: shortId.generate(), content: data, User: { id: 1, nickname: '다랑', }, Images: [], Comments: [], }); // 리듀서(reducer) : (이전 상태, 액션) => 다음 상태 const reducer = (state = initialState, action) => { switch (action.type) { /* ----- 포스트 추가 요청 리듀서 ----- */ case ADD_POST_REQUEST: return { addPostLoading: true, addPostDone: false, addPostError: null, }; /* ----- 포스트 추가 성공 리듀서 ----- */ case ADD_POST_SUCCESS: return { ...state, mainPosts: [dummyPost(action.data), ...state.mainPosts], addPostLoading: false, addPostDone: true, // 사용자 더미 데이터 me: dummyUser(action.data), }; /* ----- 포스트 추가 실패 리듀서 ----- */ case ADD_POST_FAILURE: return { addPostLoading: false, addPostError: action.error, }; default: return state; } }; sagas/post.js// Saga 이펙트 불러오기 import { all, fork, call, takeLatest, put, delay } from 'redux-saga/effects'; // Axios 라이브러리 불러오기 import axios from 'axios'; // reducer 포스트 추가, 답글 추가 액션 불러오기 import { ADD_POST_REQUEST, ADD_POST_SUCCESS, ADD_POST_FAILURE, } from '../reducers/post'; // addPost 실행 시 서버에 addPostAPI 요청 function addPostAPI(data) { return axios.post('/api/post', data) } // ADD_POST_REQUEST 액션이 실행되면 addPost 함수 실행 function* addPost(action) { try { // const result = yield call(addPostAPI, action.data); /* ----- 요청 성공 시 ADD_POST_SUCCESS 액션 디스패치 ----- */ yield delay(1000); yield put({ type: ADD_POST_SUCCESS, data: action.data, // 성공 결과 }); } catch (err) { /* ----- 요청 실패 시 ADD_POST_FAILURE 액션 디스패치 ----- */ yield put({ type: ADD_POST_FAILURE, error: err.response.data, // 실패 결과 }); } } 강의 잘 보고 있습니다!항상 정성스러운 답변 남겨주셔서 감사합니다 제로초님!