
인프런 ==> 따라하며 배우는 노드, 리액트 시리즈 - 유튜브 사이트 만들기
유튜브 강의 목록 : https://www.youtube.com/playlist?list=PL9a7QRYt5fqnlSRu--re7N_1Ean5jFsh3
 
소스 : https://github.com/braverokmc79/ReactYoutubeCloneSeries
1.유듀브 사이트 만들기
2.전체적인 틀 만들고 MongoDB 연결
Boiler Plate 강의
이건 완성본 소스
https://github.com/jaewonhimnae/react-youtube-clone

1. 서버 nodejs 설치
1) node 디렉토리 설치
$ mkdir server
$ cd server
$ npm init
2)모듈 설치
* dependencies 설치
bcrypt  - 비밀번호  암호화 사용으로 bcrypt 는 단방향 해시 함수를 이용한 모듈
body-parser   - request 과 응답 response 사이에서 공통적인 기능을 수행하는 미들웨어
cookie-parser   - 요청된 쿠키를 쉽게 추출할 수 있도록 해주는 미들웨어.
fluent-ffmpeg  -  썸네일과 영상정보를 추출
jsonwebtoken  - jwt 토큰발급
moment  -  날짜를 파싱, 벨리데이션, 포맷을 지정
mongoose  -  Node.js(express)와 MongoDB 연동 
multer  -  파일 업로드를 위해 사용되는 multipart/form-data를 다루기 위한 node.js 의 미들웨어
socket.io -  웹 클라이언트와 서버 간의 실시간 양방향 통신을 가능하게 해주는 Node.js의 모듈
설치
$ yarn add bcrypt body-parser cookie-parser fluent-ffmpeg jsonwebtoken moment mongoose multer socket.io
$ yarn add express connect-mongodb-session express-session
* devDependencies 설치 (개발시 사용)
concurrently  - 서버와 클라이언트 동시에 실행 시키기 
nodemon   -수정이 있으면 서버를 자동으로 restart해주는 모듈
설치
$ npm i -D concurrently nodemon
 
3) 다음 이미와 같이 디렉토리 생성 및 파일생성

