인프런 ==> 따라하며 배우는 노드, 리액트 시리즈 - 유튜브 사이트 만들기
유튜브 강의 목록 : 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)
댓글 남기기