React

 

 

 

 Next.js 서버사이드렌더링

 

버전이 다르기 때문에 소스가 강좌와 다를 수 있다.

버전

next:  13.0.4

antd:  5.0.1

 

소스 : https://github.dev/braverokmc79/node-bird-sns

 

제로초 소스 : https://github.com/ZeroCho/react-nodebird

 

 

 

 

63.  서버사이드렌더링 준비하기

 

강의 :

https://www.inflearn.com/course/%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/unit/48852?tab=curriculum


 

 

리듀서 설정 오류 변 경

reducer/index.js

import { HYDRATE } from 'next-redux-wrapper';
import { combineReducers } from 'redux';

import user from './user';
import post from './post';


//(이전상태,액션)  => 다음 상태
const rootReducer = (state, action) => {
    switch (action.type) {
        case HYDRATE: {
            console.log('HYDRATE', action);
            return action.payload;
        }

        default: {
            const combinedReducer = combineReducers({
                user,
                post
            });
            return combinedReducer(state, action);
        }
    }
}


export default rootReducer;


 

 

참조 :

Next.js에서 SSR(서버사이드 렌더링)적용하기

 

 

서버 사이드 랜더링 적용

적용 오류시   다음 참조 :

https://www.inflearn.com/questions/230014/typeerror-nextcallback-is-not-a-function-next-redux-wrapper-7-0

 

pages/index.js

import React, { useEffect, useCallback } from 'react';
import AppLayout from './../components/AppLayout';
import { useSelector, useDispatch } from 'react-redux';
import PostCard from './../components/PostCard';
import PostForm from './../components/PostForm';
import { LOAD_MY_INFO_REQUEST } from './../reducers/user';
import { LOAD_POSTS_REQUEST } from './../reducers/post';
import { END } from 'redux-saga';
import wrapper from '../store/configureStore';

const Home = () => {
    const dispatch = useDispatch();
    const { me } = useSelector((state) => state.user);
    const { mainPosts, hasMorePosts, loadPostsLoading, reTweetError, reTweetDone } = useSelector((state) => state.post);

    useEffect(() => {
        if (reTweetError) {
            return alert(reTweetError);
        }
    }, [reTweetError]);

    useEffect(() => {
        if (reTweetDone) {
            alert("리트윗 되었습니다.");
        }
    }, [reTweetDone]);

    useEffect(() => {
        function onScroll() {
            if (window.scrollY + document.documentElement.clientHeight > document.documentElement.scrollHeight - 300) {
                // console.log("hasMorePosts && !loadPostsLoading : ", hasMorePosts, loadPostsLoading);

                if (hasMorePosts && !loadPostsLoading) {
                    const lastId = mainPosts[mainPosts.length - 1]?.id;
                    // console.log(" lastId  : ", lastId);
                    dispatch({
                        type: LOAD_POSTS_REQUEST,
                        lastId
                    })
                }
            }
        }

        window.addEventListener('scroll', onScroll);
        //항상 반환처리시 이벤트를 제거해야지 메모리상에 낭비를 줄일 수 있다.
        return () => {
            window.removeEventListener('scroll', onScroll);
        }
    }, [hasMorePosts, loadPostsLoading, mainPosts]);



    return (
        <AppLayout>
            {me && <PostForm />}
            {mainPosts && mainPosts.map((post) => <PostCard key={post.id} post={post} />)}
        </AppLayout>
    );
};

// 서버사이드 렌더링 : 프론트 서버가 직접 요청하기 때문에 withCredentials문제 다시 발생하므로 브러우저 대신 cookie를 보내줘야함.
//home 실행되기전에 가장 먼저 실행 된다.
//초기 화면 랜더될때에는 리덕스에 데이터가 채워진체로 실행