4) 몽고 DB 연결
B)[MongoDB] 몽고디비 GUI 개발/관리도구 Studio 3T 설치 (Robo 3T)
C) 몽고 DB 클라우드 가입 : https://cloud.mongodb.com/
D)몽고 DB 툴로 연결 할 경우
로컬일 경우 : mongodb://localhost:27017
클라우드일 경우 : mongodb+srv://macaronics:<password>@mongo-macaronics.y37mjuf.mongodb.net/test
E)package.json scripts 를 다음 과 같이 수정
nodemon 은 실시간 프로젝트 변경감지
"scripts": {
    "start": "NODE_ENV=production node index.js",
    "backend":"NODE_ENV=development nodemon index.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },$ production 환경 실행 : npm start 또는 npm run start
$ development 환경 실행 : npm run backend
dev.js
module.exports = {
    mongoURI: "mongodb+srv://macaronics:<password>@mongo-macaronics.y37mjuf.mongodb.net/test"
}
prod.js
module.exports = {
    //MONGO_URI 는 mongodb 클라우스드에서 필요로 하는  변수명이다. 따라서 다른 업체 서버에 실행시 환경에 맞게 변경
    mongoURI: process.env.MONGO_URI
}
key.js
if (process.env.NODE_ENV === 'production') {
    module.exports = require("./prod");
} else {
    module.exports = require("./dev");
}
F) DB 연결 테스트
index. js 다음과 같이 작성후 실행
$ npm run backend
const express = require('express');
const app = express();
const port = 5000;
const mongoose = require("mongoose");
const config = require("./config/key");
mongoose.connect(config.mongoURI,
).then(() => console.log("MongoDB Connected...")).catch(err => console.error("에러 :", err));
app.get('/', (req, res) => {
    res.send("Hello World!");
})
app.listen(port, () => {
    console.log(`node and react project port ${port}`);
});
5) MongoDB Users Model & Schema & routes & 회원 가입기능 & auth 미들웨어
a) routes/users.js
const express = require('express');
const router = express.Router();
const { User } = require("../models/User");
const { auth } = require("../middleware/auth");
/*1.인증확인 처리
role 1 어드민  role 2 특정 부서 어드민
rele 0 -> 일반유저 ,  role 0 이 아니면 관리자.
*/
router.post("/auth", auth, (req, res) => {
    //auth 미들웨어 통해 인증 처리되었으면 Authentication 가 True 이다.
    //따라서, 다음과 같이 유저 정보를 반환 처리한다.
    res.status(200).json({
        _id: req.user._id,
        isAdmin: req.user.role === 0 ? false : true,
        isAuth: true,
        email: req.user.email,
        name: req.user.name,
        lastname: req.user.lastname,
        role: req.user.role,
    })
});
/*2. 회원등록 처리*/
router.post("/register", (req, res) => {
    const user = new User(req.body);
    //몽고 DB 에 설정된 save  사용한다. 이때  models/User.js  에서 userSchema.pre('save') 호출하면서
    //저장 처리를 진행 한다.
    user.save((err, doc) => {
        if (err) return res.json({ success: false, err });
        return res.status(200).json({
            success: true
        });
    });
});
/**3. 로그인처리 */
router.post("/login", (req, res) => {
    //1)요청한 이메일을 DB 에서 찾는다.
    User.findOne({ email: req.body.email }, (err, user) => {
        if (!user)
            return res.json({
                loginSuccess: false,
                message: "제공된 이메일에 해당하는 유저가 없습니다."
            });
        //2)요청한 이메일이 DB에 있다면 비밀번호가 맞는지 비교
        user.comparePassword(req.body.password, (err, isMatch) => {
            if (!isMatch)
                return res.json({ loginSuccess: false, message: "비밀번호가 틀렸습니다." });
            //3)비밀번호까지 같다면 Token 생성
            user.generateToken((err, user) => {
                if (err) return res.status(400).send(err);
                //4)토큰을 쿠키에 저장한다.
                res.cookie("w_authExp", user.tokenExp);
                res.cookie("w_auth", user.token)
                    .status(200)
                    .json({
                        loginSuccess: true,
                        userId: user._id
                    });
            });
        });
    });
});
/*4. 로그아웃 처리*/
router.get("/logout", auth, (req, res) => {
    User.findOneAndUpdate({ _id: req.user._id }, { token: "", tokenExp: "" }, (err, doc) => {
        if (err) return res.json({ success: false, err });
        return res.status(200).send({
            success: true
        });
    });
});
module.exports = router;
b)models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const saltRounds = 10;
const jwt = require("jsonwebtoken");
const SECRET_KEY = "abcd!!!333";
const userSchema = mongoose.Schema({
    name: {
        type: String,
        maxlength: 50
    },
    email: {
        type: String,
        trim: true,
        unique: 1
    },
    password: {
        type: String,
        minlength: 5
    },
    lastname: {
        type: String,
        maxlength: 50
    },
    role: {
        type: Number,
        default: 0
    },
    image: String,
    token: {
        type: String
    },
    tokenExp: {
        type: Number
    }
});
//1. 토큰을 복호화 한후 유저를 찾는다. 사용법 :  //https://www.npmjs.com/package/jsonwebtoken
userSchema.statics.findByToken = function (token, callback) {
    const user = this;
    //decoded + SECRET_KEY = tokne 생성 => 아이디와 생성된 토큰으로 몽고DB 함수 findOne() 으로 유저를 조회 처리후, 유저가 존재하면 유저정보를 콜백반환처리
    //user._id+'abcd!!!333'= tokne 생성
    jwt.verify(token, SECRET_KEY, function (err, decoded) { //여기서 decoded 는 user._id 이다.
        user.findOne({ "_id": decoded, "token": token }, function (err, user) {
            if (err) return callback(err);
            callback(null, user);
        });
    });
}
//2.DB에 저장하기 전에 실행한다.
userSchema.pre('save', function (next) {
    const user = this;
    //비밀번호가 변환될때만 다음을 실행하며, 비밀번호가 아닌것은 next()
    if (user.isModified('password')) {
        //비밀번호를 암호와 시킨다.
        bcrypt.genSalt(saltRounds, function (err, salt) {
            if (err) return next(err);
            bcrypt.hash(user.password, salt, function (err, hash) {
                if (err) return next(err);
                user.password = hash;
                next();
            });
        });
    } else {
        next();
    }
})
/*3.로그인 처리시 comparePassword 커스텀 함수 생성  (userSchema.methods+함수명)  */
userSchema.methods.comparePassword = function (plainPassword, callback) {
    //plainPassword 비밀번호가 12345 일때,   this.password 는 암호화된 비밀번호 $2b$10$LK86g2vaPNMHVLkj69hO7uzodTXATNMezdKnWymKi8QoTX9pE3bey
    bcrypt.compare(plainPassword, this.password, function (err, isMatch) {
        if (err) return cb(err);
        else return callback(null, isMatch);
    });
}
/*4.로그인 처리 - 토큰 발행 - jsonwebtoken 사용법 :  //https://www.npmjs.com/package/jsonwebtoken  */
userSchema.methods.generateToken = function (cb) {
    const user = this;
    //jwt.sign({ foo: 'bar' }, 'shhhhh');  shhhhh 는 임이 문자이다. jwt.sign 을 이용해서 token 을 생성한다.
    const token = jwt.sign(user._id.toHexString(), SECRET_KEY);
    user.token = token;
    user.save(function (err, user) {
        if (err) return cb(err)
        cb(null, user)
    });
}
const User = mongoose.model('User', userSchema);
module.exports = { User }
c)middleware/auth.js 미들웨어
const { User } = require("../models/User");
/** 인증 처리를 하는 곳 */
let auth = (req, res, next) => {
    //1.클라이언트 쿠키에서 토큰을 가져온다.
    let token = req.cookies.w_auth;
    //2.토큰을 복호화 한후 유저를 찾는다.
    User.findByToken(token, (err, user) => {
        //3.유저가 없으면 인증 No !
        if (err) throw err;
        if (!user) return res.json({ isAuth: false, error: true });
        //4.유저가 있으면 인증 Okey
        req.token = token;
        req.user = user;
        next();
    });
}
module.exports = { auth };
d) index.js
const express = require('express');
const app = express();
const port = 5000;
const mongoose = require("mongoose");
const bodyParser = require("body-parser");
const cookieParser = require("cookie-parser");
const config = require("./config/key");
mongoose.connect(config.mongoURI,
).then(() => console.log("MongoDB Connected...")).catch(err => console.error("에러 :", err));
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(cookieParser());
app.use("/api/users", require("./routes/users"));
app.get('/', (req, res) => {
    res.send("Hello World!");
})
app.listen(port, () => {
    console.log(`node and react project port ${port}`);
});
실행 테스트)
$npm run backend
postman 설치 : https://www.postman.com/
Studio 3D for MongoDB 툴로 DB 저장 확인

