React

 

 

 

 Next.js 서버사이드렌더링

 

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

버전

next:  13.0.4

antd:  5.0.1

 

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

 

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

 

 

 

 

69.  getStaticPaths

 

강의 :

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/48858?tab=curriculum

 

 

 

리액트

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)

    // if (router.isFallback) {
    //     return <div>로딩중...</div>;
    // }

    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} />}

            {singlePost == null && '등록된 게시글이 없습니다.'}

        </AppLayout>
    );
};


//getStaticProps  는 다이나믹 패스에서 사용하는데,
//다이나믹이니깐 어떤것을 만들어 줘야할지 모른다 따라서,
//다음과 해당 id 만 정적인 html 만들어 준다
// export async function getStaticPaths() {
//     return {
//         paths: [
//             { params: { id: '16' } },
//             { params: { id: '17' } },
//             { params: { id: '18' } },
//         ],
//         fallback: true,
//     };
// }




//getServerSideProps

// export const getStaticProps = wrapper.getStaticProps(async (context) => {
//     const cookie = context.req ? context.req.headers.cookie : '';

//     console.log("context    : ::: ", context);

//     axios.defaults.headers.Cookie = '';
//     if (context.req && cookie) {
//         axios.defaults.headers.Cookie = cookie;
//     }
//     context.store.dispatch({
//         type: LOAD_MY_INFO_REQUEST,
//     });
//     context.store.dispatch({
//         type: LOAD_POST_REQUEST,
//         data: context.params.id,
//     });
//     context.store.dispatch(END);
//     await context.store.sagaTask.toPromise();
// });


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

    console.log(" reqreqreqreq    ", req);

    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;


 

 

 

 

 

 

 

 

70.  swr 사용해보기

강의 : 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/48859?tab=curriculum

 

 

=> React(Nextjs) 프로젝트는 ★ Redux를 Saga 대체 SWR 사용하기 

 

 

swr 라이브러리 설치후

npm i swr

다음과 같이 사용하면  된다.

~


const fetcher = (url) => axios.get(url, { withCredentials: true }).then((result) => result.data);
const backUrl = 'http://localhost:3065';

    const [followersLimit, setFollowersLimit] = useState(3);
    const [followingsLimit, setFollowingsLimit] = useState(3);

    const { data: followingsData, error: followingError } = useSWR(`${backUrl}/user/followings?limit=${followingsLimit}`, fetcher);
    const { data: followersData, error: followerError } = useSWR(`${backUrl}/user/followers?limit=${followersLimit}`, fetcher);





~

 

 

 

 

 

 

백엔드

 routes/user.js

~
//팔로워 불러워기
router.get('/followers', isLoggedIn, async (req, res, next) => { //get  /user/followers
    console.log(" 팔로워 불러워기  followers   : ");
    try {
        //패스포트에 로그인한 user id  값 :  req.user.id
        const user = await User.findOne({ where: { id: req.user.id } });
        //3개씩 불러오기
        const followers = await user.getFollowers({
            limit: parseInt(req.query.limit, 10)
        });
        res.status(200).json(followers)
    } catch (error) {
        console.error(error);
        next(error);
    }
});



// 팔로잉 불러워기 시퀄라이즈에서 다음과 같이 Followings  처리를 해서  getFollowings 적용 됨
router.get('/followings', isLoggedIn, async (req, res, next) => { //get  /user/followings
    try {
        const user = await User.findOne({ where: { id: req.user.id } });
        const followings = await user.getFollowings({
            limit: parseInt(req.query.limit, 10)
        });
        res.status(200).json(followings);
    } catch (error) {
        console.error(error);
        next(error);
    }
});

~

 

 

 

 

프론트엔드

pages/profile.js

import React, { useEffect, useState, useCallback } from 'react';
import AppLayout from './../components/AppLayout';
import Head from 'next/head';
import { useSelector } from 'react-redux';
import Router from 'next/router';
import FollowList from './../components/FollowList';
import NicknameEditForm from './../components/NicknameEditForm';
import { LOAD_MY_INFO_REQUEST } from '../reducers/user';
import { END } from 'redux-saga';
import wrapper from '../store/configureStore';
import axios from 'axios';
import useSWR from 'swr';


const fetcher = (url) => axios.get(url, { withCredentials: true }).then((result) => result.data);
const backUrl = 'http://localhost:3065';


