React

 

 

 

John Ahn

 

인프런   ==>     라하며 배우는 노드, 리액트 시리즈 - 유튜브 사이트 만들기

 

 

유튜브 강의 목록 :   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 연결 

 

A) [MongoDB] 윈도우 몽고디비 설치 방법

 

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 설정

 

https://github.com/braverokmc79/ReactYoutubeCloneSeries/commit/ac90b760e83a0e407584129586b61e55194ef908#diff-2e1615e91a87829b905fb26d1e8bfdb9070917181be5690cf2154fec0

 

PHRASE

Level 60  라이트

닭 소 보듯 소 닭 보듯 , 서로 마주 보고도 덤덤하게 대하거나, 상대편의 하는 일에 아무런 관심이 없음을 이르는 말.

댓글 ( 4)

댓글 남기기

작성