cloud.mongodb.com 사이트에서 DB 저장 확인

6) git 저장
[GITHUB 사용법] 왕초보를 위한 깃허브사용법 (Git사용법)
.gitignore
https://github.com/braverokmc79/ReactYoutubeCloneSeries/blob/main/.gitignore
2. 클라이언트 Reactjs - Boiler Plate 설정
vscode 확장 패키지 추가
1. - Auto Import - ES6, TS, JSX, TSX
2. - Reactjs code snippets
3. - ESLint
4. - Prettier - Code formatter
1) 리액트 설치
$ mkdir client
$ cd client
$ npx create-react-app .
$ npm start
2) 리액트 라이브러리 설치
antd - UI를 편하게 도와주는 툴 (Bootstrap 비슷) 중국산
axios - GET, PUT, POST, DELETE 등의 메서드로 API 요청
formik  -  form관리 라이브러리
moment   - 시작하기날짜를 손쉽게 다룰수 있는 라이브러리
react-dropzone  - 파일 업로드를 구현하기 위하여 이용한 라이브러리인 
react-icons  -  리액트를 이용하여 빠르게 아이콘을 사용하고 싶을때 
socket.io-client  - 브라우저와 서버 간의 실시간, 양방향, 그리고 이벤트 기반 통신을 가능하게 해주는 라이브러리 
yup - form라이브러리인 Formik과 유효성 검사 라이브러리 Yup
react-router-dom - React-Router 를 통해 Link 태그를 사용하여 화면을 전환을 도와주는 라이브러리
&yarn add antd@^3.24.1 axios formik moment react-dropzone react-icons socket.io-client yup react-router-dom
redux   - 상태 관리 라이브러리
react-redux  - redux react-redux 한셋트
redux-promise  - redux-promise를 사용한다면 Promise를 객체가 통신이 끝난 뒤 그 값을 payload로 응답 - redux-pomise-middleware에 기능
redux-thunk  -  리덕스에서 비동기 작업을 처리를 위해 사용 :함수-  함수를 리턴하면 그 함수를 실행이 끝난 뒤에 값을 액션
redux-form -  form태그의 value들을 관리하거나 validation하는 것을 도와주는 라이브러리
&yarn add redux react-redux redux-promise redux-thunk redux-form
3) 이미지와 같이 디렉토리 및 파일 생성