const Profile = () => {
    const { me } = useSelector((state) => state.user);
    const [followersLimit, setFollowersLimit] = useState(3);
    const [followingsLimit, setFollowingsLimit] = useState(3);

    const { data: followingsData, error: followingError } = useSWR(`${backUrl}/user/followings?limit=${followingsLimit}`, fetcher);
    const { data: followersData, error: followerError } = useSWR(`${backUrl}/user/followers?limit=${followersLimit}`, fetcher);



    useEffect(() => {
        if (!(me && me.id)) {
            Router.replace('/');
        }
    }, [me && me.id])


    const loadMoreFollowings = useCallback(() => {
        setFollowingsLimit((prev) => prev + 3);
    }, []);


    const loadMoreFollowers = useCallback(() => {
        setFollowersLimit((prev) => prev + 3);
    }, []);



    if (!me) {
        return '내 정보 로딩중...';
    }

    if (followerError || followingError) {
        console.error(followerError || followingError);
        return <div>팔로잉/팔로워 로딩 중 에러가 발생합니다.</div>;
    }

    return (
        <>
            <Head>
                <title>프로필 | NodeBird</title>
            </Head>
            <AppLayout>
                <NicknameEditForm />
                <div style={{ marginBottom: 20 }}></div>
                {/* <FollowList header="팔로잉" data={me.Followings} />
                <FollowList header="팔로워" data={me.Followers} /> */}

                <FollowList header="팔로잉" data={followingsData} onClickMore={loadMoreFollowings} loading={!followingsData && !followingError} />
                <FollowList header="팔로워" data={followersData} onClickMore={loadMoreFollowers} loading={!followersData && !followerError} />
            </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(END);
    await store.sagaTask.toPromise();
})

export default Profile;

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

71.  해시태그 검색하기

강의 : 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/48860?tab=curriculum

 

백엔드

routes/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) => {
    console.log(" /hashtag  /노드  :  ", decodeURIComponent(req.params.hashtag));
    let hashtag = decodeURIComponent(req.params.hashtag);

    //다음과 같은 형식으로 파라미터를 받을 경우
    // /_next/data/development리액트.json?tag=리액트
    if (hashtag.indexOf("tag=") !== -1) {
        hashtag = hashtag.split("tag=");
        hashtag = hashtag[1];
        console.log("문자열 포함  :hashtag  => ", hashtag);
    }

    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: 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;

 

 

 

]

 

프론트엔드

 

components/AppLayout.js

import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import Link from 'next/link'
import { Menu, Input, Row, Col } from 'antd';
import styled from 'styled-components';
import { useSelector } from 'react-redux';
import Router from 'next/router';
import UserProfile from './UserProfile';
import LoginForm from './LoginForm';
import useInput from '../hooks/useInput';

const SearchInput = styled(Input.Search)`
    vertical-align: 'middle' ;
`;

const AppLayout = ({ children }) => {
    const [searchInput, onChangeSearchInput] = useInput('');
    const { me } = useSelector((state) => state.user);

    const onClick = useCallback((e) => {
        if (e.key === "item-4") {
            if (searchInput) {
                // onChangeSearchInput("sdfsdf");
                console.log("검색어: ", searchInput);
                Router.push(`/hashtag/${searchInput}`);
            }
        }
    }, [searchInput]);



    const items = [
        { label: <Link href="/" >노드버드</Link>, key: 'item-1' },
        me && { label: <Link href="/profile">프로필</Link>, key: 'item-2' },
        !me && { label: <Link href="/signup">회원가입</Link>, key: 'item-3' },
        {
            label: <SearchInput enterButton value={searchInput} onChange={onChangeSearchInput} />,
            key: 'item-4'
        }
    ];

    return (
        <div>

            <Menu onClick={onClick} mode="horizontal" items={items} />

            <Row gutter={24} style={{ marginTop: 20 }}>
                <Col xs={24} md={6} style={{ marginTop: 20 }}>
                    Hello.Next
                </Col>
            </Row>

            <Row gutter={24} style={{ marginTop: 20 }}>
                <Col xs={24} md={6} style={{ marginTop: 20 }}>
                    {me ? <UserProfile /> : <LoginForm />}
                </Col>
                <Col xs={24} md={16} style={{ marginTop: 20 }} >
                    {children}
                </Col>
            </Row>

            <Row gutter={24} style={{ marginTop: 20 }}>
                <Col xs={24} md={24} style={{ marginTop: 20 }}>
                    <a href='https://macaronics.net' target="_blank" rel="noreferrer noopener"  >
                        Made by macaronics
                    </a>
                </Col>
            </Row>

        </div>
    );
};


AppLayout.prototype = {
    children: PropTypes.node.isRequired
}

export default AppLayout;

 

 

 

 

 

 

 

 

 

 

72.  moment와 next 빌드하기

강의 :

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/48861?tab=curriculum

 

 

day.js 사용 방법 - JavaScript 날짜 라이브러리

 

 

 

프론트엔드

components/PostCard.js

~
import moment from 'moment';


~

moment.locale("ko");