export const getServerSideProps = wrapper.getServerSideProps((store) => async ({ req, res, ...etc ) => {

    console.log('getServerSideProps start');
    //console.log(context.req.headers);

    store.dispatch({
        type: LOAD_MY_INFO_REQUEST,
    });


    store.dispatch({
        type: LOAD_POSTS_REQUEST
    });

    //다음 코드는 nextjs  문서
    store.dispatch(END);
    console.log('getServerSideProps end');
    await store.sagaTask.toPromise();
})

export default Home;

 

콘솔로그가 웹 브라우저 창이 아닌  서버  터미널에  찍힌다.

 

 

 

 

 

 

 

 

 

 

 

 

 

64.  SSR시 쿠키 공유하기

강의:

https://www.inflearn.com/course/%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/unit/48853?tab=curriculum

 

 

 로그인이 풀리는 현상 은 쿠키값이 백엔드 서버에 전달되지 않아서 ,따라서  쿠키  전달.

그리고 공유 시 주의 사항

    const cookie = req ? req.headers.cookie : '';
    axios.defaults.headers.Cookie = ''; //*** 쿠키가 공유될수 있으므로 쿠키 초기화 필수
    if (req && cookie) { //서버일때와 쿠키가 존재할때만  실행
        axios.defaults.headers.Cookie = cookie;
    }

 

 

프론트엔드

pages/index.js

import React, { useEffect, useCallback } from 'react';
import AppLayout from './../components/AppLayout';
import { useSelector, useDispatch } from 'react-redux';
import PostCard from './../components/PostCard';
import PostForm from './../components/PostForm';
import { LOAD_MY_INFO_REQUEST } from './../reducers/user';
import { LOAD_POSTS_REQUEST } from './../reducers/post';
import { END } from 'redux-saga';
import wrapper from '../store/configureStore';
import axios from 'axios';


const Home = () => {
    const dispatch = useDispatch();
    const { me } = useSelector((state) => state.user);
    const { mainPosts, hasMorePosts, loadPostsLoading, reTweetError, reTweetDone } = useSelector((state) => state.post);

    useEffect(() => {
        if (reTweetError) {
            return alert(reTweetError);
        }
    }, [reTweetError]);

    useEffect(() => {
        if (reTweetDone) {
            alert("리트윗 되었습니다.");
        }
    }, [reTweetDone]);

    useEffect(() => {
        function onScroll() {
            if (window.scrollY + document.documentElement.clientHeight > document.documentElement.scrollHeight - 300) {
                // console.log("hasMorePosts && !loadPostsLoading : ", hasMorePosts, loadPostsLoading);

                if (hasMorePosts && !loadPostsLoading) {
                    const lastId = mainPosts[mainPosts.length - 1]?.id;
                    // console.log(" lastId  : ", lastId);
                    dispatch({
                        type: LOAD_POSTS_REQUEST,
                        lastId
                    })
                }
            }
        }

        window.addEventListener('scroll', onScroll);
        //항상 반환처리시 이벤트를 제거해야지 메모리상에 낭비를 줄일 수 있다.
        return () => {
            window.removeEventListener('scroll', onScroll);
        }
    }, [hasMorePosts, loadPostsLoading, mainPosts]);



    return (
        <AppLayout>
            {me && <PostForm />}
            {mainPosts && mainPosts.map((post) => <PostCard key={post.id} post={post} />)}
        </AppLayout>
    );
};

// 서버사이드 렌더링 : 프론트 서버가 직접 요청하기 때문에 withCredentials문제 다시 발생하므로 브러우저 대신 cookie를 보내줘야함.
//home 실행되기전에 가장 먼저 실행 된다.
//초기 화면 랜더될때에는 리덕스에 데이터가 채워진체로 실행
//다음 코드는 프론트 서버에서 실행
//도메인이 다르면 쿠키전달 안된다.
export const getServerSideProps = wrapper.getServerSideProps((store) => async ({ req, res, ...etc }) => {

    console.log('getServerSideProps start');

    //서버에 쿠키가 전달이 안된다. 따라서 새로고침시 로그인 풀리는 현상
    //따라서 다음과 같은 코드로 서버에 쿠키값을 보내는 처리를 한다.
    console.log(" store  :", store);

    const cookie = req ? req.headers.cookie : '';
    axios.defaults.headers.Cookie = ''; //*** 쿠키가 공유될수 있으므로 쿠키 초기화 필수
    if (req && cookie) { //서버일때와 쿠키가 존재할때만  실행
        axios.defaults.headers.Cookie = cookie;
    }

    console.log(" req cookie :", cookie);


    store.dispatch({
        type: LOAD_MY_INFO_REQUEST,
    });


    store.dispatch({
        type: LOAD_POSTS_REQUEST
    });

    //다음 코드는 nextjs  문서
    store.dispatch(END);
    console.log('getServerSideProps end');
    await store.sagaTask.toPromise();
})

export default Home;

 

 

백엔드

const express = require('express');
const { User, Post } = require('../models');
const bcrypt = require('bcrypt');
const passport = require('passport');
const router = express.Router();
const { isLoggedIn, isNotLoggedIn } = require('./middlewares');


//브라우저에서 새로고침 할때마다 요청처리 된다.
router.get('/', async (req, res, next) => {
    console.log("브라우저에서 새로고침 할때마다 요청처리 :", req.headers);

    try {
        if (req.user) {
            const fullUserWithoutPassword = await User.findOne({
                where: {
                    id: req.user.id
                },
                attributes: {
                    exclude: ['password']
                },
                include: [{
                    model: Post,
                    attributes: ['id'],
                }, {
                    model: User,
                    as: "Followers",
                    attributes: ['id'],
                },
                {
                    model: User,
                    as: "Followings",
                    attributes: ['id'],
                }
                ]
            })
            res.status(200).json(fullUserWithoutPassword);
        } else {
            res.status(200).json(null);
        }

    } catch (error) {
        console.error("/ 쿠키 정보 가져오기 에러 :  ", error);
        next(error);
    }
});


~

 

 

 

 

 

 

 

 

 

 

 

 

65.  getStaticProps 사용해보기

강의 :

https://www.inflearn.com/course/%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/unit/48854?tab=curriculum

 

 

 

SSR 과 getStaticProps 차이는 언제 써도 변경하지 않는다면 getStaticProps 을 사용

접속한 환경에 따라 변화 한다면 SSR 사용 , getStaticProps 까다롭다. 대부분  SSR  사용
 

 

pages/about.js

import React from 'react';
import { useSelector } from 'react-redux';
import Head from 'next/head';
import { END } from 'redux-saga';

import { Avatar, Card } from 'antd';
import AppLayout from '../components/AppLayout';
import wrapper from '../store/configureStore';
import { LOAD_USER_INFO_REQUEST } from '../reducers/user';

const Profile = () => {
    const { userInfo } = useSelector((state) => state.user);

    return (
        <AppLayout>
            <Head>
                <title>ZeroCho | NodeBird</title>
            </Head>
            {userInfo
                ? (
                    <Card
                        actions={[
                            <div key="twit">
                                짹짹
                                <br />
                                {userInfo.Posts}
                            </div>,
                            <div key="following">
                                팔로잉
                                <br />
                                {userInfo.Followings}
                            </div>,
                            <div key="follower">
                                팔로워
                                <br />
                                {userInfo.Followers}
                            </div>,
                        ]}
                    >
                        <Card.Meta
                            avatar={<Avatar>{userInfo.nickname[0]}</Avatar>}
                            title={userInfo.nickname}
                            description="노드버드 매니아"
                        />
                    </Card>
                )
                : null}
        </AppLayout>
    );
};

export const getStaticProps = wrapper.getStaticProps((store) => async (req, res, ...etc) => {
    console.log('getStaticProps  : static 으로 특정한 사용자 정보 가져오기  1 번 유저  : ');
    store.dispatch({
        type: LOAD_USER_INFO_REQUEST,
        data: 1,
    });
    store.dispatch(END);
    await store.sagaTask.toPromise();
});


export default Profile;

 

 

 

백엔드

routes/user.js

~


//특정 유저 정보 가져오기
router.get('/:userId', async (req, res, next) => {
    console.log(" 유저 정보 가져오기 : ", req.params.userId);
    try {
        const fullUserWithoutPassword = await User.findOne({
            where: {
                id: req.params.userId
            },
            attributes: {
                exclude: ['password']
            },
            include: [{
                model: Post,
                attributes: ['id'],
            }, {
                model: User,
                as: "Followers",
                attributes: ['id'],
            },
            {
                model: User,
                as: "Followings",
                attributes: ['id'],
            }
            ]
        })

        if (fullUserWithoutPassword) {
            const data = fullUserWithoutPassword.toJSON();

            //개인정보 침해 예방을 위해 서버에서  데이터길이만 변경해서 보내 준다.
            data.Posts = data.Posts.length;
            data.Followings = data.Followings.length;
            data.Followers = data.Followers.length;

            res.status(200).json(data);
        } else {
            res.status(404).json("존재하지 않는 사용자입니다.");
        }
    } catch (error) {
        console.error("/ 쿠키 정보 가져오기 에러 :  ", error);
        next(error);
    }
});


 

 

 

 

 

profile.js  , signup.js   ServerSideProps  적용

 

~

//새로 고침시 유지

export const getServerSideProps = wrapper.getServerSideProps((store) => async ({ req, res, ...etc }) => {
    const cookie = req ? req.headers.cookie : '';
    axios.defaults.headers.Cookie = '';
    if (req && cookie) {
        axios.defaults.headers.Cookie = cookie;
    }

    store.dispatch({
        type: LOAD_MY_INFO_REQUEST,
    });

    store.dispatch(END);
    await store.sagaTask.toPromise();
})

 

 

 

 

 

 

 

 

 

 

 

66.  다이나믹 라우팅

강의 :

https://www.inflearn.com/course/%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/unit/48855?tab=curriculum

 

 

백엔드

routes/post.js

~


//GET /post  한개 정보 가져오기
router.get('/:postId', async (req, res, next) => {
    console.log(" 한개의. 정보 가져오기 : ", req.params.postId);
    try {

        const posts = await Post.findOne({
            where: { id: req.params.postId },

            include: [{
                model: User,
                attributes: ['id', 'nickname']
            },
            {
                model: Image
            },
            {
                model: Comment,
                include: [{
                    model: User,
                    attributes: ['id', 'nickname']
                }]
            },
            {
                model: User, //좋아요 누른 사람       
                as: 'Likers',
                attributes: ['id']
            },
            {
                model: Post,
                as: 'Retweet',
                include: [
                    {
                        model: User,
                        attributes: ['id', 'nickname']
                    },
                    {
                        model: Image
                    }
                ]
            }
            ]
        });

        console.log(" 한개의. 정보 posts : ", posts);
        res.status(200).json(posts);
    } catch (error) {
        console.error("posts error : ", error);
        next(error);
    }

});



~

 

 

 

 

 

프론트엔드

 

pages/post/[id].js

//post/[id].js
import { useSelector } from 'react-redux';
import Head from 'next/head';
import { useRouter } from 'next/router';
import AppLayout from '../../components/AppLayout';
import PostCard from '../../components/PostCard';
import { LOAD_POST_REQUEST } from './../../reducers/post';
import { LOAD_MY_INFO_REQUEST } from './../../reducers/user';
import { END } from 'redux-saga';
import wrapper from '../../store/configureStore';
import axios from 'axios';

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


    return (
        <AppLayout>
            {singlePost &&
                <Head>
                    <title>
                        {singlePost.User.nickname}
                        님의 글
                    </title>
                    <meta name="description" content={singlePost.content} />
                    <meta property="og:title" content={`${singlePost.User.nickname}님의 게시글`} />
                    <meta property="og:description" content={singlePost.content} />
                    <meta property="og:image" content={singlePost.Images[0] ? singlePost.Images[0].src : 'https://nodebird.com/favicon.ico'} />
                    <meta property="og:url" content={`https://nodebird.com/post/${id}`} />
                </Head>
            }

            {singlePost && <PostCard key={id && id} post={singlePost} />}
        </AppLayout>
    );
};