다음 이미지와 같이 디렉토리 및 파일을 생성한다.

4) CORS 이슈, Proxy 설정
다음을 설치
$ npm install http-proxy-middleware --save $ # or $ yarn add http-proxy-middleware
src/setupProxy.js 파일 생성후
const { createProxyMiddleware } = require('http-proxy-middleware');
//node 서버 포트번호에 맞게 /api 로 시작하는 url 은  포트번호를 5000번으로 변경처리 한다.
module.exports = function (app) {
    app.use(
        '/api',
        createProxyMiddleware({
            target: 'http://localhost:5000',
            changeOrigin: true,
        })
    );
};
nodejs 서버에 concurrently 설치후 package.json scripts 에 다음 내용을 추가한다.
"dev": "concurrently \"npm run backend\" \"npm run start --prefix ../client\""
"scripts": {
  "start": "NODE_ENV=production node index.js",
  "backend": "NODE_ENV=development nodemon index.js",
  "test": "echo \"Error: no test specified\" && exit 1",
  "dev": "concurrently \"npm run backend\" \"npm run start  --prefix ../client\""
},
node 디렉토리에서 실행
$ npm run dev
5) Redux 설정

1. index.js : 리덕스 라이브러리 연동설정(미들웨어, thunk - 동기식처리, devTool, promise 결과값 처리 )
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './components/App';
import { Provider } from 'react-redux';
import { applyMiddleware, legacy_createStore as createStore } from 'redux';
import promiseMiddleware from 'redux-promise';
import ReduxThunk from 'redux-thunk';
import Reducer from './_reducers';
const createStoreWithMiddleware = applyMiddleware(promiseMiddleware, ReduxThunk)(createStore);
const devTools = window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__();
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <Provider store={
    createStoreWithMiddleware(Reducer, devTools)}>
    < App />
  </Provider >
);
2.action
_actions/types.js
export const LOGIN_USER = 'login_user'; export const REGISTER_USER = 'register_user'; export const AUTH_USER = 'auth_user'; export const LOGOUT_USER = 'logout_user';
_actions/user_actions.js
import axios from 'axios';
import { LOGIN_USER, REGISTER_USER, AUTH_USER, LOGOUT_USER } from './types';
import { USER_SERVER } from '../components/Config.js';
/** 유저 로그인 */
export function loginUser(dataTomSubmit) {
    const request = axios.post(`${USER_SERVER}/login`, dataTomSubmit)
        .then((res) => {
            return res.data;
        }).catch((Error) => {
            console.error("에러 :", Error);
        });
    return {
        type: LOGIN_USER,
        payload: request
    }
}
/** 유저 등록 */
export function registerUser(dataTomSubmit) {
    const request = axios.post(`${USER_SERVER}/register`, dataTomSubmit)
        .then(res => res.data);
    return {
        type: REGISTER_USER,
        payload: request
    }
}
/** 유저 권한확인 */
export function auth() {
    const request = axios.post(`${USER_SERVER}/auth`)
        .then(res => res.data);
    return {
        type: AUTH_USER,
        payload: request
    }
}
/** 로그아웃 */
export function logoutUser() {
    const request = axios.get(`${USER_SERVER}/logout`)
        .then(response => response.data);
    return {
        type: LOGOUT_USER,
        payload: request
    }
}
3.reducer
_reducers/index.js
import { combineReducers } from 'redux';
import user from './user_reducer';
const rootReducer = combineReducers({
    user,
});
export default rootReducer;
_reducers/user_reducer.js
import { LOGIN_USER, REGISTER_USER, AUTH_USER, LOGOUT_USER } from '../_actions/types';
export default function user_reducers(state = {}, action) {
    switch (action.type) {
        case LOGIN_USER:
            return { ...state, loginSuccess: action.payload }
        case REGISTER_USER:
            return { ...state, register: action.payload }
        case AUTH_USER:
            return { ...state, userData: action.payload }
        case LOGOUT_USER:
            return { ...state }
        default:
            return state;
    }
}
6) 인증 체크 고차 컴포넌트(HOC, Higher Order Component) 사용

