강의

멘토링

커뮤니티

인프런 커뮤니티 질문&답변

uphoon님의 프로필 이미지
uphoon

작성한 질문수

[리뉴얼] React로 NodeBird SNS 만들기

다이나믹 라우터 애러 도와주시면 감사하겠습니다.

해결된 질문

작성

·

286

0

 1 of 1 unhandled error

Server Error

TypeError: Cannot read properties of null (reading 'Likers')

This error happened while generating the page. Any console logs will be displayed in the terminal window.

Source

components\PostCard.js (18:23) @ PostCard


  16 |     const dispatch = useDispatch();
  17 |     const id = useSelector((state) => state.user.me?.id)
> 18 |     const liked = post.Likers.find((item) => item.id === id)
     |                       ^
  19 | 
  20 |     const onToggleComment = useCallback(() => {
  21 |         setCommentFormOpened((prev) => !prev)
Call Stack
Function.getInitialProps

pages\_document.tsx (91:33)

Show collapsed frames

선생님도 같은 오류 나왔었던거 있어서 그대로 오류 해결하는데도 이 오류가 안없어지네요...

//서버 메세지

GET /posts?lastId=0 200 12.677 ms - 2197
GET /post/60 200 15.146 ms - 216

서버에서는 요청 잘 받는것 같은데 Likers가 계속 비어있는거 같습니다...

 

메인 화면에서는 작동이제 다 잘되는데 다이나믹 라우트 post/ 로 넘어가기만 하면 오류 발생하네요... 하루종일 매달리고 있는데... 선생님 깃허브 코드랑 강의 코드가 달라서 조금 헷갈려서요... 깃허브 주소 남기겠습니다 한번 봐주시면 감사하겠습니다. ㅠㅠ

https://github.com/wihyanghoon/react-nodebird

// [id].js

import React from 'react';
import { useSelector } from 'react-redux';
import { useRouter } from 'next/router';
import { END } from 'redux-saga';

import axios from 'axios';
import { LOAD_POST_REQUEST } from '../../reducers/post';
import wrapper from '../../store/configureStore';
import PostCard from '../../components/PostCard';
import AppLayout from '../../components/AppLayout';
import { LOAD_MYINFO_REQUEST } from '../../reducers/user';

const Post = () => {
    const { singlePost } = useSelector((state) => state.post);
    const router = useRouter();
    const { id } = router.query;

    // if (router.isFallback) {
    //   return <div>Loading...</div>
    // }

    return (
        <AppLayout>
            <PostCard post={ singlePost } />
        </AppLayout>
    );
};

export const getServerSideProps = wrapper.getServerSideProps(async (context) => {
    const cookie = context.req ? context.req.headers.cookie : '';
    console.log(context);
    axios.defaults.headers.Cookie = '';
    if (context.req && cookie) {
        axios.defaults.headers.Cookie = cookie;
    }
    context.store.dispatch({
        type: LOAD_MYINFO_REQUEST,
    });
    context.store.dispatch({
        type: LOAD_POST_REQUEST,
        data: context.params.id,
    });
    context.store.dispatch(END);
    await context.store.sagaTask.toPromise();
});

export default Post;
import { delay, all, fork, takeLatest, put, throttle, call } from "redux-saga/effects";
import {
    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,
    LOAD_POSTS_REQUEST, LOAD_POSTS_SUCCESS, LOAD_POSTS_FAILURE,
    LIKE_POST_REQUEST, LIKE_POST_SUCCESS, LIKE_POST_FAILURE,
    UNLIKE_POST_REQUEST, UNLIKE_POST_SUCCESS, UNLIKE_POST_FAILURE,
    UPLOAD_IMAGES_REQUEST, UPLOAD_IMAGES_SUCCESS, UPLOAD_IMAGES_FAILURE,
    RETWEET_REQUEST,RETWEET_SUCCESS,RETWEET_FAILURE,
    LOAD_POST_REQUEST, LOAD_POST_SUCCESS, LOAD_POST_FAILURE
} from '../reducers/post'

import { ADD_POST_TO_ME, REMOVE_POST_TO_ME } from "../reducers/user";
import axios from "axios";
import shortId from 'shortid';

function addPostAPI(data) {
    return axios.post('/post', data);
}

function* addPost(action) {
    try {
        const result = yield call(addPostAPI, action.data);
        yield console.log(result)
        yield put({
            type: ADD_POST_SUCCESS,
            data: result.data,
        });
        yield put({
            type: ADD_POST_TO_ME,
            data: result.data.id,
        });
    } catch (err) {
        console.error(err);
        yield put({
            type: ADD_POST_FAILURE,
            error: err.response.data,
        });
    }
}

function removePostAPI(data) {
    return axios.delete(`/post/${data}`)
}

function* removePost(action) {
    console.log(action.data)
    try {
        const result = yield call(removePostAPI, action.data)
        yield console.log(typeof result.data.PostId)
        yield put({
            type: REMOVE_POST_SUCCESS,
            data: result.data
        })
        yield put({
            type: REMOVE_POST_TO_ME,
            data: result.data.PostId
        })
    } catch (err) {
        yield put({
            type: REMOVE_POST_FAILURE,
            error: err.response.data
        });
    }
}

function addCommentAPI(data) {
    return axios.post(`/post/${data.postId}/comment`, data)
}

function* addComment(action) {
    try {
        const result = yield call(addCommentAPI, action.data)
        yield put({
            type: ADD_COMMENT_SUCCESS,
            data: result.data
        });
    } catch (err) {
        yield put({
            type: ADD_COMMENT_FAILURE,
            error: err.response.data
        });
    }
}

function loadPostsAPI(lastId) {
    return axios.get(`/posts?lastId=${lastId || 0}`);
}

function* loadPosts(action) {
    try {
        const result = yield call(loadPostsAPI, action.lastId);

        yield put({
            type: LOAD_POSTS_SUCCESS,
            data: result.data,
        });
    } catch (err) {
        console.error(err);
        yield put({
            type: LOAD_POSTS_FAILURE,
            data: err.response.data,
        });
    }
}

function loadPostAPI(data) {
    return axios.get(`/post/${data}`);
}

function* loadPost(action) {
    try {
        const result = yield call(loadPostAPI, action.data);

        yield put({
            type: LOAD_POST_SUCCESS,
            data: result.data,
        });
    } catch (err) {
        console.error(err);
        yield put({
            type: LOAD_POST_FAILURE,
            error: err.response.data,
        });
    }
}

function likePostAPI(data) {
    return axios.patch(`/post/${data}/like `, data);
}

function* likePost(action) {
    try {
        const result = yield call(likePostAPI, action.data);
        yield put({
            type: LIKE_POST_SUCCESS,
            data: result.data,
        });
    } catch (err) {
        console.error(err);
        yield put({
            type: LIKE_POST_FAILURE,
            data: err.response.data,
        });
    }
}

function UnLikePostAPI(data) {
    return axios.delete(`/post/${data}/like`);
}

function* UnLikePost(action) {
    try {
        const result = yield call(UnLikePostAPI, action.data);
        yield put({
            type: UNLIKE_POST_SUCCESS,
            data: result.data,
        });
    } catch (err) {
        console.error(err);
        yield put({
            type: UNLIKE_POST_FAILURE,
            data: err.response.data,
        });
    }
}

function upLoadImagesAPI(data) {
    return axios.post('/post/images', data);
}

function* upLoadImages(action) {
    try {
        const result = yield call(upLoadImagesAPI, action.data);
        yield put({
            type: UPLOAD_IMAGES_SUCCESS,
            data: result.data,
        });
    } catch (err) {
        console.error(err);
        yield put({
            type: UPLOAD_IMAGES_FAILURE,
            data: err.response.data,
        });
    }
}

function retweetApi(data){
    return axios.post(`/post/${data}/retweet`);
}

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,
            err: err.response.data,
        });
    }
}