//새로 고침시 유지
export const getServerSideProps = wrapper.getServerSideProps((store) => async ({ req, res, ...etc }) => {

    const cookie = req ? req.headers.cookie : '';
    axios.defaults.headers.Cookie = '';
    if (req && cookie) {
        axios.defaults.headers.Cookie = cookie;
    }

    store.dispatch({
        type: LOAD_MY_INFO_REQUEST,
    });

    store.dispatch({
        type: LOAD_POST_REQUEST,
        postId: req.url.replace('/post/', '')
    });

    store.dispatch(END);
    await store.sagaTask.toPromise();
})

export default Post;

 

 

 

reducers/post.js

~
    //한개의 POST 정보 불러오기
        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;

~

 

saga/post.js

~


//한개의 POST 정보 불러오기
function loadPostAPI(postId) {
    return axios.get(`/post/${postId}`);
}

function* loadPost(action) {
    try {
        
        const result = yield call(loadPostAPI, action.postId);
        yield put({
            type: LOAD_POST_SUCCESS,
            data: result.data
        });
    } catch (err) {
        console.log(err);
        yield put({
            type: LOAD_POST_FAILURE,
            error: err.response.data
        });
    }
}

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





~

 

 

 

 

 

 

 

 

67.  CSS 서버사이드렌더링