1.Nodejs 서버
//role 1 어드민  role 2 특정 부서 어드민
//rele 0 -> 일반유저 ,  role 0 이 아니면 관리자.
app.post('/api/users/auth', auth, (req, res) => {
 
    //여기 까지 미들웨어를 통과해 왔다는 얘기는 Authentication이 True 라는 말
    res.status(200).json({
        _id: req.user._id,
        isAdmin: req.user.role === 0 ? false : true,
        isAuth: true,
        email: req.user.email,
        name: req.user.name,
        lastname: req.user.lastname,
        role: req.user.role,
        image: req.user.image
    });
 
});
 
2.Action ( _actions/user_actions.js)
export function auth() {
    const request = axios.post('/api/users/auth')
        .then(res => res.data);
 
    return {
        type: AUTH_USER,
        payload: request
    }
}
3.Reducer (_reducers/user_reducer.js)
case AUTH_USER:
            result = { ...state, userData: action.payload }
            break;
4.hoc/auth.js
import { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { auth } from '../_actions/user_actions';
import { useNavigate } from 'react-router-dom';
const Auth = (SpecificComponent, option, adminRoute = null) => {
    /** option 값 */
    //null =>아무나 출입이 가능한 페이지
    //true  =>로그인한 유저만 출입이 가능한 페이지
    //false =>로그인한  유저는 출입 불가능한 페이지
    function AuthenticaitonCheck(props) {
        const dispatch = useDispatch();
        const navigate = useNavigate();
        useEffect(() => {
            dispatch(auth()).then(res => {
                console.log(res);
                if (!res.payload.isAuth) {
                    //1.로그인 하지 않은 상태
                    console.log("1.로그인 하지 않은 상태");
                    if (option) {
                        navigate("/login")
                    }
                } else {
                    //2.로그인 한 상태
                    console.log("2.로그인 하지 않은 상태");
                    if (adminRoute && !res.payload.isAdmin) {
                        navigate("/");
                    } else {
                        if (option === false)
                            navigate("/");
                    }
                }
            })
        }, []);
        return <SpecificComponent />
    }
    return AuthenticaitonCheck;
};
export default Auth;
5.components/App.js
const AuthLandingPage = Auth(LandingPage, null); //null : 아무나 출입이 가능한 페이지
~
function App() {
  const AuthLandingPage = Auth(LandingPage, null); //null  : 아무나 출입이 가능한 페이지
  const AuthLoginPage = Auth(LoginPage, false); //false : 로그인한 유저는 출입불가
  const AuthRegisterPage = Auth(RegisterPage, false);//false :로그인한 유저는 출입불가
  return (
    <BrowserRouter>
      <Suspense fallback={(<div>Loading...</div>)}>
        <NavBar />
        <div style={{ paddingTop: '75px', minHeight: 'calc(100vh - 80px)' }}>
          <Routes>
            <Route path="/" element={<AuthLandingPage />} />
            <Route path="/login" element={<AuthLoginPage />} />
            <Route path="/register" element={<AuthRegisterPage />} />
          </Routes>
        </div>
        <Footer />
      </Suspense>
    </BrowserRouter>
  );
}
export default App;
7) components UI 설정














댓글 ( 4)  
댓글 남기기