function* watchLoadPosts() {
    yield throttle(5000, LOAD_POSTS_REQUEST, loadPosts);
}

function* watchLoadPost() {
    yield takeLatest(LOAD_POST_REQUEST, loadPost);
}

function* watchAddPost() {
    yield takeLatest(ADD_POST_REQUEST, addPost)
}

function* watchRemovePost() {
    yield takeLatest(REMOVE_POST_REQUEST, removePost)
}

function* watchCommentPost() {
    yield takeLatest(ADD_COMMENT_REQUEST, addComment)
}

function* watchLikePost() {
    yield takeLatest(LIKE_POST_REQUEST, likePost)
}

function* watchUnLiketPost() {
    yield takeLatest(UNLIKE_POST_REQUEST, UnLikePost)
}

function* watchUpLoadImages() {
    yield takeLatest(UPLOAD_IMAGES_REQUEST, upLoadImages)
}

function* watchRetweet() {
    yield takeLatest(RETWEET_REQUEST, retweet)
}


export default function* postSaga() {
    yield all([
        fork(watchAddPost),
        fork(watchCommentPost),
        fork(watchRemovePost),
        fork(watchLoadPosts),
        fork(watchLoadPost),
        fork(watchLikePost),
        fork(watchUnLiketPost),
        fork(watchUpLoadImages),
        fork(watchRetweet),
    ]);
}
import shortId from 'shortid';
import produce from 'immer';
import faker from 'faker';
import { LIKE_FAILURE, LIKE_REQUEST, LIKE_SUCCESS } from './user';