강의 :

https://www.inflearn.com/course/%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/unit/48856?tab=curriculum

 

1 ) 라이브러리 설치

$ npm i babel-plugin-styled-components

 

2 ) .babelrc  추가

{
  "presets": ["next/babel"],
  "plugins": [["babel-plugin-styled-components", {
        "ssr": true,
        "displayName" :true
      }
  ]]
}

 

 

 

3) pages/_document.js

import React from 'react';
import Document, { Html, Head, Main, NextScript } from 'next/document';
import { ServerStyleSheet } from 'styled-components';

export default class MyDocument extends Document {
    static async getInitialProps(ctx) {
        const sheet = new ServerStyleSheet();
        const originalRenderPage = ctx.renderPage;
        try {
            ctx.renderPage = () => originalRenderPage({
                enhanceApp: App => props => sheet.collectStyles(<App {...props} />),
            });
            const initialProps = await Document.getInitialProps(ctx);
            return {
                ...initialProps,
                styles: (
                    <>
                        {initialProps.styles}
                        {sheet.getStyleElement()}
                    </>
                ),
            };
        } catch (error) {
            console.error(error);
        } finally {
            sheet.seal();
        }
    }

    render() {
        return (
            <Html>
                <Head />
                <body>
                    <Main />
                    <NextScript />
                </body>
            </Html>
        );
    }
}

 

 

 

 