const PostCard = ({ post }) => {



~
          {post.RetweetId && post.Retweet ? (
                    <Card
                        cover={post.Retweet.Images[0] && <PostImages images={post.Retweet.Images} />}
                        style={{ background: '#eee' }}
                    >

                        <div style={{ float: 'right' }}>
                            {moment(post.createdAt).format('YYYY.MM.DD')}
                        </div>
                        <Card.Meta

~


 

 

 

빌드처리

eslint 생략

.eslintignore 파일 생성후

*

 

 

빌드

$npm run build

 


=>

Route (pages)                              Size     First Load JS
┌ λ /                                      1.52 kB        1.28 MB
├   /_app                                  0 B            1.04 MB
├ ○ /404                                   1.52 kB        1.04 MB
├ ● /about (2917 ms)                       1.92 kB        1.18 MB
├ λ /hashtag/[tag]                         568 B          1.27 MB
├ λ /post/[id]                             610 B          1.27 MB
├ λ /profile                               6.67 kB        1.23 MB
├ λ /signup                                29.3 kB        1.21 MB
└ λ /user/[id]                             804 B          1.27 MB
+ First Load JS shared by all              1.04 MB
  ├ chunks/framework-3b5a00d5d7e8d93b.js   45.4 kB
  ├ chunks/main-abafce5311b78c60.js        31.2 kB
  ├ chunks/pages/_app-48dc86756f8fa920.js  961 kB
  ├ chunks/webpack-2c683dfba3390d25.js     1.89 kB
  └ css/0e8b7820bc287a84.css               194 B

λ  (Server)  server-side renders at runtime (uses getInitialProps or getServerSideProps)
○  (Static)  automatically rendered as static HTML (uses no initial props)
●  (SSG)     automatically generated as static HTML + JSON (uses getStaticProps)

 

 

chunks/pages/_app-48dc86756f8fa920.js
=>무엇을 의미하는지 모른다.

 

다음 라이브러리 설치

$  npm i @next/bundle-analyzer



 

 

 

 

 

 

73.  커스텀 웹팩과 bundle-analyzer

강의 :

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/48862?tab=curriculum

 

 

 

next.config.js  설정

 

$ npm i @next/bundle-analyzer

 

** production  설정
 

 

package.json

  "scripts": {
    "dev": "next dev -p 3060",
    "build": "ANALYZEER=true NODE_ENV=production  next build "
  },

 

=> 리눅스 와  맥에서만 적용된다.

 

따라서,  cross-env   설치 

$ npm i cross-env

 

=>

  "scripts": {
    "dev": "next dev -p 3060",
    "build": "cross-env ANALYZE=true NODE_ENV=production  next build "
  },

 

 

next.config.js

const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
});

const nextConfig = {
  reactStrictMode: true,
  swcMinify: true,
  //styledComponents: true,
  compiler: {
    // Enables the styled-components SWC transform
    styledComponents: true
  },

  compress: true,

  webpack(config, { webpack }) {
    const prod = process.env.NODE_ENV === 'production';
    const newConfig = {
      ...config,
      mode: prod ? 'production' : 'development',
      plugins: [
        ...config.plugins,
        new webpack.ContextReplacementPlugin(/moment[/\\]locale$/, /^\.\/ko$/),
      ],
    };
    if (prod) {
      newConfig.devtool = 'hidden-source-map';
    }
    return newConfig;
  },



}



module.exports = withBundleAnalyzer(nextConfig);

 

 

 

 

 

 

Route (pages)                              Size     First Load JS
┌ λ /                                      1.52 kB         352 kB
├   /_app                                  0 B             115 kB
├ ○ /404                                   1.52 kB         117 kB
├ ● /about (2793 ms)                       1.92 kB         261 kB
├ λ /hashtag/[tag]                         569 B           351 kB
├ λ /post/[id]                             610 B           351 kB
├ λ /profile                               6.67 kB         303 kB
├ λ /signup                                29.3 kB         289 kB
└ λ /user/[id]                             805 B           351 kB
+ First Load JS shared by all              116 kB
  ├ chunks/framework-3b5a00d5d7e8d93b.js   45.4 kB
  ├ chunks/main-abafce5311b78c60.js        31.2 kB
  ├ chunks/pages/_app-08f11cd4ffdb460a.js  37 kB
  ├ chunks/webpack-579f3e5ed9d92545.js     1.89 kB
  └ css/0e8b7820bc287a84.css               194 B

λ  (Server)  server-side renders at runtime (uses getInitialProps or getServerSideProps)
○  (Static)  automatically rendered as static HTML (uses no initial props)
●  (SSG)     automatically generated as static HTML + JSON (uses getStaticProps)

 

 

 

 

 

 

 

 

 

 

about author

PHRASE

Level 60  라이트

재물은 정당한 방편으로 모으고, 이미 얻은 것은 마땅히 4등분하여 그 한 몫은 자기가 먹는 데 쓰고, 두 몫은 생업의 경영에 쓰며, 나머지는 간직해 두었다가 사는데 모자람이 없게 하라. -성전

댓글 ( 4)

댓글 남기기

작성