export const initialState = {
    mainPosts: [],
    imagePath: [],
    hasMorePosts: true,
    loadPostsLoading: false,
    loadPostsDone: false,
    loadPostsError: null,
    loadPostLoading: false,
    loadPostDone: false,
    loadPostError: null,
    likeLoading: false,
    likeDone: false,
    likeError: null,
    unLikeLoading: false,
    unLikeDone: false,
    unLikeError: null,
    addPostLoadding: false,
    addPostDone: false,
    addPostErr: null,
    removePostLoadding: false,
    removePostDone: false,
    removePostErr: null,
    addCommentLoadding: false,
    addCommentDone: false,
    addCommentErr: null,
    upLoadImagesLoadding: false,
    upLoadImagesDone: false,
    upLoadImagesErr: null,
    retweetLoadding: false,
    retweetDone: false,
    retweetErr: null,
    singlePost: null,
}


// export const getDemmuyPost = (number) => Array(number).fill().map(() => ({
//     id: shortId.generate(),
//     User: {
//         id: shortId.generate(),
//         nickname: faker.name.findName(),
//     },
//     content: faker.lorem.paragraph(),
//     Images: [{
//         src: faker.image.image(),
//     }],
//     Comments: [{
//         User: {
//             id: shortId.generate(),
//             nickname: faker.name.findName(),
//         },
//         content: faker.lorem.sentence(),
//     }],
// }))
export const REMOVE_IMAGES_SUCSESS = 'REMOVE_IMAGES_SUCSESS';

export const LOAD_POST_REQUEST = 'LOAD_POSTS_REQUEST';
export const LOAD_POST_SUCCESS = 'LOAD_POSTS_SUCCESS';
export const LOAD_POST_FAILURE = 'LOAD_POSTS_FAILURE';

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 LIKE_POST_REQUEST = 'LIKE_POST_REQUEST';
export const LIKE_POST_SUCCESS = 'LIKE_POST_SUCCESS';
export const LIKE_POST_FAILURE = 'LIKE_POST_FAILURE';

export const UPLOAD_IMAGES_REQUEST = 'UPLOAD_IMAGES_REQUEST';
export const UPLOAD_IMAGES_SUCCESS = 'UPLOAD_IMAGES_SUCCESS';
export const UPLOAD_IMAGES_FAILURE = 'UPLOAD_IMAGES_FAILURE';

export const UNLIKE_POST_REQUEST = 'UNLIKE_POST_REQUEST';
export const UNLIKE_POST_SUCCESS = 'UNLIKE_POST_SUCCESS';
export const UNLIKE_POST_FAILURE = 'UNLIKE_POST_FAILURE';

export const RETWEET_REQUEST = 'RETWEET_REQUEST'
export const RETWEET_SUCCESS = 'RETWEET_SUCCESS'
export const RETWEET_FAILURE = 'RETWEET_FAILURE'

export const addPostAction = (data) => {
    return {
        type: ADD_POST_REQUEST,
        data
    }
}

export const addCommentAction = (data) => {
    return {
        type: ADD_COMMENT_REQUEST,
        data
    }
}