Next.js의 _document 파일은 무엇입니까?

업데이트:2022년 7월 10일

https://www.webdevtutor.net/blog/what-is-the-document-file-in-nextjs

 

Next.js에서 _document 파일의 중요성

이 _document파일은 Next.js 애플리케이션에서 페이지의 초기화 프로세스를 전체적으로 제어할 수 있게 해주는 몇 가지 중요한 파일 중 하나입니다.

Next.js 애플리케이션을 전체적으로 제어할 수 있는 다른 파일로는 , _app.js및 가 있습니다._document.jsnext.config.jspackage.json

Next.js의 _document 파일은 무엇입니까?

_document 파일은 페이지 초기화 중에 호출되며 기본 문서 페이지를 재정의하는 데 사용됩니다. _document 파일 내에서 HTML 및 Body 태그를 업데이트하여 기본 구현을 재정의할 수 있습니다.

_document.js파일은 Next.js 애플리케이션 폴더의 루트에 있는 폴더 pages에 있습니다 pages/_document.js.

파일은 기본적으로 포함되어 있지 않으므로 파일이 보이지 않으면 에서 빈 파일을 만드 pages/_document.js십시오.

_document 파일의 사용 사례

_documentNext.js 웹 개발 프로세스 중에 파일 을 사용자 지정하려는 두 가지 일반적인 사용 사례를 살펴보겠습니다 .

_document이 게시물에서는 파일 을 사용자 지정하기 위한 다음 두 가지 일반적인 사용 사례에 대해 알아봅니다 .

  1. HTML 태그에 사용자 정의 언어 추가
  2. Body 태그에 사용자 지정 ClassName 추가

HTML 태그에 사용자 정의 언어 추가

HTMLNext.js의 태그에 사용자 정의 언어를 추가하려는 경우 _document파일이 이를 위한 완벽한 장소입니다.

English다음은 애플리케이션에서 생성된 모든 HTML 파일에 기본 언어를 추가하는 코드 예제입니다 .

import { Html, Head, Main, NextScript } from "next/document";

export default function Document() {
  return (
    <Html lang="en">
      <Head />
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}

Body 태그에 사용자 정의 CSS 클래스 추가

body파일 을 업데이트하면 Next.js 애플리케이션 의 태그에 맞춤 CSS 클래스를 쉽게 추가 할 수 _document있습니다.

이 코드 예제에서 bodyNext.js에 의해 생성된 모든 HTML 페이지의 전체 요소는 검정색 배경을 갖습니다.

import { Html, Head, Main, NextScript } from "next/document";

export default function Document() {
  return (
    <Html>
      <Head />
      <body className="bg-black">
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}

추천도서

Next.js의 _document 파일과 _app 파일 비교

_document 파일과 _app 파일은 Next.js에서 중요한 두 파일입니다. 이 블로그 게시물은 _document 파일과 _app 파일을 비교하고 대조합니다.

Next.js의 _document 파일과 _app 파일 비교

Next.js 애플리케이션의 _document 파일 이해 요약

이 블로그 게시물에서는 HTML 태그를 사용자 정의하는 방법과 사용자 정의 클래스 이름을 body 태그에 추가하는 방법을 배웠습니다 .

Next.JS 애플리케이션에 추가 사용자 지정을 추가 하려면 Next.js의 _app 파일에 대한 사용 사례를 확인하세요 .

✍️ 새 블로그 게시물

주니어 개발자로서 가면 증후군을 극복하는 방법

???? NextJS 마스터리

아래 기사를 선택하여 NextJS 숙달의 길을 계속 가십시오 !

Next.js 앱을 위한 5가지 SEO 팁

SEO가 중요합니다. Next.js 앱에서 제대로 구현되었나요? SEO를 개선하려면 다음 5가지 팁을 따르세요!

Next.js 앱을 위한 5가지 SEO 팁

Next.js의 페이지에 메타 태그 추가

Next.js에서 SEO를 하고 계십니까? 이 가이드를 따라 메타 태그를 사용하세요!

Next.js의 페이지에 메타 태그 추가

Next.js의 링크 구성 요소에서 onclick 이벤트 처리

Next.js에서 onclick 이벤트를 처리하는 데 문제가 있습니까? 이 게시물은 링크 구성 요소로 이러한 이벤트를 처리하는 방법을 보여줍니다.

Next.js의 링크 구성 요소에서 onclick 이벤트 처리

 

 

 

 

 

 

 

 

 

 

 

 

68.  사용 게시글, 해시태그 게시글

강의 : 

https://www.inflearn.com/course/%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/unit/48857?tab=curriculum

 

 

백엔드

back/hashtag.js

const express = require('express');
const { Op } = require('sequelize');
const { Post, Hashtag, Image, User, Comment } = require('../models');

const router = express.Router();



//GET /hashtag  /노드
router.get('/:hashtag', async (req, res, next) => {
    try {
        const where = {};
        if (parseInt(req.query.lastId, 10)) { //초기 로딩이 아닐때
            //다음 코드 내용은 id 가  lastId  보다 작은 것 =>  id < lastId
            // Op 의미는 연산자 의미  lt 는  <  
            where.id = { [Op.lt]: parseInt(req.query.lastId, 10) }
        };

        const posts = await Post.findAll({
            where,
            limit: 10,
            order: [['createdAt', 'DESC'],],
            include: [
                {
                    model: Hashtag,
                    where: { name: decodeURIComponent(req.params.hashtag) }
                },
                {
                    model: User,
                    attributes: ['id', 'nickname']
                },
                {
                    model: Image
                },
                {
                    model: Comment,
                    include: [{
                        model: User,
                        attributes: ['id', 'nickname']
                    }]
                },
                {
                    model: User, //좋아요 누른 사람       
                    as: 'Likers',
                    attributes: ['id']
                },
                {
                    model: Post,
                    as: 'Retweet',
                    include: [
                        {
                            model: User,
                            attributes: ['id', 'nickname']
                        },
                        {
                            model: Image
                        }
                    ]
                }
            ]
        });

        res.status(200).json(posts);

    } catch (error) {
        console.error("posts error : ", error);
        next(error);
    }

});



module.exports = router;

 

back/user.js

const express = require('express');
const { User, Post, Comment, Image } = require('../models');
const { Op } = require('sequelize');
const bcrypt = require('bcrypt');
const passport = require('passport');
const router = express.Router();
const { isLoggedIn, isNotLoggedIn } = require('./middlewares');



~

//특정 사용자에 대한 게시글 목록
//GET /posts
router.get('/:userId/posts', async (req, res, next) => {

    try {
        console.log(" 특정 사용자에 대한 게시글 목록  :", req.query.lastId);
        const where = { UserId: req.params.userId };
        if (parseInt(req.query.lastId, 10)) { //초기 로딩이 아닐때
            //다음 코드 내용은 id 가  lastId  보다 작은 것 =>  id < lastId
            // Op 의미는 연산자 의미  lt 는  <  
            where.id = { [Op.lt]: parseInt(req.query.lastId, 10) }
        };

        const posts = await Post.findAll({
            where,
            limit: 10,
            order: [
                ['createdAt', 'DESC'],
                [Comment, 'createdAt', 'DESC']
            ],
            // offsset: parseInt(req.params.limit),
            //21,20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1
            include: [{
                model: User,
                attributes: ['id', 'nickname']
            },
            {
                model: Image
            },
            {
                model: Comment,
                include: [{
                    model: User,
                    attributes: ['id', 'nickname']
                }]
            },
            {
                model: User, //좋아요 누른 사람       
                as: 'Likers',
                attributes: ['id']
            },
            {
                model: Post,
                as: 'Retweet',
                include: [
                    {
                        model: User,
                        attributes: ['id', 'nickname']
                    },
                    {
                        model: Image
                    }
                ]
            }
            ]
        });

        res.status(200).json(posts);

    } catch (error) {
        console.error("posts error : ", error);
        next(error);
    }

});

 

 

 

 

 

 

 

프론트엔드

pages/[tag].js

// hashtag/[tag].js
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useRouter } from 'next/router';
import { END } from 'redux-saga';

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

const Hashtag = () => {
    const dispatch = useDispatch();
    const router = useRouter();
    const { tag } = router.query;
    const { mainPosts, hasMorePosts, loadPostsLoading } = useSelector((state) => state.post);

    useEffect(() => {
        const onScroll = () => {
            if (window.pageYOffset + document.documentElement.clientHeight > document.documentElement.scrollHeight - 300) {
                if (hasMorePosts && !loadPostsLoading) {
                    dispatch({
                        type: LOAD_HASHTAG_POSTS_REQUEST,
                        lastId: mainPosts[mainPosts.length - 1] && mainPosts[mainPosts.length - 1].id,
                        data: tag,
                    });
                }
            }
        };
        window.addEventListener('scroll', onScroll);
        return () => {
            window.removeEventListener('scroll', onScroll);
        };
    }, [mainPosts.length, hasMorePosts, tag, loadPostsLoading]);

    return (
        <AppLayout>
            {mainPosts.map((c) => (
                <PostCard key={c.id} post={c} />
            ))}
        </AppLayout>
    );
};


export const getServerSideProps = wrapper.getServerSideProps((store) => async ({ req, res, ...etc }) => {
    const cookie = req ? req.headers.cookie : '';
    axios.defaults.headers.Cookie = '';
    if (req && cookie) {
        axios.defaults.headers.Cookie = cookie;
    }

    store.dispatch({
        type: LOAD_MY_INFO_REQUEST,
    });
    store.dispatch({
        type: LOAD_HASHTAG_POSTS_REQUEST,
        data: req.url.replace('/hashtag/', '')
    });

    store.dispatch(END);

    await store.sagaTask.toPromise();
});

export default Hashtag;

 

 

 

pages/[id].js

import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Avatar, Card } from 'antd';
import { END } from 'redux-saga';
import Head from 'next/head';
import { useRouter } from 'next/router';