const reducer = (state = initialState, action) => {
    return produce(state, (draft) => {
        switch (action.type) {
            case RETWEET_REQUEST:
                draft.retweetLoadding = true
                draft.retweetDone = false
                draft.retweetErr = null
                break;

            case RETWEET_SUCCESS:
                draft.retweetLoadding = false
                draft.retweetDone = true
                draft.mainPosts.unshift(action.data)
                break;

            case RETWEET_FAILURE:
                draft.retweetLoadding = false
                draft.retweetErr = action.err
                break;

            case REMOVE_IMAGES_SUCSESS:
                console.log(action.data)
                draft.imagePath = draft.imagePath.filter((item, index) => index !== action.data)
                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 = draft.mainPosts.concat(action.data);
                draft.hasMorePosts = draft.mainPosts.length === 10;
                break;

            case LOAD_POSTS_FAILURE:
                draft.loadPostsLoading = false;
                draft.loadPostsError = action.error;
                break;

            case LOAD_POST_REQUEST:
                draft.loadPostLoading = true;
                draft.loadPostDone = false;
                draft.loadPostError = null;
                break;

            case LOAD_POST_SUCCESS:
                draft.loadPostLoading = false;
                draft.loadPostDone = true;
                draft.singlePost = action.data;
                break;

            case LOAD_POST_FAILURE:
                draft.loadPostLoading = false;
                draft.loadPostError = action.error;
                break;

            case ADD_POST_REQUEST:
                draft.addPostLoadding = true
                draft.addPostDone = false
                draft.addPostErr = null
                break;

            case ADD_POST_SUCCESS:
                draft.addPostLoadding = false
                draft.addPostDone = true
                draft.mainPosts.unshift(action.data)
                draft.imagePath = []
                break;

            case ADD_POST_FAILURE:
                draft.addPostLoadding = false
                draft.addPostErr = action.err
                break;

            case REMOVE_POST_REQUEST:
                draft.removePostLoadding = true
                draft.removePostDone = false
                draft.removePostErr = null
                break;

            case REMOVE_POST_SUCCESS:
                draft.removePostLoadding = false
                draft.removePostDone = true
                draft.mainPosts = state.mainPosts.filter((item) => item.id !== action.data.PostId)
                break;

            case REMOVE_POST_FAILURE:
                draft.removePostLoadding = false
                draft.removePostErr = action.err
                break;

            case ADD_COMMENT_REQUEST:
                draft.addCommentLoadding = true
                draft.addCommentDone = false
                draft.addCommentErr = null
                break;

            case ADD_COMMENT_SUCCESS:
                const post = draft.mainPosts.find((item) => { return item.id === action.data.PostId })
                post.Comments.unshift(action.data)
                draft.addCommentLoadding = false
                draft.addCommentDone = true
                break;

            case ADD_COMMENT_FAILURE:
                draft.addCommentLoadding = false
                draft.addCommentErr = action.error
                break;

            case LIKE_POST_REQUEST:
                draft.likeLoading = true
                draft.likeDone = false
                draft.likeError = null
                break;

            case LIKE_POST_SUCCESS: {
                draft.likeLoading = false
                draft.likeDone = true
                const post = draft.mainPosts.find((item) => item.id === action.data.PostId)
                post.Likers.push({ id: action.data.UserId })
                break;
            }
            case LIKE_POST_FAILURE:
                draft.unLikeLoading = false
                draft.unLikeError = true
                break;

            case UNLIKE_POST_REQUEST:
                draft.unLikeLoading = true
                draft.unLikeDone = false
                draft.unLikeError = null
                break;

            case UNLIKE_POST_SUCCESS: {
                draft.unLikeLoading = false
                draft.unLikeDone = true
                const post = draft.mainPosts.find((v) => v.id === action.data.PostId);
                post.Likers = post.Likers.filter((v) => v.id !== action.data.UserId);
                break;
            }
            case UNLIKE_POST_FAILURE:
                draft.unLikeLoading = false
                draft.unLikeDone = true
                break;

            case UPLOAD_IMAGES_REQUEST:
                draft.upLoadImagesLoadding = true
                draft.upLoadImagesDone = false
                draft.upLoadImagesErr = null
                break;

            case UPLOAD_IMAGES_SUCCESS:
                draft.upLoadImagesLoadding = true
                draft.upLoadImagesDone = false
                draft.imagePath = action.data
                break;

            case UPLOAD_IMAGES_FAILURE:
                draft.upLoadImagesLoadding = false
                draft.upLoadImagesErr = action.error
                break;
            default:
                return state
        }
    })
}

export default reducer
const express = require('express')
const multer = require('multer')
const path = require('path')
const fs = require('fs')
const { Post, Comment, Image, User, Hashtag } = require('../models')
const { isLoggedIn } = require('./middlewares')
const user = require('../models/user')

const router = express.Router();

try {
  fs.accessSync('uploads')
} catch (error) {
  console.log('폴더가 없으므로 생성합니다.')
  fs.mkdirSync('uploads')
}

const upload = multer({
  storage: multer.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 /post
  try {
    const hashtags = req.body.content.match(/#[^\s#]+/g);
    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]]
      await post.addHashtags(result.map((v) => v[0]));
    }
    if (req.body.image) {
      if (Array.isArray(req.body.image)) { // 이미지를 여러 개 올리면 image: [제로초.png, 부기초.png]
        const images = await Promise.all(req.body.image.map((image) => Image.create({ src: image })));
        await post.addImages(images);
      } else { // 이미지를 하나만 올리면 image: 제로초.png
        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.delete('/:PostId', isLoggedIn, async (req, res, next) => {
  try {
    await Post.destroy({
      where: {
        id: req.params.PostId,
        UserId: req.user.id
      },
    })
    res.status(201).json({ PostId: parseInt(req.params.PostId, 10) })
  } catch (error) {
    console.error(error)
    next(error)
  }
})

router.post('/:postId/comment', isLoggedIn, async (req, res) => {
  try {
    const post = await Post.findOne({
      where: { id: req.params.postId }
    })
    if (!post) {
      return res.status(403).send('존재하지 않는 게시글 입니다.')
    }
    const commnet = await Comment.create({
      content: req.body.content,
      PostId: Number(req.params.postId),
      UserId: req.user.id,
    })
    const fullComment = await Comment.findOne({
      where: { id: commnet.id },
      include: [{
        model: User,
        attributes: ['id', 'nickname']
      }]
    })
    res.status(201).json(fullComment)
  } catch (error) {
    console.error(error)
    next(error)
  }
})

router.get('/:postId', async (req, res, next) => {
  try {
    const post = await Post.findOne({
      where: { id: req.params.postId },
    })
    if (!post) {
      return res.status(404).send('존재하지않는 게시글입니다..')
    }
    const fullPost = await Post.findOne({
      where: { id: post.id },
      include: [{
        model: Post,
        as: 'Retweet',
        include: [{
          model: User,
          attributes: ['id', 'nickname']
        }, {
          model: Image,
        }]
      }, {
        model: User,
        attributes: ['id', 'nickname']
      }, {
        model: User,
        as: 'Likers',
        attributes: ['id', 'nickname']
      }, {
        model: Image,
      }, {
        model: Comment,
        include: [{
          model: User,
          attributes: ['id', 'nickname']
        }]
      }]
    })
    res.status(200).json(fullPost)
  } catch (err) {
    console.error(err)
    next(err)
  }
});


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/like', (req, res, next) => {

})

router.post('/images', isLoggedIn, upload.array('image'), (req, res, next) => {
  res.json(req.files.map((item) => item.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: User, // 좋아요 누른 사람
        as: 'Likers',
        attributes: ['id'],
      }, {
        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;

답변 1

0

제로초(조현영)님의 프로필 이미지
제로초(조현영)
지식공유자

에러메시지를 잘 읽으셔야 합니다. Likers가 비어있는게 아닙니다. post가 null인 겁니다. post가 null이라는건 서버사이드렌더링이 안 된겁니다. 리덕스에서 hydrate 액션 시 post가 null인지 확인해보시면 되겠죠?

uphoon님의 프로필 이미지
uphoon
질문자

네 선생님 hydrate 확인하고 리덕스 본건데

image

index.js 즉 localhost:3000 에서는 잘나오고 이제

import React from 'react';
import { useSelector } from 'react-redux';
import { useRouter } from 'next/router';
import { END } from 'redux-saga';

import axios from 'axios';
import { LOAD_POST_REQUEST, LOAD_POSTS_REQUEST } from '../../reducers/post';
import wrapper from '../../store/configureStore';
import PostCard from '../../components/PostCard';
import AppLayout from '../../components/AppLayout';
import { LOAD_MYINFO_REQUEST } from '../../reducers/user';

const Post = () => {
    const { singlePost } = useSelector((state) => state.post);
    const router = useRouter();
    const { id } = router.query;

    // if (router.isFallback) {
    //   return <div>Loading...</div>
    // }

    return (
        <AppLayout>
            <PostCard post={singlePost} />
        </AppLayout>
    );
};

export const getServerSideProps = wrapper.getServerSideProps(async (context) => {
    const cookie = context.req ? context.req.headers.cookie : "";
    axios.defaults.headers.Cookie = ""
    if (context.req && cookie) {
        axios.defaults.headers.Cookie = cookie
    }
    context.store.dispatch({
        type: LOAD_MYINFO_REQUEST,
    });
    context.store.dispatch({
        type: LOAD_POST_REQUEST,
    });
    context.store.dispatch(END);
    await context.store.sagaTask.toPromise();
})

export default Post;

여기가 다이나믹 라우터 쪽 로직인데 제가 밑에 ServerSideProps를 잘 못 만든 걸까요?

제로초(조현영)님의 프로필 이미지
제로초(조현영)
지식공유자

아뇨 mainPosts가 아니라 post를 보셔야죠. state.post니까요

uphoon님의 프로필 이미지
uphoon
질문자

네 선생님 그러니까 지금

imageinit 일때

imagehydrate 일때 state.post안에 값들 다들어오는데요? 제가 질문을 잘못이해 했을까요?

제로초(조현영)님의 프로필 이미지
제로초(조현영)
지식공유자

post 페이지서 렌더링되는 주체는 mainPosts가 아니라 singlePost입니다. singlePost가 null이네요

uphoon님의 프로필 이미지
uphoon
질문자

어 선생님 제가 그래서 위에 말씀드린게 인덱스에서는 나오고 post페이지에서는 아예 리덕스 저거 추적조차 못해서 계속 말씀드리는거여서...

제로초(조현영)님의 프로필 이미지
제로초(조현영)
지식공유자

loadPost saga를 잘 봐보시고 dispatch부분도 잘 봐보세요. 틀린 부분이있습니다.

uphoon님의 프로필 이미지
uphoon
질문자

네 찾아보겠습니다!

uphoon님의 프로필 이미지
uphoon
질문자

선생님 제가 틀린게 있나요? loadPost saga 및 디스패치 계속 확인해도 ....

export const getServerSideProps = wrapper.getServerSideProps(async (context) => {
    const cookie = context.req ? context.req.headers.cookie : '';
    console.log(context);
    axios.defaults.headers.Cookie = '';
    if (context.req && cookie) {
        axios.defaults.headers.Cookie = cookie;
    }
    context.store.dispatch({
        type: LOAD_MYINFO_REQUEST,
    });
    context.store.dispatch({
        type: LOAD_POST_REQUEST,
        data: context.params.id,
    });
    context.store.dispatch(END);
    await context.store.sagaTask.toPromise();
    return { props: {} };
});
function loadPostAPI(data) {
    return axios.get(`/post/${data}`);
}

function* loadPost(action) {
    try {
        const result = yield call(loadPostAPI, action.data);
        yield put({
            type: LOAD_POST_SUCCESS,
            data: result.data,
        });
    } catch (err) {
        console.error(err);
        yield put({
            type: LOAD_POST_FAILURE,
            error: err.response.data,
        });
    }
}
function* watchLoadPost() {
    yield takeLatest(LOAD_POST_REQUEST, loadPost);
}
export default function* postSaga() {
    yield all([
        fork(watchAddPost),
        fork(watchCommentPost),
        fork(watchRemovePost),
        fork(watchLoadPosts),
        fork(watchLoadPost),
        fork(watchLikePost),
        fork(watchUnLiketPost),
        fork(watchUpLoadImages),
        fork(watchRetweet),
    ]);
}
제로초(조현영)님의 프로필 이미지
제로초(조현영)
지식공유자

context.params.id를 추가하신 건 맞고요. 네트워크탭이랑 서버쪽 콘솔에서 /post/아이디 요청이 제대로 가는지 봐야합니다.

uphoon님의 프로필 이미지
uphoon
질문자

//서버 메세지

GET /posts?lastId=0 200 12.677 ms - 2197
GET /post/60 200 15.146 ms - 216

서버쪽에서 응답은 잘받는것같습니다 ㅠㅠ 그냥 밀고 다시 해야할까요…

제로초(조현영)님의 프로필 이미지
제로초(조현영)
지식공유자

음.. 프론트서버쪽에서 콘솔에 리덕스 스테이트들이 ssr되는 과정이 나올텐데요. 1 2 3 4모두 singlePost가 null인가요? *loadPost에서도 console.log(result)도 해보세요.

uphoon님의 프로필 이미지
uphoon
질문자

선생님 오류 찾았습니다...

export const LOAD_POST_REQUEST = 'LOAD_POSTS_REQUEST';
export const LOAD_POST_SUCCESS = 'LOAD_POSTS_SUCCESS';
export const LOAD_POST_FAILURE = 'LOAD_POSTS_FAILURE';

export const LOAD_POSTS_REQUEST = 'LOAD_POSTS_REQUEST';
export const LOAD_POSTS_SUCCESS = 'LOAD_POSTS_SUCCESS';
export const LOAD_POSTS_FAILURE = 'LOAD_POSTS_FAILURE';

action 함수가 함수이름만 다르고... string값은 같아서 오류났던거였습니다..... 드디어 해결했습니다 ㅠㅠ

uphoon님의 프로필 이미지
uphoon

작성한 질문수

질문하기