import axios from 'axios';
import { LOAD_USER_POSTS_REQUEST } from '../../reducers/post'; //특정 사용자의 글
import { LOAD_MY_INFO_REQUEST, LOAD_USER_INFO_REQUEST } from '../../reducers/user';
import PostCard from '../../components/PostCard';
import wrapper from '../../store/configureStore';
import AppLayout from '../../components/AppLayout';

//특정 사용자에 대한 게시글 목록
const User = () => {
    const dispatch = useDispatch();
    const router = useRouter();
    const { id } = router.query;
    const { mainPosts, hasMorePosts, loadPostsLoading } = useSelector((state) => state.post);
    const { userInfo, me } = useSelector((state) => state.user);

    useEffect(() => {
        const onScroll = () => {
            if (window.pageYOffset + document.documentElement.clientHeight > document.documentElement.scrollHeight - 300) {
                if (hasMorePosts && !loadPostsLoading) {
                    dispatch({
                        type: LOAD_USER_POSTS_REQUEST,
                        lastId: mainPosts[mainPosts.length - 1] && mainPosts[mainPosts.length - 1].id,
                        data: id,
                    });
                }
            }
        };
        window.addEventListener('scroll', onScroll);
        return () => {
            window.removeEventListener('scroll', onScroll);
        };
    }, [mainPosts.length, hasMorePosts, id, loadPostsLoading]);

    return (
        <AppLayout>
            {userInfo && (
                <div>{userInfo.nickname}</div>

                // <Head>
                //     <title>
                //         {userInfo.nickname}
                //         님의 글
                //     </title>
                //     <meta name="description" content={`${userInfo.nickname}님의 게시글`} />
                //     <meta property="og:title" content={`${userInfo.nickname}님의 게시글`} />
                //     <meta property="og:description" content={`${userInfo.nickname}님의 게시글`} />
                //     <meta property="og:image" content="https://nodebird.com/favicon.ico" />
                //     <meta property="og:url" content={`https://nodebird.com/user/${id}`} />
                // </Head>
            )}
            {userInfo && (userInfo.id !== me?.id)
                ? (
                    <Card
                        style={{ marginBottom: 20 }}
                        actions={[
                            <div key="twit">
                                짹짹
                                <br />
                                {userInfo.Posts}
                            </div>,
                            <div key="following">
                                팔로잉
                                <br />
                                {userInfo.Followings}
                            </div>,
                            <div key="follower">
                                팔로워
                                <br />
                                {userInfo.Followers}
                            </div>,
                        ]}
                    >
                        <Card.Meta
                            avatar={<Avatar>{userInfo.nickname[0]}</Avatar>}
                            title={userInfo.nickname}
                        />
                    </Card>
                )
                : null}
            {mainPosts.map((c) => (
                <PostCard key={c.id} post={c} />
            ))}
        </AppLayout>
    );
};




export const getServerSideProps = wrapper.getServerSideProps((store) => async ({ req, res, ...etc }) => {
    const cookie = req ? req.headers.cookie : '';
    axios.defaults.headers.Cookie = '';
    if (req && cookie) {
        axios.defaults.headers.Cookie = cookie;
    }

    store.dispatch({
        type: LOAD_USER_POSTS_REQUEST,
        data: req.url.replace('/user/', ''),
    });
    store.dispatch({
        type: LOAD_MY_INFO_REQUEST,
    });
    store.dispatch({
        type: LOAD_USER_INFO_REQUEST,
        data: req.url.replace('/user/', ''),
    });


    store.dispatch(END);
    await store.sagaTask.toPromise();
})

export default User;

 

 

 

reducers/post.js

//특정 사용자 게시글
export const LOAD_USER_POSTS_REQUEST = 'LOAD_USER_POSTS_REQUEST';
export const LOAD_USER_POSTS_SUCCESS = 'LOAD_USER_POSTS_SUCCESS';
export const LOAD_USER_POSTS_FAILURE = 'LOAD_USER_POSTS_FAILURE';

//특정 해쉬 글
export const LOAD_HASHTAG_POSTS_REQUEST = 'LOAD_HASHTAG_POSTS_REQUEST';
export const LOAD_HASHTAG_POSTS_SUCCESS = 'LOAD_HASHTAG_POSTS_SUCCESS';
export const LOAD_HASHTAG_POSTS_FAILURE = 'LOAD_HASHTAG_POSTS_FAILURE';




~

        //무한 스크롤 
        case LOAD_USER_POSTS_REQUEST:
        case LOAD_HASHTAG_POSTS_REQUEST:
        case LOAD_POSTS_REQUEST:
            draft.loadPostsLoading = true;
            draft.loadPostsDone = false;
            draft.loadPostsError = null;
            break;

        case LOAD_USER_POSTS_SUCCESS:
        case LOAD_HASHTAG_POSTS_SUCCESS:
        case LOAD_POSTS_SUCCESS:
            draft.loadPostsLoading = false;
            draft.loadPostsDone = true;
            draft.mainPosts = draft.mainPosts.concat(action.data);
            // console.log("무한 스크롤 : ", draft.mainPosts);
            //draft.mainPosts = action.data.concat(draft.mainPosts);
            draft.hasMorePosts = action.data.length === 10;
            break;

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

~

 

 

 

saga/post.js

~
import {
~
   LOAD_USER_POSTS_REQUEST, LOAD_USER_POSTS_SUCCESS, LOAD_USER_POSTS_FAILURE,
    LOAD_HASHTAG_POSTS_REQUEST, LOAD_HASHTAG_POSTS_SUCCESS, LOAD_HASHTAG_POSTS_FAILURE

} from '../reducers/post'






//해시 태그에 대한  게시글 목록
function loadHashtagPostsAPI(data, lastId) {
    return axios.get(`/hashtag/${encodeURIComponent(data)}?lastId=${lastId || 0}`);
}
function* loadHashtagPosts(action) {
    try {
        const result = yield call(loadHashtagPostsAPI, action.data, action.lastId);
        yield put({
            type: LOAD_HASHTAG_POSTS_SUCCESS,
            data: result.data
        });

    } catch (err) {
        console.log(err);
        yield put({
            type: LOAD_HASHTAG_POSTS_FAILURE,
            error: err.response.data
        });
    }
}
function* watchLoadHashtagPosts() {
    yield takeLatest(LOAD_HASHTAG_POSTS_REQUEST, loadHashtagPosts);
}


//특정 사용자에 대한 게시글
function loadUserPostsAPI(data, lastId) {
    return axios.get(`/user/${data}/posts?lastId=${lastId || 0}`);
}
function* loadUserPosts(action) {
    try {
        const result = yield call(loadUserPostsAPI, action.data, action.lastId);
        yield put({
            type: LOAD_USER_POSTS_SUCCESS,
            data: result.data
        });
    } catch (err) {
        console.log(err);
        yield put({
            type: LOAD_USER_POSTS_FAILURE,
            error: err.response.data
        });
    }
}
function* watchLoadUserPosts() {
    yield takeLatest(LOAD_USER_POSTS_REQUEST, loadUserPosts);
}




export default function* postSaga() {
    yield all([
~

        fork(watchLoadHashtagPosts),
        fork(watchLoadUserPosts)


    ]);
}




 

 

 

 

saga/user.js

~

// 유저정보 가져오기
function loadUserInfoAPI(userId) {
    //console.log("사가 유저 정보 가져오기  : ", userId);
    return axios.get(`/user/${userId}`);
}

~

//1-3 로그인 처리
function* watchLogIn() {
    //LOG_IN 실행 될때 까지 기다리겠다.
    //console.log("2. watchLogIn ");
    yield takeLatest(LOG_IN_REQUEST, login);
}


 

 

 

 

 

 

 

 

 

 

about author

PHRASE

Level 60  라이트

여가 있는 생활과 부부만의 생활을 자주 가져라. -부부이십훈-

댓글 ( 4)

댓글 남기기

작성