Youtube Clone

SOO·2022년 8월 29일
0

노마드코더

목록 보기
2/2

정리 참고

What is Node JS

NodeJs = 크롬 V8 자바스크립트 엔진으로 빌드 된 자바스크리트 런타임

브라우저 밖에서 돌아가는 자바스크립트

VSCode는 HTML, CSS, JS, NodeJS로 만들어짐

What is NPM

npm은 자바스크립트 언어를 위한 패키지 매니저입니다.
npm은 기본적으로 nodeJS와 같이 설치됨
npm을 사용하면 다른 누군가가 이미 만들어 높은 패키지를 가져다 쓰기 정말 쉽다.

Your First Node JS Project

Json: 프로그래머가 파일에 정보를 저장하기 위해 만든 방식 중 하나
nodeJS 프로젝트를 만들 때 가장 먼저 만들어야할 파일 -> package.json

git init
git remote add origin url
npm init

package.json이 만들어짐
main이 뭐냐면 프로젝트의 대표 파일이 뭔지 알려주는거임
index.js 만들기
2개의 파일만으로 프로젝트 시작함

Installing Express

우리가 만들고 배포한 package를 다른 사람들이 설치하면 main을 사용할거임
main 지우기

"scripts":{
    "win": "node index.js"  
  },

추가
npm run win 하면
Hello NodeJs!

요점은 콘솔에서 너가 만든 scripts를 쓸 수 있음
우리가 실행하는 모든 명령어는 package.json이 있는 곳에서 실행해야함

npm i 설치하고 싶은 package이름
npm i express

node_modules에는 npm으로 설치한 모든 패키지가 저장될거임
dependencies는 express가 작동되려면 필요한 패키지들을 말함
express가 의존하고 있는 패키지들도 다른것들에 의존하기 때문에 많은 것들이 설치됨

package lock 뭐시기랑 modules 파일 삭제하기

Understanding Dependencies

npm i
npm은 똑똒해서 package.json안에 dependencies봐서 그 안에 있는 모듈들을 알아서 설치한거임 그래서 package.json이 중요하다는거임
우리 프로젝트를 동작시킬 때 필요한 모듈들이 어떤건지 정보를 담고 있어
node_modules 이런거 너무 heavy하니까 친구한테 보낼떄도 그냥 dependencies만 있다면 친구가 프로젝트를 열어서 npm i만하면 필요한 걸 다 설치해줌
project를 공유하고 싶으면 package.json과 index.js만 보내주면 됨

package-lock.json은 너의 패키지들을 안전하게 관리해줌
패키지가 수정 됐는지 해시값으로 체크해주고 정말 안전해

node_modules가 깃허브에 올라가지않게 숨길거임
.gitignore 파일 만들고 /node_modules

npm install을 할 때는 package.json을 꼭 닫고 실행하삼

<depencencies가 프로젝트를 구동시키는데 필요한 모듈이다-!>

The Tower of Babel

nodeJS는 우리가 작성한 자바스크립트를 이해할거임
아직 nodeJS가 이해하지 못하는 최신 JS 코드가 있음
babel is js compiler.
babel은 우리가 작성한 최신 js를 compile 해줄거임
우리가 작성한 최신 JS를 nodeJS가 이해할 수 있는 JS로 바꾸는거임

바벨 설치
nodeJS를 위한 babel을 사용해야함
npm install --save-dev @babel/core
package.json 파일은 그냥 txt 파일이기에 내용을 수정하고 저장해도 큰 문제가 안됨

dependencies는 프로젝트가 작동하기 위해 필요한 것들
devDependencies는 개발자에게 필요한 dependencies ex)nodemon

npm i @babel/preset-env --save-dev

preset은 babel을 위한 엄청 거대한 플러그인
우리가 사용할 건 최신 js를 쓸 수 있는 preset-env

Nodemon

npm i @babel/core @babel/node --save-dev

"dev": "babel-node index.js"를 package.json에

npm run dev

nodemon은 우리가 만든 파일이 수정되는 걸 감시해주는 패키지
파일이 수정되면 nodemon이 알아서 재시작 해줌
"dev": "nodemon --exec babel-node index.js"

babel 설정을 하기 위해 babel.config.json을 만들면 됨
nodeJS로 코드를 실행하지 않고, babel-node 사용
npm run dev를 매번 실행하고 싶지 않아서 nodemon을 설치함

Quiz

1.What is NodeJS? Is JavaScript outside of the browser.
2.JavaScript comes installed inside of the browser.
3.Before NodeJS where could we use JavaScript? On the frontend only.
4.NPM is installed along with NodeJS.
5.What is the purpose of NPM? We use it to share and download JS packages. NPM = Node Package Manager
6.Where do we put information about our NodeJS project?package.json
7.What does the npm init command do? It helps us create a package.json.
8.In the package.json the ‘author’ field is required for the project to run. No
9.In the package.json the ‘description’ field is required for the project to run. No.
10.What does the npm run hi command do? It runs the 'hi' script from package.json
11.npm i and npm install are the same command.
12.How can I install a package named ‘lodash’ from NPM into my project?
npm i lodash
13.What is the node_modules folder? Is where the downloaded packages will be saved.
14.The developer has to create the node_module folder manually. No.
15.What is a “dependency” on package.json? A package that our project needs to run.
16.Developers have to add dependencies manually to package.json. No.
17.After we do npm i express what will happen? 'express' will be downloaded to the node_modules folder.
18.What does npm i do compared to npm i express? "npm i" will download the "dependencies" and "devDependencies" in our package.json
19.Do we need to share the node_modules folder when working on a team? NO
20.Is it a good practice to upload the node_modules folder to Github.NO
21.What happens if we delete the node_modules folder accidentally? We can reinstall the packages using "npm i"
22.What does Babel do? It takes sexy code and turns it into compatible code.
23.What is the difference between devDependencies and dependencies? A devDependency is a package that our developer needs to code.
24.devDependencies and dependencies go to the node_modules folder.
25.How can we move a package from dependencies to devDependencies?
We can just cut + paste. package.json is just text.

Your First Server

1.express에서 express를 import 해와야함
import express from "express";

2.express application 만듦
const app = express();

서버는 항상 켜져있는 컴퓨터, request를 listening 하고 있음
만약 google.com을 간다면 지금 google.com에 request를 보낸거임
내 서버는 listening하고 있음

한마디로 서버는 24시간 내내 온라인에 연결된 컴퓨터
그리고 request를 listening하고 있음
google.com에 접속하기 위해 요청하면 이게 request.
카카오톡에 메시지를 보낸다 이것도 request
내가 서버에게 메시지를 보내면 서버는 나에게 응답을 보냄
서버와 상호작용을 하는 모든 일들. 서버는 그걸 listen
서버는 너의 행동을 listening하고 있어야됨.서버는 너를 기다리고 있음.

3.서버가 사람들이 뭔가를 요청할 때까지 기다리게 해야됨
app.listen()으로 말이지!
listen에 callback 있음

callback은 기본적으로 서버가 시작될 때 작동하는 함수
콜백하기 전에 서버에게 어떤 port를 listening할 지 얘기해 줘야함

port는 컴퓨터 문이나 인터넷 창 같은 것!

보통은 서버를 시작했다면 localhost를 통해서 접속할 수 있음
localhost:4000

const handleListening = () => 
console.log(`Server listening on port http://localhost:${PORT} 🚀`);

app.listen(PORT, handleListening); //외부 접속 listen

ctrl + c 하면 서버 죽음
서버 종료하면 localhost가 접속을 거부

가고자하는 문은 url을 통해 선택됨 이런 모든 문들을 routes라고 부름

Get REQUESTS

/ 서버의 root, 페이지
예를 들어 google.com에 접속한다면, google.com === google.com/
GET은 HTTP method.
HTTP는 우리가 서버와 소통하는 방법, 서버가 서로 소통하는 방법
많은 방법 중 하나지만, 가장 안정적이고, 오래되고, 처음 사용된 방식

유저들이 여기에 접속하려고 할 때, http request를 만들어 보내는 거임
사실 직접 만드는건 아니고, 브라우저가 대신해서 http request를 만들어줘
http request는 말하자만 웹사이트에 접속하고 서버에 정보를 보내는 방법

GET 방법은 GET THIS PAGE(페이지 가져와) 같은 의미
웹사이트에 접속할 때 너가 직접 접속하는게 아니라 웹사이트에게 '이봐~ 나한테 너네 홈페이지 갖다 줘!' 라고 얘기하는 거임
웹사이트가 너(브라우저)에게 오게 하는거
다시 말하면 웹사이트에 접속할 때 브라우저가 너를 어디론가 데려가는게 아님
브라우저는 너를 대신해서 웹사이트를 request하고 페이지를 가져다 주는거

get은 request의 여러 종류 중 하나
request: 유저가 뭔가를 요청하거나, 보내거나, 네게 무슨 행동을 한다, 그게 request
사용자가 원하는 걸 요청할 때, 사용자가 직접 GET REQUEST하는게 아님
브라우저가 get request를 보내는거
브라우저가 홈페이지를 request.

GET / -> 브라우저가 우리 서버의 ROOT로 get request를 보내고 있다

홈(root page)으로 get request를 보낸다면, 반응하는 callback을 추가할거임
app.get("/", () => console.log("Somebody is trying to go home."));

===

const handleHome = () => console.log("Somebody is trying to go home."));
app.get("/", handleHome); //express가 handleHome을 호출하는것

이제 서버는 get requests에 반응할 수 있다는 걸 알아 근데 계속 로딩만 ...

request는 뭐냐면, 브라우저가 '나 이 URL 좀 가져다줘'라고 하는거임
근데 응답은 안해주네,,

client에 브라우저가 있다고 생각하면 되나봄

Responses

  1. Though setting up .get("URL", "GET handler function") will handle a Get request, it will not respond to a GET reqeust.
  2. In order to make a server respond to a user's GET request, you need to modify the EventHandler function into an arrow function. Then, make the response argument .end() or .send() as below:
    const handleHome = (req, res) => {
    return res.send("I still love you.");
    };
  3. The first argument inside GET handler function is usually named "req," it takes in a request object.
  4. The second argument is usually named "res," and it takes in a response object.
  5. res.end() will end the response without returning anything; res.send() will return an input to the user's browser. For this particular example, the user will see a string "I still love you." on their browser when they request for a home ("/") URL page to a server.

express에는 두개의 object가 있음
request, response

request를 종료 시키는 방법들이 있는데 그 중 하나

const handleHome = (req, res) => {
    return res.end();
};

다른 방법

const handleHome = (req, res) => {
    return res.send("I still love you");
};

우린 request를 받게 될거임. 브라우저가 requests를 보낼거고,
root page를 가져달라는 request를 받으면, 우리는 응답 해주는거임

이렇게 브라우저와의 상호작용

Recap

express에서 가장 중요한 부분은, 방금 본 application

-request
-response
-Router; handler로 url 정돈,

브라우저가 request하고 응답하는거임
응답해주지 않으면 브라우저는 계~~속 기다림

request 받으면 응답해야한다!

브라우저는 서버에게 페이지를 request하는거야
그럼 서버가 이 request를 받아들이고, 브라우저에게 그래 홈페이지를 가져가도 조아~ 하고 누군가 홈페이지에 오려고 하면 서버는 handleHome 함수를 실행시키는거임
근데 이 함수는 뭔가를 return 해줘야함. 브라우저가 뭔가를 요청했는데, 서버가 응답하지 않으면 브라우저는 계속 기다릴거임.

challenge

(1) import express from "express": Express 서버를 만들기 위해 express 모듈을 불러옵니다.

(2) const app = express()

express()로 익스프레스 어플리케이션을 생성합니다.
참고 링크
(3)

app.get("/", (req, res) => res.send("<h1>Home</h1>")): / url

로 get 요청(request)을 보내면, 그에 대한 응답으로 "<h1>Home</h1>"을 /로 보내어 HTML을 반환하는 GET 라우트입니다.

(3-1) app.get(path, callback)

GET 메서드는 콜백 함수를 사용하여 지정된 경로로 HTTP GET 요청을 보냅니다.
참고 링크
따라서 각각의 경로(/, /about, /contact, /login)를 지정하여 GET 라우트를 작성하면 됩니다.
(3-2) res.send()

HTML을 반환하기 위해, HTML을 보낼 수 있는 res.send() 응답 메서드를 사용합니다.
참고 링크
따라서 각각의 GET 라우트에 res.send(“<h1>태그</h1>”)
를 사용하여 HTML을 보내면 됩니다.
(4) 동일한 방법으로 나머지 라우트도 작성합니다.

app.get("/about", (req, res) => res.send("<h1>About</h1>"))
app.get("/contact", (req, res) => res.send("<h1>Contact</h1>"))
app.get("/login", (req, res) => res.send("<h1>Login</h1>"))

(5) app.listen(() => "Listening!✅")

app.listen()을 사용하여 express 서버를 시작합니다.
(참고) 보통 포트를 따로 설정하지만, 여기에서는 블루프린트로부터 임의의 미사용 포트를 할당받기 위해 포트를 생략했습니다. 참고 링크

Middlewares

middleware

브라우저가 뭔가를 request하면, 서버는 거기에 응답해준다.
middleware는 requset와 response 사이에 있음.
브라우저가 request한 다음, 그리고 너가 응답하기 전. 그 사이 middleware 존재
모든 middleware는 handler고, handler는 middleware임.
handler 대신 controller라고 할게 즉 모든 controller은 middleware
원래 controller에는 두개의 argument 말고 하나 더 있음
next!
next argument는 몰까? next는 다음 함수를 호출해줘
모든 controller가 middleware가 될 수 있다!

get은 path를 필요로 함, path는 URL이고

handler에는 다수의 handler를 사용할 수 있음
middleware는 request에 응답하지 않음 request를 지속시키는 것일뿐
middleware는 작업을 다음함수에게 넘기는 함수임. 응답하는 함수가 아님
middleware는 필요한 만큼 만들 수 있음

app.use는 global middleware를 만들 수 있게 해줌 = app 전체에 어떤 url에서도 사용할 수 있도록 할 수 있음
모든 route에서 이 함수를 사용하는거임
모든 handler, 구성요소들에는 req,res,next가 있음

app.get("/", logger, handleHome);
middleware는 왼쪽에서 오른쪽으로 실행됨
먼저 route가 있고, 첫 번째 함수가 실행되고, 만약 이 logger가 next()를 호출하면(응답하기시러), 다음은 handleHome이 실행됨
응답하면(return) next()는 실행되지않는거지 ㅇㅇ !
관습적으로 응답을 해주는 마지막 controller에는 next를 안 씀

Recap

package.json은 node.js 관련 정보를 담는 방법
그냥 text기 때문에 뭘 넣어도 상관없음
왜 port를 쓰는가? 서버는 네 컴퓨터 전체를 listen 할 수 없기 때문이다!

서버가 건물이야. 가고자하는 문은 url을 통해 선택됨

express() 첫 번째 인수는 request object로 기본 설정되어있고 두번째는 response object.

컨트롤러는 전달받은 request를 처리하고 response를 전달하기 위한 콜백함수이다.

app.get("/", methodLogger, routerLogger, home);
app.get("/login", methodLogger, routerLogger, login);

굳이 이렇게 써?
app.use(methodLogger, routerLogger);
app.get("/", home);
app.get("/login", login);

순서 중요! 연결은 위에서 아래로 가고,

Challenge

미들웨어 4개를 만드세요.

URL Logger: 이 미들웨어는 방문 중인 URL을 기록(log) 해야 합니다.
Time Logger: 이 미들웨어는 요청(request)의 년, 월, 일을 기록해야 합니다.
Security Logger: 이 미들웨어는 프로토콜이 https이면 secure이라고 기록하고, 그 외의 경우 insecure라고 기록해야 합니다.
Protector Middleware: 이 미들웨어는 사용자가 /protected로 이동하려고 할 경우 이동하지 못하도록 해야 합니다.

import express from "express";

const app = express();

const URLLogger = (req, res, next) => {
  console.log("Path: ", req.path);
  next();
};

const timeLogger = (req, res, next) => {
  const now = new Date();
  console.log(`Time: ${now.getFullYear()}.${now.getMonth()}.${now.getDate()}`);
  next();
};

const protectorLogger = (req, res, next) => {
  if (req.path === "/protected") {
    return res.send("<h1>Forbidden</h1>");
  }
  next();
};

const secureLogger = (req, res, next) => {
  if (req.protocol === "https") {
    console.log("Secure ✅");
  } else {
    console.log("Insecure ❌");
  }
  next();
};

app.use(URLLogger, timeLogger, secureLogger, protectorLogger);
app.get("/", (req, res) => res.send("<h1>Home!</h1>"));
app.get("/protected", (req, res) => res.send("<h1>Protected</h1>"));

app.listen(() => `Listening!✅`);

External Middlewares

morgan은 node.js용 request logger middleware
morgan 함수를 호출하면, 네가 설정한 대로 middleware를 return해줘

app.use(logger("dev")); //dev도 옵션 중 하나 tiny, 등등

const logger = morgan("dev");
app.use(logger);

morgan은 get, path, status code, 이 모든 정보를 가지고 있음

morgan도 next(); 있음

Quiz

  1. A server is a computer listening for requests. Yes.
  2. When we go to google.com is the same as going to google.com/
    YES
  3. When we go to google.com with our browser what kind of request are we making? GET request

4. A GET request happens when the user wants to sends data to the server.
No.

  1. What is the first argument given to our route handlers? The request object
  2. What is the second argument given to our route handlers? The response object
  3. What can we find inside the request object? Information about the request the user is making.
  4. What can we find inside the response object? Functions to respond to the user.
  5. What is a route handler? The function called when the user goes to a URL
  6. What is a middleware? A function that runs between the request and the response to the user.
  7. What is the third argument given to our handlers? The next() function
  8. How can a middleware move on to the next handler? Call the next() function
  9. How can I make a middleware run for every route? app.use(myMiddleware)
  10. How can I make a middleware run only for one route.
    app.get("/", myMiddleware, myHandler)
  11. Middlewares will always call the function next(). No.

What are Routers?

라우터는 너희 컨트롤러와 URL의 관리를 쉽게 해줌
쉽게 말해 미니 어플리케이션을 만들게 해주는거임
라우터는 너희가 작업중인 주제를 기반으로 URL을 그룹화해줌

Making Our Routers

홈에서 바로 갈 수 있는 페이지들을 글로벌 라우터라고 부를 것

const globalRouter = express.Router();

const handleHome = (req, res) => res.send("Home");

globalRouter.get("/", handleHome);

app.use("/", globalRouter);

Cleaning the Code

우리가 만들고 있는 파일들은 하나의 모듈, 그리고 그것들은 독립되어있어
그래서 한 파일안에서도 돌아가는 환경을 코드로 만들어야함
임포트 하기전에는 익스포트를 해야함 왜냐하면 모든 파일이 모듈이니까

export default globalRouter;

우리가 한 건 유저가 여기로와서 /videos로 시작하는 url에 들어가면, express는 비디오 라우터 안에 들어감
그 다음에 비디오 라우터에서 express는 url의 나머지 주소를 찾아
그러니 만약 url이 /videos로 시작하면 비디오 라우더로 들어가는거임
그 다음 비디오 라우터 안에서 /watch를 찾을거임

Exports

라우터는 url의 시작부분일 뿐이야
라우터와 컨트롤러를 섞어서 쓰는건 좋지 않음
컨트롤러는 함수, 라우터는 그 함수를 이용하는 입장이니까

글로벌 라우터는 url을 깔끔하게 하기 위해 쓰는 것 뿐 -> controller 사용 ㄴ

default로 export할 때는 내가 원하는 어떤 이름으로 던지 임포트할 수 있음
-> node.js가 default export를 가지고 이름을 바꿔준다
왜냐면 파일은 한가지 default export밖에 가질 수 없으니까

import userRouter from "./routers/userRouter";
export default userRouter;

export const ~ 는 한 파일이 여러개를 익스포트할 수 있음

export const edit = (req,res) => res.send("Edit User");
export const remove = (req,res) => res.send("Remove User");
import {edit, remove} from "../controllers/userController";

그 대신 실제 이름을 써야함!

new,delete같은거 변수로 못씀

Recap

url을 더 낫고 독립적인 방법으로 관리하기 위해 라우터 사용
라우터는 공통 시작 부분으로 그룹화 하는 것

우리 서버는 url이 어떻게 시작하는지에 따라 라우터를 설정

URL Parameters

url안에 변수를 포함시킬 수 있게 해줌
파라미터는 url안에 변수 넣는 걸 허용해줌
:id
:가 꼭 있어야함

여기서 /upload를 위에 쓴 이유
respond 를 받아올때 /:id 의 변수 중 하나라고 인식하기 때문이다
리퀘스트는 제일 위에 있는 걸 먼저 봄

문자열 패턴을 기반으로 하는 라우트 경로의 몇 가지 예
다음의 라우트 경로는 acd 및 abcd와 일치합니다.

app.get('/ab?cd', function(req, res) {
  res.send('ab?cd');
});

다음의 라우트 경로는 abcd, abbcd 및 abbbcd 등과 일치합니다.

app.get('/ab+cd', function(req, res) {
  res.send('ab+cd');
});

정규식은 문자열로부터 특정 정보를 추출해내는 방법
정규식은 모든 프로그래밍 언어에 존재

Routing
https://expressjs.com/ko/guide/routing.html

정규표현식 테스트 사이트
https://www.regexpal.com
w: 아무문자
\w+: 모든 문자
\d+: 모든 숫자 선택

(nico\w+)
nico로 시작하는 모든 문자를 선택

code challenge

라우터(router) 3개를 만드세요: globalRouter, storyRouter, userRouter
라우터 안에 다음과 같은 GET라우트(route)를 만드세요.
/
/trending
/new
/join
/login
/users
/users/:id
/users/edit-profile
/stories/:id
/stories/:id/edit
/stories/:id/delete
위 라우트를 globalRouter, storyRouter, userRouter에 분배하세요. 반드시 모든 라우트마다 컨트롤러가 있어야 합니다.

해설

(1) 가장 먼저 URL을 디자인합니다.

globalRouter: "/", "/trending", "/new", "/join", "/login"
userRouter: "/users", "/users/:id", "/users/edit-profile"
storyRouter: "/stories/:id", "/stories/:id/edit", "/stories/:id/delete"
알맞은 도메인(/, /users, /stories)을 기반으로 각각의 라우터에 라우트를 나눠주면 됩니다.
(2) src 폴더에 controllers 폴더를 만들고 storiesController와 userController로 파일을 나누어 코드를 작성합니다.: Router

(2-1)

export const 컨트롤러 이름 = (req, res) => res.send("<>설명<>")
각 라우트가 컨트롤러를 가질 수 있도록 각 라우트와 연결할 컨트롤러를 만들고 내보냅니다(export).
자바스크립트의 모든 파일은 독립적이기 때문에 해당 파일 혹은 변수를 다른 파일에서 사용하기 위해서는 export/import를 해줘야 합니다.
(2-2) export const seeStory = (req, res) => res.send(<h1>seeStory: ${req.params.id}</h1>)
3-4. 에서처럼 라우트에 /:id를 포함하여 작성할 경우, :id는 매개변수로 url 안에 변수를 포함시킬 수 있게 됩니다. 따라서 사용자가 /stories/123으로 이동할 경우 화면에는 seeStory: 123이 나옵니다.
참고 링크

(3)

src 폴더에 routers 폴더를 만들고 globalRouter, storyRouter, userRouter로 파일을 나누어 아래와 같이 코드를 작성합니다. 아래 내용은 storyRouter 파일에 대한 해설로 나머지 라우터도 동일한 방식으로 작성하면 됩니다.
(3-1) import express from "express" : 라우터 파일에 express를 불러옵니다.
(3-2)import {seeStory, editStory, deleteStory} from "../controllers/storiesController": import {컨트롤러, ...} from “컨트롤러 위치”로 각 라우트에 연결할 컨트롤러를 불러옵니다.
(3-3) const storyRouter = express.Router(): 스토리 라우터를 만듭니다.
(3-4) 라우터 이름.get("/라우트", 컨트롤러), storyRouter.get("/:id", seeStory)
스토리 라우터의 도메인인 /stories에 해당하는 GET 라우트를 모두 연결하고, 각각의 GET 라우트의 컨트롤러도 연결합니다.
기준이 되는 url(/stories)를 생략하고 뒷부분인 :/id만 적으면 됩니다.
(3-5) export default storyRouter: 스토리 라우터를 export default 해줍니다.

(4)

import storyRouter from "./routers/storyRouter"
export default 변수명으로 내보낸 파일은 import 변수명 from "파일 위치"로 불러올 수 있습니다.
모든 라우터 파일을 서버(index.js)에 불러옵니다.

(5)

app.use("/", globalRouter), app.use("/stories", storyRouter), app.use("/users", userRouter): app.use()`로 각각의 기준이 되는 라우트(url)에 라우터를 연결합니다.

index.js

import express from "express";
import globalRouter from "./routers/globalRouter";
import storyRouter from "./routers/storyRouter";
import userRouter from "./routers/userRouter";

const app = express();

app.use("/", globalRouter);
app.use("/users", userRouter);
app.use("/stories", storyRouter);

// Codesanbox does not need PORT :)
app.listen(() => console.log(`Listening!`));

globalRouter.js

import express from "express";
import { home, trending, newStories } from "../controllers/storiesController";
import { join, login } from "../controllers/usersController";

const globalRouter = express.Router();

globalRouter.get("/", home);
globalRouter.get("/trending", trending);
globalRouter.get("/new", newStories);
globalRouter.get("/join", join);
globalRouter.get("/login", login);

export default globalRouter;

storiesController

export const home = (req, res) => res.send("<h1>home</h1>");
export const trending = (req, res) => res.send("<h1>trending</h1>");
export const newStories = (req, res) => res.send("<h1>newStories</h1>");
export const seeStory = (req, res) =>
  res.send(`<h1>seeStory: ${req.params.id}</h1>`);
export const editStory = (req, res) =>
  res.send(`<h1>editStory: ${req.params.id}</h1>`);
export const deleteStory = (req, res) =>
  res.send(`<h1>deleteStory: ${req.params.id}</h1>`);

userssController

export const join = (req, res) => res.send("<h1>join</h1>");
export const login = (req, res) => res.send("<h1>login</h1>");
export const seeUsers = (req, res) => res.send("<h1>seeUsers</h1>");
export const seeUser = (req, res) =>
  res.send(`<h1>seeUser: ${req.params.id}</h1>`);
export const editProfile = (req, res) => res.send("<h1>editProfile</h1>");

근데
/
users
videos
(use. 할때 )만 입력하면 안됨 왜냐 그 다음에 뭔가를 주거나

app.use("/", globalRouter);
globalRouter.get("/", trending);
/처럼 global에서 설정해야 돌아감

Configuring Pug

pug는 템플릿 엔진
템플릿을 이용해서 뷰를 만드는걸 돕는거

npm i pug

Express에게 html 헬퍼로 pug를 쓰겠다고 말하는거

pug가 우리의 뷰 엔진이 될 것이다!

app.set("view engine", "pug");

Express는 html을 리턴하기 위해 pug를 사용할거야

  1. pug를 설치
  2. pug를 뷰 엔진으로 설정
  3. pug 파일 생성

기본적으로 Express는 여기 있는 views 폴더 안에 있는 파일을 찾아
express는 여기서 view를 찾을거야
이 컨텍스트 안에서는 뷰나 템플릿이나 html이나 같은거임

기본적으로 express가 뷰를 찾는 방법
process.cwd() + '/views'
이건 현재 작업 디렉토리에서 /views라는 디렉토리를 찾아
그 말은 우리가 /views라는 폴더를 만들어야 한다는 걸 의미해
그러니 src로 와서 views라는 새로운 폴더를 만듦

express는 cwd + /views에서 pug 파일을 찾음

긴 루트 파일은 서버를 가동하는 파일의 위치에 따라 결정됨
어디서 노드를 부르고 있는지에 따라 결정됨
어디서 node.js를 부르고, 어디서 서버를 기동하고 있는지 말야
누가 서버를 가동해? package.json

/documents/wetube 폴더에서 package.json이 node.js를 실행하고 있음
그래서 이 디렉토리가 현재 직업 디렉토리가 되는거임 src 폴더가 아니라

현재 작업 디렉토리 = 노드를 시작하는 디렉토리
그리고 우리의 경우 노드를 사용하는 디렉토리는 /wetube

Partials

pug는 html을 이런식으로 작성할 수 있음
pug의 최고 장점은 반복할 필요가 없다
파일명은 같게 해야함!
그리고 파일명은 띄어쓰기가 있으면 안되고 전부 소문자로 해야함
우리는 템플릿에 어느 자바스크립트 코드라도 넣을 수 있음
JS code는 유저의 브라우저로 가지 않음
JS Code는 유저가 보기전에 평범한 텍스트로 변환됨
그게 바로 렌더링 -> pug가 하는 일

doctype html
html(lang="ko")
    head
        title Wetube
    body 
        h1 Watch video!
        footer &copy; #{new Date().getFullYear()} Wetube    

pug는 이 코드를 받아오고 모든걸 체크하고 자바스크립트를 실행함
그리고나서 그걸 유저에게 제공함 그리고 그걸 렌더링이라고 부름
그러니 여길 조사해보면 유저는 이렇게 2022를 얻게 됨
유저는 어떤 자바스크립토도 볼 수 없음

나는 하나의 footer만 가지고 그 footer만 바꾸면 나머지 페이지들도 다 업데이트 되었으면 좋겠음 ㅎ_ㅎ

partial이 pug의 가장 큰 강점

include를 하면 pug가 이 페이지를 렌더링하고, 이 페이지를 평범한 html로 변환하고
그 때 pug는 footer로 가서 그 내용을 가져와서 복붙하는거임

pug가 멋진 이유
1. 깔끔한 html을 작성하도록 해주기 때문
2. 우리의 html에 자바스크립트를 포함시킬 수 있다는 점
3. 우리가 반복하지않아도 되고, 한 파일로 모든 템플릿을 업데이트 할 수 있다는 점

Extending Templates

상속; 일종의 베이스를 만들어줌
extends; 베이스가 되는 파일을 가져다가 그대로 쓴다

extends base.pug

블록; 블록은 템플릿의 창문

base.pug

body 
        block content

edit.pug

extends base.pug

block content
    h1 Edit Video

base.pug를 확장한 후에 우리가 만들어 놓은 블록에 내용을 집어 넣을 수 있음

Variables to Templates

누가 템플릿을 렌더링해 ? 컨트롤러

head
        title #{pageTitle} | Wetube

videoController

export const trending = (req,res) => res.render("home", {pageTitle: "Home"});

render은 2가지 인수를 받음
하나는 view의 이름, 하나는 템플릿에 보낼 변수
home 파일을 렌더링하면 home 파일은 base.pug가 될거고
base.pug는 pateTitle을 찾을거임
우리는 이 render안에 여기에 pageTitle을 전해줌

우리는 home.pug를 렌더링하는데 home.pug는 base.pug를 확장하고
base.pug는 우리가 직접 채우는 content 블록을 가지고 있고,
base.pug는 pateTitle을 가지고 있음 그게 우리가 제공해야하는 변수
변수를 제공하는 방법은 render에 파일명을 쓰고 변수를 쓰는거임

MVP Styles

기본적으로 HTML의 element들을 예쁘게 만들어주는 역할을 함
우리가 쓸 middleware는 MVP.css
우리 HTML 태그에 몇 가지 기본 스타일들을 입혀줌

link(rel="stylesheet" href="https://unpkg.com/mvp.css")

Coding Challenge

컨트롤러의 모든 res.send를 res.render()로 변경하세요.
각 컨트롤러마다 .pug 템플릿을 만드세요.
모든 템플릿은 layout.pug에서 확장되어야 합니다.
footer, head, header는 partials로 만드세요.
각 컨트롤러에 있는 pageTitle 변수를 각각의 템플릿으로 가져와야 합니다.
MVP.css를 사용하여 기본 스타일을 추가하세요.
(Blueprint에 Pug가 이미 설정되어 있습니다.)

head.pug

head
  meta(charset="UTF-8")
  meta(name="viewport", content="width=device-width, initial-scale=1.0")
  meta(http-equiv="X-UA-Compatible", content="ie=edge")
  title=pageTitle
  link(rel="stylesheet", href="https://unpkg.com/mvp.css")

layout.pug

doctype html
html(lang="en")
  include ../partials/head
  body
    include ../partials/header
    main
      block content
    include ../partials/footer

header.pug

header
  h1=pageTitle

storiesController

export const home = (req, res) => res.render("home", {pageTitle: "Home"})
export const trending = (req, res) => res.render("trending", {pageTitle: "Trending"})
export const newStories = (req, res) => res.render("stories", {pageTitle: "Stories"})
export const seeStory = (req, res) =>
  res.render("story", {pageTitle: "Story"})
export const editStory = (req, res) =>
  res.render("editStory", {pageTitle: "Edit Story"})
export const deleteStory = (req, res) =>
  res.render("deleteStory", {pageTitle: "Delete Story"})

join.pug

extends layouts/layout

block content
  h1=pageTitle

quiz

  1. A router helps us divide URLs in small ‘mini-applications’ Yes.
  2. How can I make a router called productsRouter? const productsRouter = express.Router()
  3. How can I use the productsRouter for the /products URL prefix? app.use("/products", productsRouter)
    4. How can I create a get router inside of productsRouter for the /products URL? productsRouter.get("/", fn)
  4. If I have export default B on A.js I can import B using
    import Z from "./A.js"
  5. If I have export B on A.js I can import B using. import {B} from "./A.js"
  6. A .js file can have more than one default export. No.
  7. A .js file can have many export const .... Yes.
  8. On this URL: /products/:category/:id what are the parameters? id, category
  9. How can I make my URL have a ‘id’ parameter? /products/:id
  10. On my controller, how can I access id on this URL /products/:id
    req.params.id
  11. To return HTML to the user we need to use templates, it's the only option.
    No.
  12. Pug is the only template engine we can use with Express. No.
  13. A template engine takes sexy code and turns it into normal HTML. Yes.
    15. What is the name of the folder where Express looks for the templates by default? '/views'

Conditionals

h1=pageTitle
h1은 오직 pageTitle의 값만 갖는거

variable과 text를 섞으면
title #{pageTitle} | Wetube

if fakeUser.loggedIn 
                        li
                            a(href="/logout") Log out
                    else
                        li
                            a(href="/login") Login

Iteration

Elements의 list를 보여주는 것

어떻게 lists를 보여줄까?
template에는 array인 variable이 있어야함
each 적고, 보여주고 싶은 variable 이름을 적어주면 됨

export const trending = (req, res) => {
  const videos = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
  return res.render("home", { pageTitle: "Home",videos});
};
each video in videos 
            li=video

video array 안의 각 element에 대해서, list item을 만들어서
video를 그 안에 넣어주고 있어
video는 array안의 각 item을 가리킴
videos는 Controller 부분의 videos와 같아야함

댓글이나 비디오, 다른 무엇이든 리스트를 보여주고 싶을 때 iteration을 쓰는거임

array안에 아무것도 없는경우? 이게 바로 else조건이 있는 이유

else 
        li Sorry nothing found.    

Mixins

mixin은 partial이긴 한데 데이터를 받을 수 있는 partial을 말해
mixin은 똑똑한 partial이다!
일종의 미리 만들어진 HTML block이라 볼 수 있다.

mixin이 받게될 데이터 적기

mixins/video.pug

mixin video(info)
    div
        h4=info.title
        ul
            li #{info.rating}/5.
            li #{info.comments} comments.
            li Posted #{info.createdAt}.
            li #{info.views} views.

info는 mixin이 받게 될 데이터를 지칭함

home.pug

each potato in videos 
        +video(potato)

videos안의 각각의 potato에 대해서, video라는 mixin을 호출해서 potato라는 정보 객체를 보내고 있는거임
video는 argument가 필요해 (potato)

include mixins/video라고 해주면 됨

다른 데이터를 포함하지만 같은 형태의 HTML을 보여주는 것. 이것이 MIXIN의 역할임.

Array Database

href, class나 id 같은거에 ${} 못씀

방법 1

 a(href="/videos/" + video.id)=video.title

방법 2

a(href=`/videos/${video.id}`)=video.title

url에 id를 가지고 있으니까 우린 req.params을 출력해볼 수 있음

const id = req.params.id;랑
const { id } = req.params;

단복수 구분 ternary operator

 #{video.views ===1 ? "view" : "views" }

absolute vs relative

a(href="/edit") Edit Video &rarr;

href의 앞머리 부분에 /를 넣으면, 너가 어디있든지 상관 없이 root 경로 + /edit으로 가게 되는거임
ex) localhost:4000/edit

a(href="edit") Edit Video &rarr;

relative url, /가 없기 때문에 브라우저는 edit을 붙인 경로로 우리를 보내줌
마지막 경로에서 해당 url로 이동
ex)localhost:4000/profile/edit-profile/edit

Edit Video

GET request vs POST request

기본값으로 method는 GET임. 그냥 데이터를 받는 것.
구글이나 네이버에 검색할 때 검색어가 주소창에 포함

POST는 파일을 보내거나, database에 이는 값을 바꾸는ㄴ 뭔가를 보낼 때 사용함
로그인할 때도 post 사용

getEdit은 form을 화면에 보여주는 녀석
postEdit은 변경사항을 저장해주는 녀석

method; form과 back end 사이의 정보 전송에 관한 방식
만약 get method를 사용하면, 그 form에 있는 정보가 url에 들어가게 됨
검색 페이지를 만들 때 우린 get을 사용할 거고, 그 검색어는 url 안에 들어갈거임

'이 데이터는 back end로 들어가서 몰 할까?'
'이 데이터가 나의 db 상태를 수정할건가?'
즉 너의 db를 변경할 data로 뭔가를 하는가?

action이 있으면 데이터들을 특정 url로 보낼 수 있음

videoRouter.get("/:id(\\d+)/edit", getEdit);
videoRouter.post("/:id(\\d+)/edit", postEdit);
videoRouter.route("/:id(\\d+)/edit").get(getEdit).post(postEdit);

이렇게 짧게 쓸 수 있음

app.use(express.urlencoded({ extended: true })); -> form의 body 이해
middleware를 route를 사용하기 전에 사용해야 됨
app.use("/", globalRouter);

form 에 대한 data를 보낼때 input의 name 으로 정보를 받기 때문에 꼭 form엔 name을 정해줘야한다!

get - 접근
post - 전송
redirect - 다시보내다
parameter - 매개변수
express는 form으로 보낸 데이터를 읽지못함

More Practice part One

가장 먼저 해야할 건 바로 get에 대한 걸 만드는거
왜냐면 user가 form을 볼 수 있어야 하니까

컨트롤러 만들고, router 만들고!

export const getUpload = (req,res) => {
  return res.render("upload");
};

export const postUplodad = (req, res) => {
  // here we will add a video to the videos array.
  return res.redirect("/");
};
videoRouter.get("/upload", getUpload);
videoRouter.post("/upload", postUplodad);

랑 동일한 코드 한 줄

videoRouter.route("/upload").get(getUpload).post(postUpload);

어떻게하면 input에서 data를 받을 수 있을까? req.body를 사용함으로써 ~
모든 input들은 이름이 있어야함

MongoDB & mongoose

mongoDB: document based
mongoose: node.js와 mongoDB를 이어주는 다리가 되기 때문!

mongoose.connect("mongodb://127.0.0.1:27017/nameofyourdb")

db.on("error", (error) => console.log("DB Error", error));

on은 여러번 발생시킬 수 있음

once는 오로지 한 번만 발생

Video Model

import mongoose from "mongoose";

const videoSchema = new mongoose.Schema({
    title:String,
    description: String,
    createdAt: Date,
    hashtags: [{ type: String }],
});

비디오별 데이터가 가진 형식을 정의
hashtags: [{ type: String }]
array인데 string type인것

title: {type: String} 도 ㄱㅊ

const Video = mongoose.model("Video", videoSchema);
export default Video;

모델 만들기 - model의 이름과 데이터의 형태인 schema로 구성하면 됨

import "./models/Video"

coding challenge

import mongoose from "mongoose";

// Create a Movie Model here.
const MovieSchema = mongoose.Schema({
  title: {
    type: String,
    required: true
  },
  summary: {
    type: String,
    required: true
  },
  year: {
    type: Number,
    required: true
  },
  rating: {
    type: Number,
    required: true
  },
  genres: [
    {
      type: String,
      required: true
    }
  ]
});

const Movie = mongoose.model("Movie", MovieSchema);

export default Movie;

const Movie = mongoose.model("Movie", MovieSchema)

mongoose.model("모델이 사용하는 컬렉션의 단수(singular) 이름", 스키마 이름)을 사용하여 MovieSchema를 Movie 모델로 컴파일

Our First Query

callback이란 무언가가 발생하고 난 다음 호출되는 function을 말함
JS에서 기다림을 표현하는 하나의 방법이라 생각하면 편함
DB는 JS 밖에 존재하기 때문에 약간의 기다림이 필요

callback함수를 쓰면 아무것도 return 되지 않아야됨

export const home = (req, res) => {
  Video.find({}, (error, videos) => {
    console.log("errors", error);
    console.log("videos", videos);
  });
  console.log("hello);
  return res.render("home", { pageTitle: "Home", videos: [] });
};

터미널
hello
GET / 304
errors null
videos []

export const home = (req, res) => {
	console.log("Start");
  Video.find({}, (error, videos) => {
    console.log("Finished");
    return res.render("home", { pageTitle: "Home", videos});
  });
};

터미널
Start
Finished
GET/304 ~

console.log("hello")하고 render 과정을 거쳐야 logger를 얻게 되는거임
callback함수를 쓰면 특정 코드를 마지막에 실행되게 하느 ㄴ거임

Async Await

promise는 callback의 최신 버전

export const home = async(req, res) => {
  const videos = await Video.find({})
  return res.render("home", { pageTitle: "Home", videos});
};

find는 네가 callback을 필요로 하지 않는다는걸 알게 되는거임
await을 보면 아 ~ 자바스크립트가 무엇인가를 기다리는구나!

Returns and Renders

function안에서 return은 기능이 없고 그냥 function을 마무리할 뿐임
1. return의 역할 : 본질적인 return의 역할보다는 function을 마무리짓는 역할로 사용되고 있음.

  • 이러한 경우 return이 없어도 정상적으로 동작하지만 실수를 방지하기 위해 return을 사용
  1. render한 것은 다시 render할 수 없음
  • redirect(), sendStatus(), end() 등등 포함 (express에서 오류 발생)

Creating a Video part One

schema는 우리 비디오의 형태를 정의해줌

await하는 이유는 데이터를 db에 전송하는데 시간이 걸리기 때문임

MongoDB의 collection이름이 Video가 아닌 videos인 이유
Mongoose는 자동으로 모델을 찾고, 해당 모델의 이름을 따서 소문자+뒤에 s(복수형)을 붙여 컬렉션을 생성합니다.
Tank 모델은 -> 컬렉션에 저장될 때, tanks로 저장됩니다.

Document.prototype.save()
https://mongoosejs.com/docs/api.html#document_Document-save

Model.create()
하나 이상의 문서를 데이터베이스에 저장하기 위한 손쉬운 방법입니다.
MyModel.create(docs)는 문서의 모든 문서에 대해 새로운 MyModel(doc).save()를 수행합니다.
create()을 하게 되면 save()를 생략할 수 있습니다.
create()이 다음 미들웨어인 save()를 트리거하기 때문입니다.
https://mongoosejs.com/docs/api.html#model_Model.create

Collection: Document들을 담고 있는 묶음

Exceptions and Validation

mongoose에게 구체적으로 줄 수록 굿

  1. try/catch를 통한 에러처리, 에러메세지 전달
  2. Schema parameter : default -> meta에 대해 신경쓸 필요 없음
  3. Schema CreatedAt에 Date.now()가 아닌 Date.now를 적는 이유 : 생성될때만 실행되도록

createdAt: {type: Date, required: true}
required: true 를 설정함으로써 필수로 작성해야하는 칸을 설정해줄수 있다

const videoSchema = new mongoose.Schema({
  title: { type: String, required: true },
  description: { type: String, required: true },
  createdAt: { type: Date, required: true, default: Date.now },
  hashtags: [{ type: String }],
  meta: {
    views: { type: Number, default: 0, required: true },
    rating: { type: Number, default: 0, required: true },
  },
});

Quiz

  1. What is a Document Database?
    A DB where we store data on JSON-like documents.

  2. What is the job of Mongoose?
    We use Mongoose to talk to Mongo from JS

  3. If we use Mongoose we don’t need MongoDB.
    False

  4. What is a Schema?
    The shape of the data that will go to the DB

  5. On the Schema we write the data we want to put on the DB.
    No.

  6. What is a callback in JS?
    A function to be executed after another function has finished executing

  7. To use await we must first make the function async
    True

  8. With try/catch, when is the ‘catch’ code executed?
    When there is an error on the 'try' block

  9. We must always call res.render with return to render a template.
    No, return is not required.

  10. After calling res.render we can call res.send and it will work.
    False

11. To save a new video to the database we use new Video.
No.

12.What is the difference between new Video and Video.create?
new Video creates a JS object. Video.create saves a new video to the DB.

More Schema

minlength , maxlength -> form과 database측면 둘다 해줘야한다

video Detail

정규표현식
http://regexpal.com

몽고DB Document
몽고DB는 ObjectID를 24바이트 16진 문자열 표현으로 반환한다.
https://mongodb.github.io/node-mongodb-native/api-bson-generated/objectid.html
https://docs.mongodb.com/manual/reference/method/ObjectId/

십육진법 (Hexadecimal)
십육진법은 십육을 밑으로 하는 기수법이다. 보통 0부터 9까지의 수와 A에서 F까지의 로마 문자를 사용하고, 이때 대소문자는 구별하지 않는다.
Hexadecimal: 0~9까지의 숫자와 A-F까지의 알파벳이 조합된 string

findOne
해당 조건과 일치하는 document를 찾는다.
_id로 찾는 경우에는 findById()를 사용할 것을 권장
findById(id)는 거의* findOne({ _id: id })과 동일합니다.
https://mongoosejs.com/docs/api.html#model_Model.findOne

findById
https://mongoosejs.com/docs/api.html#model_Model.findById

Edit Video

!video
video가 없을 때

  video.title = title;
  video.description = description;
  video.hashtags = hashtags
    .split(",")
    .map((word) => (word.startsWith("#") ? word : `#${word}`));
  await video.save();

이걸 이렇게 변경

 await Video.findByIdAndUpdate(id, {
    title,
    description,
    hashtags: hashtags
      .split(",")
      .map((word) => (word.startsWith("#") ? word : `#${word}`)),
  });
  return res.redirect(`/videos/${id}`);

Video는 우리가 쓰는 Model

postedit에서
exist()는 filter를 받음
video objext를받는 대신에 true 혹은 false를 받는거
왜냐면 영상 존재 여부만 파악하면 되니까

getEdit에서는 video obejct가 꼭 필요함
object를 edit template로 보내줘야 하거든

findById는 꼭 id를 인자로 받음
우리가 영상을 생성하거나 업데이트 하기 전에 작동할 function의 필요성에 대한 이해

Middleware

middleware는 무조건 model이 생성되기 전에 만들어야한다는 것

Statics

  1. findByIdAndUpdate()에서는 save 훅업이 발생하지 않음 => 다른 방법을 알아보자
  2. Video.js에 function을 만들어서 관리하기 => 이것도 괜찮음 근데 다른것도 알아보자
  3. static을 사용하면 import 없이도 Model.function()형태로 사용이 가능함 => super cool
// Assign a function to the "statics" object of our animalSchema
animalSchema.statics.findByName = function(name) {
return this.find({ name: new RegExp(name, 'i') });
};
// Or, equivalently, you can call `animalSchema.static()`.
animalSchema.static('findByBreed', function(breed) { return this.find({ breed }); });

https://mongoosejs.com/docs/guide.html#statics

Delete Video

Model.findOneAndDelete()
Model.findOneAndRemove()

이 둘은 정말 약간의 차이가 있는데 대부분의 상황에서 타당한 이유가 없는 한 delete를 사용하라고 되어 있음.

https://www.zerocho.com/category/MongoDB/post/579ecb1fc097d015000404dd

여기 글을 읽어보니 몽고 db는 롤백이 안되서 remove를 하면 다시 되돌릴 수 없기에 remove보다 delete를 사용하라고 권장하는듯

이번 강의 핵심 코드
export const deleteVideo = async (req, res) => {
const { id } = req.params;
await Video.findByIdAndDelete(id);
return res.redirect("/");
};

Model.find()
documents를 찾습니다. (findOne과 다르게 전체 document를 찾습니다.)
Mongoose는 명령이 전송되기 전에 모델의 스키마와 일치하도록 필터를 캐스팅합니다.
https://mongoosejs.com/docs/api.html#model_Model.find

정규표현식
https://www.regexpal.com

몽고DB regex ($regex)
몽고DB에서 정규표현식을 사용하기 위해 사용하는 키워드
쿼리의 패턴 일치 문자열에 대한 정규식 기능을 제공합니다.
https://docs.mongodb.com/manual/reference/operator/query/regex

RegExp mdn
RegExp 생성자는 패턴을 사용해 텍스트를 판별할 때 사용합니다.
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/RegExp

RegExp 사용 방법
RegExp 객체는 리터럴 표기법과 생성자로써 생성할 수 있습니다.
리터럴 표기법의 매개변수는 두 빗금으로 감싸야 하며 따옴표를 사용하지 않습니다.
생성자 함수의 매개변수는 빗금으로 감싸지 않으나 따옴표를 사용합니다.

/ab+c/i 를 아래 RegExp 생성자를 이용해서 만들 수 있습니다.
new RegExp(/ab+c/, 'i') // 리터럴 표기법
new RegExp('ab+c', 'i') // 생성자 함수

videos title을 검색할때 keyword가 포함된것들을 regex operator를 통해 검색해 줄 수 있다.
(regex = regular expression의 약자)
const { keyword } = req.query;
.
.
regex:newRegExp(keyword,"i")>keyword가포함된것들을검색.regex: new RegExp(keyword, "i") -> keyword가 포함된 것들을 검색.regex: new RegExp(^${keyword}, "i") -> keyword로 시작되는 것들을 검색.
regex:newRegExp(regex: new RegExp(`{keyword}$`, "i") -> keyword로 끝나는 것들을 검색.
(여기서 "i" = Welcome,welcome 둘다 같게 해주는것 즉 lowercase,uppercase의 구분을 없게 해주는것)
( mongoose가 아닌 mongoDB가 해주는 기능이다)

Create Account

비밀번호 털렸다고? 암호화. 해시함수. 5분 설명 영상
https://www.youtube.com/watch?v=67UwxR3ts2E

해싱은 일방향 함수
같은 입력값으로는 항상 같은 해시값이 나옴 -> 결정적 함수

remove() 명령어 실행이 안될 때
db.users.remove()는 deprecated되었기 때문에
db.users.deleteMany({})로 지우시면 됩니다.

해시함수 테스트
https://emn178.github.io/online-tools/sha256.html

bcrypt 설치
암호를 해시하는 데 도움이 되는 라이브러리입니다.
npm i bcrypt
https://www.npmjs.com/package/bcrypt

해커가 해싱된 password를 가지고 할 수 있는 공격이 있는데 바로 rainbow table! (유툽참고)
rainbow table 공격을 bcrypt가 막아줄거임!

Schema.prototype.pre()
https://mongoosejs.com/docs/api.html#schema_Schema-pre

Form Validation

$or

$or 연산자는 둘 이상의 조건에 대해 논리적 OR 연산을 수행하고 조건 중 하나 이상을 충족하는 문서를 선택합니다.

// 예시

db.inventory.find( { $or: [ { quantity: { $lt: 20 } }, { price: 10 } ] } )

https://docs.mongodb.com/manual/reference/operator/query/or/#mongodb-query-op.-or

어떤 조건이 하나라도 true이면 해당 데이터 찾아올거임

Status Codes

상태코드

  • 200(OK): 서버가 요청을 제대로 처리했다는 뜻이다. 이는 주로 서버가 요청한 페이지를 제공했다는 의미로 쓰인다.
  • 400(Bad Request): 서버가 요청의 구문을 인식하지 못할 때 발생한다. 클라이언트 측에서 문제가 있을 때 주로 발생한다.
  • 404(Not Found): 서버가 요청한 페이지를 찾을 수 없을 때 발생한다. 서버에 존재하지 않는 페이지에 대한 요청이 있을 경우 서버는 이 코드를 제공한다.
    https://ko.wikipedia.org/wiki/HTTP_%EC%83%81%ED%83%9C_%EC%BD%94%EB%93%9C

res.status(code)
response에 대한 HTTP 상태를 설정합니다. (status를 설정한다.)
https://expressjs.com/ko/api.html#res.status
https://nodejs.org/api/http.html#http_response_statuscode

render할 때 어떻게 상태 코드를 보낼 수 있나?
res.render()사이에 status(400)을 추가하면 됨

브라우저에게 url을 기록하지 말라고 보내주는건 상태 코드 404을 보내주는거임

Login part

bcrypt를 이용해서 비밀번호 비교

password: 유저가 입력한 비밀번호
user.passwordHash: DB에 해시화되서 저장된 비밀번호

const match = await bcrypt.compare(password, user.passwordHash);

https://www.npmjs.com/package/bcrypt

Sessions and Cookies

쿠키를 이해하기 위해서는 우선 세션에 대해 알아야함
세션: 백엔드와 브라우저 간에 어떤 활동을 했는지 기억하는 걸 말함, 브라우저와 백엔드 사이의 memory, history 같은거
이게 작동하려면 백엔드와 브라우저가 서로에 대한 정보를 가지고 있어야 됨

stateless: 요청을 받고 처리를 끝내면, 서버에서는 누가 요청을 보냈는지 잊어버리게 되고 브라우저도 마찬가지로 잊어버리게 됨. 서버가 더이상 필요 없으니까.
한 번 연결이 되었다가 끝나는거임. 이 둘 사이 연결에 state가 없는거임

유저가 로그인 할 때 유저한테 어떤 텍스트를 준다
유저한테는 요청을 보낼 때마다 그 텍스트를 같이 보내달라고 할거임

  1. 브라우져가 서버에 접근
  2. 서버가 브라우져에게 Cookie 준다.
  3. 브라우져가 서버에 다시 접근할 때 2.에서 받은 Cookie를 함께 건냄.
  4. 서버는 Cookie를 통해 브라우져를 구분 할 수 있다.
  • 기본적으로 서버와 유저의 연결은 stateless한 성질을 띈다.(wifi가 쭉 연결되어 있는것과 다르게)
  • 이러한 연결 특성으로 인해 매번 연결시 유저는 새로이 서버에 자신을 확인 받아야 함
  • 만약 증표(증거)가 있다면 다시 연결시 유저에 대한 확인이 쉬워짐
  • 쿠기가 증표 역활을 함. 유저는 서버 연결시 서버에게 증표를 건네받음(쿠키는 유저가 보관)
  • 서버는 session으로 해당 증표를 가진 유저의 기록을 저장해둠
  • 유저가 증표(쿠키)를 가지고 오면 서버는 저장되어 있는 session을 통해 유저를 쉽게 확인

백엔드의 메모리에 세션을 저장할 수 있는 DB가 생김
백엔드의 각 세션들은 ID를 가지고 있었고, 이 ID를 브라우저에한테 보냈음
그러면 브라우저가 요청을 보낼 때마다 그 ID를 같이 보내줘서
브레이브 브라우저와 일치하는 세션이 뭔지 알 수 있고 어떤 세션이 크롬과 일치하는지도 알 수 있었음

서로 다른 브라우저에서는 서로 다른 쿠키를 가지고 있음, 서로 다른 세션 id를 가지고 있음
세션과 세션 id는 브라우저를 기억하는 방식 중 하나임
백엔드에 요청을 보낼 때마다 이 id를 같이 보내줘야함 그러면 백엔드가 기억할 수 있음
그리고 이 세션 id를 가지고 있으면 세션 object에 정보를 추가할 수 있어
브라우저마다 서로 다른 세션 id를 가진 텍스트를 보내고 있음

우리가 줬던 id를 백엔드에 요청을 보낼 때마다 같이 보내고 있음
그러면 백엔드의 세션 DB에서 이 ID를 가진 세션을 찾겠지

브라우저한테 우리 백엔드 URL을 방문할 때마다 보여줘야하는 ID카드를 주는것

즉, 브라우져에서 서버에 로그인 요청을 해서 로그인이 되면 서버는 세션id를 response해주고
브라우져는 쿠키스토리지에 그 세션id를 보관하고 있다가 이후 다시 서버에 방문할 시에는 그 세션
id만 보여주면 자동으로 로그인되게 해줘서 계속 로그인할 수고를 덜어준다는 것이겠군요.

Logged In User

res.locals

request 범위가 지정된 response 로컬 변수를 포함하는 객체이므로 request, response 주기동안 렌더링된 view에서만 사용할 수 있습니다.
(Pug나 EJS같은 템플릿 엔진에서 사용 가능하다는 의미)
이 속성은 request path, 인증된 사용자, 사용자 설정 등과 같은 request level의 정보를 노출하는 데 유용합니다.

// 사용 예시
app.use(function (req, res, next) {
res.locals.user = req.user
res.locals.authenticated = !req.user.anonymous
next()
})

https://expressjs.com/ko/api.html#res.locals

middleware.js 파일 내에서
res.locals.loggedInUser = req.session.user;
위 코드가 실행되면 res.locals.loggedInUser에 req.session.user 객체가 할당돼요. 그런데 req.session.user 객체 안에는 회원가입할 때 입력했던 정보들이 저장되어 있죠(email,username,password,name,location) 그래서 loggedInUser : loggedInUser: {
_id: '임의로 생성된 값',
email: '회원 가입할 때 입력한 이메일 주소',
username: '회원 가입할 때 입력한 유저이름',
password: '암호화된 비밀번호',
name: '회원 가입할 때 입력한 이름',
location: '회원 가입할 때 입력한 주소',
__v: 0
}

이렇게 됩니다~

Recap

middleware가 뭐하나면 브라우저가 우리의 backend와 상호작용할 때마다
여기 보이는 session이라는 middleware가 브라우저에 cookie를 전송해

쿠키는 backend가 너의 브라우저에 주는 정보, cookie에는 정해진 규칙이 있기 때문에, 너가 매번 backend에 request를 할 때 브라우저는 알아서 그 request에 cookie를 덧붙이게 됨
브라우저는 cookie로 뭘 할지, 어디에 넣을지 모든걸 알고 있음

브라우저는 우리가 매번 backend localhost에 있는 url로 request를 보낼 때마다
cookie가 request랑 같이 전송된다는 것도 알고 있음

쿠키에는 어떤 정보도 넣을 수 있음

우리가 쿠키에 넣을 것은 session ID
왜냐면 브라우저와 backend와의 연결이 평생 보장된 것은 없기 때문임

http는 stateless야
우리가 homepage를 들어가면 connection이 열리고, render가 끝나면 connection이 끊어짐

쿠키: 단지 정보를 주고 받는 방법, 쿠키를 받고 보내는 과정에서 사용자는 아무것도 안해도 됨
별개의 코드를 작성할 필요도 없음 우린 http 표준을 따르고 있으니까 !

세션:

백엔드는 생성된 모든 session ID를 관리하는 곳
Session store는 우리가 session을 저장하는 곳

locals는 너가 뭐든 할 수 있는 object
locals안에 넣기만 하면 pug에서 쓸 수 있음
사용자를 로그인시키는 것은 우리가 req.session object안에 정보를 넣는것!

code challenge (21)

(1) 미들웨어

res.locals.loggedIn = Boolean(req.session.loggedIn)
세션에 로그인한 것이 참이면 로컬 로그인도 참이 되도록 작성합니다.
res.locals.loggedInUser = req.session.user
퍼그 템플릿에서 사용할 수 있도록, 세션의 유저 정보를 로컬의 로그인된 유저에 추가합니다.
(2) User 모델

import bcrypt from "bcrypt"
bcrypt는 비밀번호 해싱을 도와줍니다. 유저 모델 파일에 bcrypt를 불러옵니다.
유저 스키마를 작성할 때 username을 고유하게 하기 위해 unique: true를 작성하면 됩니다.
참고 링크
UserSchema.pre("save", async function () {this.password = await bcrypt.hash(this.password, 5);})
위의 코드처럼 작성하면 비밀번호를 5번 해싱 하여 DB에 저장합니다.
bcrypt를 사용하여 비밀번호를 해싱 하면, 해싱 된 비밀번호가 DB에 저장되기 때문에 보안을 강화할 수 있습니다.
(3) 컨트롤러 (3-1) home 컨트롤러

세션에 로그인이 되어 있으면(req.session.loggedIn) 유저의 프로필을 보이게 하고, 로그아웃 상태면 로그인 페이지로 redirect 하면 됩니다.
따라서 home.pug에 loggedInUser를 사용하여 로그인한 사용자의 name과 username을 표시하면 됩니다.
(3-2) postJoin 컨트롤러

const { name, username, password, password2 } = req.body
먼저 req.body로 사용자가 form에 입력한 데이터를 받아옵니다.
가입 시 입력한 두 비밀번호가 일치하지 않거나 username이 이미 존재할 경우 에러를 표시해야 합니다.
res.status(400).render("join", { pageTitle, errorMessage: "Password confirmation does not match." }); }
password !== password2인 경우, 비밀번호가 일치하지 않는다는 오류를 보내기 위해 join 페이지를 렌더 할 때 status(400)을 함께 보내어 오류가 발생했음을 알리고 에러 메시지를 함께 보내면 됩니다.

링크텍스트
링크텍스트
링크텍스트

const exists = await User.exists({ username })
사용자가 입력한 username이 이미 있는 username인지 단순히 확인하기 위해서 Model.exists() 메서드를 사용합니다.
참고 링크

이미 사용하고 있는 username이라면 join 페이지를 렌더 할 때 status(400)을 함께 보내어 오류가 발생했음을 알리고 에러 메시지를 함께 보내면 됩니다.
User.create({name,username,password})
이미 사용하고 있는 username이 아니라면 Model.create() 메서드를 사용하여 새로운 User를 생성하고, /login 으로 redirect 하면 됩니다. 이때 혹시 모를 오류를 대비하여 try catch 구문을 사용합니다.
참고 링크
참고 링크
(3-3) postLogin

const { username, password } = req.body
먼저 req.body로 사용자가 form에 입력한 데이터를 받아옵니다.
const user = await User.findOne({ username })
username으로 유저를 찾아서 로그인을 구현하기 위해 Model.findOne() 메서드를 사용합니다.
유저가 없으면(!user) status(400)과 함께 에러 메시지를 login 페이지로 렌더 합니다.
const ok = await bcrypt.compare(password, user.password)
비밀번호가 일치하는지 확인하기 위해 bcrypt를 사용합니다.
비밀번호가 틀리면(!ok) status(400)과 함께 에러 메시지를 login 페이지로 렌더 합니다.
req.session.loggedIn = true
문제가 없으면, 세션 로그인이 참이 되게 합니다.
req.session.user = user
그리고 세션에 유저 정보를 넣고 / 로 redirect하면 됩니다.
결론
MongoDB와 session을 사용하여 로그인 인증 시스템 구축을 연습할 수 있는 챌린지였습니다.

MongoStore

session id는 쿠키에 저장하지만, 데이터 자체는 서버에 저장되는거임.
서버에 저장되는 default session storage는 MemoryStore이고 실제 사용하기 위해 있는 건 아님

connect-mongo는 세션을 MongoDB에 저장할거임
세션은 브라우저가 우리의 backend를 방문할 때 만들어짐

 store: MongoStore.create({ mongoUrl: "mongodb://127.0.0.1:27017/wetube" }),

내가 새로고침을 해도 계속 로그인 되어있음
왜냐면 더 이상 로그인 정보가 서버에 있지 않거든

Uninitialized Sessions

브라우저가 backend에 접근할 때마다 DB에 쿠키와 sessison을 하나씩 저장하고 있음
로그인한 사용자의 session만 저장하는게 조음
나는 내가 기억하고 싶은 사용자에 대해서만 쿠키를 주고 시픔
saveUninitialized; 세션이 새로 만들어지고 수정된 적 없을 때

새로운 세션이 있는데, 수정된 적 없으면 초기화되지않은것
그럼 세션은 어디서 수정? 한 곳에서만 수정 가능

  req.session.loggedIn = true;
  req.session.user = user;

이 두줄이 우리가 실제로 세션을 초기화하는 부분
세션을 수정할 때만 세션을 db에 저장하고 쿠키를 넘겨주는 것
backend가 로그인한 사용자에게만 쿠키를 주도록 설정

  • resave : 모든 request마다 세션의 변경사항이 있든 없든 세션을 다시 저장한다.
  • true:
  • 스토어에서 세션 만료일자를 업데이트 해주는 기능이 따로 없으면 true로 설정하여 매 request마다 세션을 업데이트 해주게 한다.
  • false:
  • 변경사항이 없음에도 세션을 저장하면 비효율적이므로 동작 효율을 높이기 위해 사용한다.
  • 각각 다른 변경사항을 요구하는 두 가지 request를 동시에 처리할때 세션을 저장하는 과정에서 충돌이 발생할 수 있는데 이를 방지하기위해 사용한다.
  • saveUninitialized : uninitialized 상태인 세션을 저장한다. 여기서 uninitialized 상태인 세션이란 request 때 생성된 이후로 아무런 작업이 가해지지않는 초기상태의 세션을 말한다.
  • true:
  • 클라이언트들이 서버에 방문한 총 횟수를 알고자 할때 사용한다.
  • false:
  • uninitialized 상태인 세션을 강제로 저장하면 내용도 없는 빈 세션이 스토리지에 계속 쌓일수 있다. 이를 방지, 저장공간을 아끼기 위해 사용한다.

Expiration and Secrets

1.secret
secret은 우리가 쿠키에 sign할 때 사용하는 string
쿠키에 sign하는 이유는 우리 backend가 쿠키를 줬다는걸 보여주기 위함
secret 쓸 때 stirng 이용하는데 이것은 길게 작성되고 강력하고 무작위로 만들어야함

2.domain
이 쿠키를 만든 backend가 누구인지 알려줌
브라우저는 domain에 따라 쿠키를 저장하도록 되어있음
쿠키는 domain에 있는 backend로만 전송됨
쿠키가 어디에서 왔는지, 어디로 가야하는지 알려줌

3.path
단순한 url

4.Expires
만료 날짜를 지정하지 않으면 이건 session cookie로 설정되고
사용자가 닫으면 session cookie는 끝나게 됨

  1. Max-Age
    말 그대로 언제 세션이 만료되는지 알려주는거임
    값은 1/1000초 단위로 작성
cookie: {
      maxAge: 20000,
    },

Set-Cookie
Set-Cookie HTTP 응답 헤더는 서버에서 사용자 브라우저에 쿠키를 전송하기 위해 사용됩니다.
https://developer.mozilla.org/ko/docs/Web/HTTP/Headers/Set-Cookie

쿠키에 설정가능한 옵션
Domain
쿠키가 적용되어야 하는 호스트를 지정.

Expires
HTTP 타임스템프로 기록된 쿠키의 최대 생존 시간(수명).

Max-Age
쿠키가 만료될 때 까지의 시간 (밀리세컨드)

secret
이것은 세션 ID 쿠키에 서명하는 데 사용되는 비밀입니다.
https://www.npmjs.com/package/express-session

COOKIE_SECRET에 넣을 랜덤 문자열 생성 사이트
https://randomkeygen.com/

환경변수

  1. .env 코드들은 깃헙에 올라가면 안되니까 .gitignore에 추가해주기
    env 파일에 추가하는 모든 건 대문자로 적어야함

  2. 비밀로 해야하는 string을 process.env.(환경변수)로 바꾸기

COOKIE_SECRET=dklskfjlk3kd9dsjlzkd2

env에 일케 쓰고

secret: process.env.COOKIE_SECRET,

일케 불러와주면 됨

Environment Variables

require('dotenv').config()
dotenv는 파일을 읽고 env에 추가.

우리가 dotenv를 사용하고 싶은 모든 파일에 require을 추가해 줘야함

-> 귀찮

init.js에 import "dotenv/config"; 추가하기

Github Login part

내가 만약 깃헙으로 로그인하고 싶다면 사용자를 깃헙으로 보내는거야.
사용자는 깃헙으로 이메일이랑 패스워드 등등을 넣게 됨
그리고 우리에게 정보를 공유하는 것을 승인하게 될거야. 그러면 깃헙은 사용자를 우리 웹사이트로 돌려보낼거임

https://github.com/settings/apps 들어가서 OAuth APPs를 눌러줘
작성하기~

  1. 사용자들은 깃헙 신원을 요청하기 위해 redirect된다

a(href="https://github.com/login/oauth/authorize?client_id=7816c70c2e45e04438ac&allow_signup=false") Continue with Github →

allow_signup=false-> 이미 계정이 있는 사람만 가능하게!

scope는 유저에게서 얼마나 많이 정보를 읽어내고 어떤 정보를 가져올 것에 대한 것.
그러니 당연히 필요한 정보만 요청하도록

깃헙이랑 대화할 때 "난 이런 이런 정보를 원해'라고 요구하는 것과 같음

만일 user가 yes라고 하면 github이 user를 어플리케이션의 이 URL로 보내주게됨
callback URL !!

깃헙에서 받은 토큰을 Access 토큰으로 바꿔줘야함

  1. access_token을 갖고 API에 접근하기
    이제 access_token을 가지고 user의 정보를 얻을 수 있음

socialOnly가 true이면 Github 로그인을 통해 만들어진 계정

카카오 로그인 구현하기

카카오 로그인 구현하실 분들은 아래 링크들을 참조하시면 됩니다.
구현 방식은 깃허브 로그인과 거의 동일합니다.
https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api

  1. 애플리케이션 등록
    https://developers.kakao.com/docs/latest/ko/getting-started/app

  2. 인가 코드 받기
    https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#request-code

  3. 토큰 받기
    https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#request-token

  4. 사용자 정보 가져오기
    https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#req-user-info

참고 https://github.com/baenong/NomadUTube/commit/a5033f401264c80509d89a7a37b600fe84801ae9#diff-6210ace6be99d7adc7dc8298a158709fef7ba29f17e9c3bd35f1f8fab8d9c8d1R158

day 24

  1. Is it OK to save passwords in the database?
    No, we must save hashes.
  1. What does ‘deterministic’ mean in the context of hash functions?
    It means that the same input will always return the same output

3. I can reverse the hash of a password and get the initial password.
No.

  1. Is HTTP stateless or stateful?
    Stateless.

  2. What does stateless mean?
    It means the server does not remember the user across requests.

  3. What do we save in the cookies?
    Session IDs

7. Where are sessions saved by default?
Memory

  1. Cookies are sent to the backend automatically.
    Yes.

  2. How can I access the session on my controller?
    req.session

10. All .pug files have access to ‘res.locals’? Yes

File Uploads

middleware을 실행한 다음 postedit을 실행하는 거임

middleware는 왼쪽에서 오른쪽으로 진행됨

Video Upload

샘플 비디오 파일
https://sample-videos.com/
https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_1mb.mp4

multer limit
fileSize: multipart forms의 경우 최대 파일 크기 (바이트)
https://www.npmjs.com/package/multer

Byte to MB
1000000 Bytes(백만 Bytes) = 1 MB (in decimal)
1000000 Bytes = 0.95367431640625 MB (in binary)
https://www.gbmb.org/bytes-to-mb

Bug fix

!== 은 값 뿐만 아니라 type도 비교!
populate를 안 써도 되는 곳이면 안쓰는게 좋징

Webpack Configuration

Webpack은 .jpg 같은 거는 압축된 jpg 를 주고, JS 는 못생긴 거로, Sass 는 몬생긴 css 로 준다. 압축, 변형, 최소화등 필요한 작업들을 거치고 정리된 코드를 결과물로 준다.

npm i webpack webpack-cli -D

webpack, webpack-cli devDependencies 로 설치

webpack아 여기는 소스파일들이 있고 여기는 너가 결과물을 보낼 폴더야
webpack.config.js 파일에서 webpack 환경설정. 이 파일에서는 되게 몬생긴 JS 만 쓸 수 있다.

중요한 거 두 가지(필수 설정)

  • entry: 우리가 처리하고자 하는 파일들(예쁜 js)
  • entry: 이 프로퍼티에 우리가 처리하고자 하는 파일의 경로 입력
  • output: 결과물
  • filename: 이 프로퍼티에 우리 결과물이 될 파일 이름 입력
  • path: 이 프로퍼티에 우리 결과물 파일을 어디에 저장할 지 지정 (이 경로는 절대경로여야 해!)

dirname = directory name 말 그대로 파일까지의 경로 전체를 말하는거임

rules: 우리가 각각의 파일 종류에 따라 어떤 전환을 할 건지 결정하는거임
rules는 array type
정적파일들은 한마디로 Expree한테 사람들이 폴더 안에 있는 파일들을 볼 수 있게 해달라고 요청하는거임

script(src="/static/js/main.js")

pug는 assets 폴더에서부터 파일ㅇ르 불러올거임

SCSS Loader

  1. webpack의 rules 내부의 'test: /.scss$/,' 코드에서 모든 scss파일들을 긁어온다

  2. ' use: ["style-loader", "css-loader", "sass-loader"],' 코드에서 sass-loader -> css-loader -> style-loader 순으로 loader가 적용되어 긁어온 scss 파일들을 변환시킨다

2.1
sass-loader가 scss확장자 파일을 브라우저가 이해할 수 있는 css 파일로 변환시킨다
2.2
css-loader가 @import, url()등의 최신형 css 코드를 브라우저가 이해할 수 있는 코드로 변환시켜 동작할 수 있도록 한다
2.3
style-loader가 위 과정으로 변환시킨 css 코드를 DOM 내부에 적용시켜준다

  1. 변환된 코드가 output에서 설정된 파일 경로에 설정된 파일명으로 저장된다

  2. 저장된 변환 js 코드를 pug 파일에 적용시키기 위해 'script(src="/static/js/main.js")' 코드를 통해 긁어와 적용시킨다

loader는 적힌 반대 순서로 작동함
sass-loader는 scss를 일반적인 css로 바꿔줌

MiniCssExtractPlugin

해당 코드를 다른 파일로 분리시키는 녀석

MiniCssExtractPlugin
이 플러그인은 CSS를 별도의 파일로 추출합니다. CSS가 포함된 JS 파일별로 CSS 파일을 생성합니다. mini-css-extract-plugin을 css-loader와 결합하는 것이 좋습니다.
npm install --save-dev mini-css-extract-plugin
https://webpack.kr/plugins/mini-css-extract-plugin/

MiniCssExtractPlugin Options

plugins: [new MiniCssExtractPlugin({ filename: "css/style.css" })]

https://webpack.js.org/plugins/mini-css-extract-plugin/#publicpath

CssMinimizerWebpackPlugin
https://webpack.kr/plugins/css-minimizer-webpack-plugin/

Better Developer Experience

watch: true, -> 매번 assets 폴더를 삭제하지 않아도 됨? 2개의 console을 실행해야한다는 거임 1. client 파일 확인을 위해 2. backend 파일 확인을 위해

clean:true, -> 말 그대로 output folder를 build를 시작하기 전에 clean 해주는거임

webpack.config.js를 저장할 때 마다 nodemon이 재시작하는 버그를 고쳐야함
nodemon에게 몇 가지 파일이나 폴더들을 무시하는 방법을 알려줘야함

nodemon.json

{
  "ignore": ["webpack.config.js", "src/client/*","assets/*"],
  "exec": "babel-node src/init.js"
}

src/client 하위의 파일들과, webpack.config.js파일을 ignore

webpack.config.js 파일이 webpack이 실행될 때 기본적으로 찾는 설정파일이기 때문에
"assets": "webpack" 으로 해놔도 됨

  "scripts": {
    "dev:server": "nodemon",
    "dev:assets": "webpack"
  },

일케 바꾸고 console 2개의 창에서 npm run dev:server
다른 창은 dev:assets 작성해주기!
server는 nodemon 명령어를 실행하고 이건 기본적으로 nodemon.js파일을 읽도록 되어있음

quiz

1.Why do we use Webpack on our lecture?
To transform frontend code.
2.What is Gulp?
Gulp is similar to Webpack but more simple and less powerful
3.Can Webpack process CSS files? Yes.
4.Can Webpack process image files? Yes.
5. Why do we install ‘webpack-cli’? To use Webpack from the console
6. What is the ‘entry’ on Webpack? The files that we are going to transform.
7. What is the ‘output’ on Webpack? The place where the transformed files should go.
8. What is a relative path and an absolute path?
Relative path is the path from the current working directory and absolute is the full path of the file on the computer.
9. What is a ‘rule’ in Webpack? The definition of the transformations to apply to our files.
10. What is a ‘loader’ in Webpack? The package that will transform our files.
11. What is the difference between ‘production’ and ‘development’ mode?
In production mode the code is compressed

Volume

input type="range"에 사용 가능한 이벤트 (change, input)
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/range

이벤트: change
change 이벤트는 요소 변경이 끝나면 발생합니다.
텍스트 입력 요소인 경우에는 요소 변경이 끝날 때가 아니라 포커스를 잃을 때 이벤트가 발생합니다.

이벤트: input
input 이벤트는 사용자가 값을 수정할 때마다 발생합니다.
키보드 이벤트와 달리 input 이벤트는 어떤 방법으로든 값을 변경할 때 발생합니다.

Duration and Current Time

meta data 비디오를 제외한 모든것을 말함

Time Formatting

시작점에서 원하는 곳까지 자를 수 있는 함수 substr()

const formatTime = (seconds) =>
  new Date(seconds * 1000).toISOString().substr(11, 8);

먼저 자바스크립트로 제로타임인 data를 만들어주고, 그 안에 필요한 비디오 시간만큼 초를 추가하면 됨. 그 다음 substr로 필요한 부분을 잘라내면 됨.
substr의 첫번째 argument는 어디서부터 자를건지 쓰고, 두번째 argument는 어느길이만큼 자를건지 쓰면 됨

Timeline

console.log(event.target.value);

const {
    target: { value },
  } = event;
  video.currentTime = value;

Fullscreen

Fullscreen API
Fullscreen API 는 특정 요소 Element(와 해당 자손들을)를 full-screen mode로 표시하고, 더 이상 필요하지 않으면 full-screen mode를 종료하는 메서드를 추가합니다.

Element.requestFullscreen() (en-US)
유저 에이전트가 지정한 요소(그리고 그 자손들까지)를 full-screen mode로 설정하고, 브라우저의 모든 UI 요소와 다른 모든 애플리케이션을 화면에서 제거하도록 요구합니다. full-screen mode가 활성화되면 Promise resolved를 반환합니다.

Document.exitFullscreen() (en-US)
user agent 가 full-screen mode에서 창 모드로 다시 전환되도록 요청합니다. full-screen mode가 완전히 종료되면 Promise resolved 를 반환합니다.

DocumentOrShadowRoot.fullscreenElement (en-US) (사용 추천)
fullscreenElement 속성은 DOM(혹은 shadow DOM)에서 현재 full-screen mode로 표시되는 요소Element를 알려줍니다. 이것이 null인 경우, document는 full-screen mode가 아닙니다.

Document.fullscreen (en-US) (더 이상 사용되지 않는 속성)
(fullscreenElement처럼 풀스크린을 감지할 수 있지만 사용 비추천)
문서에 현재 full-screen mode로 표시되는 요소가 있는 경우 true, 그렇지 않으면 false의 Boolean 값입니다.

https://developer.mozilla.org/ko/docs/Web/API/Fullscreen_API

const handleFullscreen = () => {
  const fullscreen = document.fullscreenElement;
  if (fullscreen) {
    document.exitFullscreen();
    fullScreenBtn.innerText = "Enter Full Screen";
  } else {
    videoContainer.requestFullscreen();
    fullScreenBtn.innerText = " Exit Full Screen";
  }
};

Controls Events part One

const handleMouseMove = () => {
  if (controlsTimeout) {
    clearTimeout(controlsTimeout);
    controlsTimeout = null;
  }
  videoControls.classList.add("showing");
};

const handleMouseLeave = () => {
  controlsTimeout = setTimeout(() => {
    videoControls.classList.remove("showing");
  }, 3000);
};

showing이라는 class를 받게 되고 나가면 timeout이 시작될거임 그리고 이제 timeout이 완료될거임
이제는 비디오에 들어가주고, 나갔다 다시 들어올거야 나갔다가 다시 들어왔기 때문에 timeout은 취소되어야해.
내 코드를 보면 class showing은 그대로야. timeout이 취소됐기 때문.

내가 나가면 timeout이 취소되지않고 class는 사라질거임

내가 나갈때 controlsTimeOut variable는 null value에서 그 timeout id로 바뀌어져
그 말은 즉슨, 내가 다시 안으로 들어오면, controlsTimeout은 계속 숫자일테니 true를 반환할거임 그러면 clearTimeout이 실행되는거임

Controls Events part Two

우리는 매번 마우스가 움직일 때마다 timeout을 시작 시킬거임
그 timeout은 컨트롤러를 숨겨줄거야 okay?

만약 마우스가 계속 움직인다면 timeout을 취소 시켜줄거야

비디오 안에서 마우스가 움직이기 시작하면 timeout을 시작할거고,
그 timeout은 3초 뒤에 컨트롤러를 숨길거임
그리고 마우스를 움직일 때 clearTimeout도 같이 시작하게 할거임

code challenge

const video = document.querySelector("video");
const videoContainer = document.getElementById("videoContainer");
const videoController = document.getElementById("videoController");
const psBtn = videoController.querySelector("#playPauseBtn");
const currentTime = videoController.querySelector("#currentTime");
const totalTime = videoController.querySelector("#totalTime");
const timeline = videoController.querySelector("#timeline");
const volumeBtn = videoController.querySelector("#volume");
const volumeRange = videoController.querySelector("#volumeRange");
const fullScreenBtn = videoController.querySelector("#fullScreenBtn");

let volumeValue = 0.5;
video.volume = volumeValue;

const handlePlayAndStop = () => {
  if (video.paused) {
    video.play();
    psBtn.className = "fas fa-pause";
  } else {
    video.pause();
    psBtn.className = "fas fa-play";
  }
};

const timeFormat = (seconds) =>
  new Date(seconds * 1000).toISOString().substr(11, 8);

const handleTotalTime = () => {
  if (Math.floor(video.duration) < 3600) {
    totalTime.innerText = timeFormat(Math.floor(video.duration)).substr(3);
  } else {
    totalTime.innerText = timeFormat(Math.floor(video.duration)).substr(0, 5);
  }
  timeline.max = Math.floor(video.duration);
};

const handleCurrentTIme = () => {
  if (Math.floor(video.currentTime) < 3600) {
    currentTime.innerText = timeFormat(Math.floor(video.currentTime)).substr(3);
  } else {
    currentTime.innerText = timeFormat(Math.floor(video.currentTime)).substr(
      0,
      5
    );
  }
  timeline.value = Math.floor(video.currentTime);
};

const handleTimeline = (event) => {
  const {
    target: { value }
  } = event;
  video.currentTime = value;
};

const handleSound = () => {
  if (video.muted) {
    video.muted = false;
    volumeRange.value = volumeValue;
    volumeBtn.className = "fas fa-volume-up";
  } else {
    video.muted = true;
    volumeRange.value = 0;
    volumeBtn.className = "fas fa-volume-mute";
  }
};

const handleVolume = (event) => {
  const {
    target: { value }
  } = event;
  if (video.muted) {
    video.muted = false;
    volumeBtn.className = "fas fa-volume-mute";
  }
  if (value === "0") {
    volumeBtn.className = "fas fa-volume-off";
  } else {
    volumeBtn.className = "fas fa-volume-up";
  }
  video.volume = volumeValue = value;
};

const handleFullScreen = () => {
  const fullscreen = document.fullscreenElement;
  if (fullscreen) {
    document.exitFullscreen();
    fullScreenBtn.className = "fas fa-expand";
  } else {
    videoContainer.requestFullscreen();
    fullScreenBtn.className = "fas fa-compress";
  }
};

const handleKeyboard = (e) => {
  if (e.key === " ") {
    handlePlayAndStop();
  } else if (e.key === "f") {
    videoContainer.requestFullscreen();
    fullScreenBtn.className = "fas fa-compress";
  } else if (e.key === "Escape") {
    document.exitFullscreen();
    fullScreenBtn.className = "fas fa-expand";
  }
};

psBtn.addEventListener("click", handlePlayAndStop);
video.addEventListener("loadedmetadata", handleTotalTime);
video.addEventListener("timeupdate", handleCurrentTIme);
timeline.addEventListener("input", handleTimeline);
volumeBtn.addEventListener("click", handleSound);
volumeRange.addEventListener("input", handleVolume);
fullScreenBtn.addEventListener("click", handleFullScreen);
window.addEventListener("keyup", handleKeyboard);

(1) 현재 시간 표시 (1-1) loadedmetadata 이벤트

video.addEventListener("loadedmetadata", handleTotalTime)
loadedmetadata는 비디오 오브젝트의 이벤트로 meta data가 로드될 때 실행됩니다.
(1-2) 시간 포맷

const timeFormat = (seconds) => new Date(seconds 1000).toISOString().substr(11, 8)
timeFormat은 seconds를 받아서 new Date(seconds)를 반환합니다.
Date()는 현재 시간을 알려주는 자바스크립트의 함수로, 컴퓨터의 제로 타임을 기준으로 밀리초(ms)를 추가할 수 있습니다.
예를 들어, 제로 타임으로부터 20초 뒤의 값을 알고 싶다면 New Date(20
1000)을 입력하면 됩니다.
값을 String으로 가져오기 위해 toISOString()을 호출하면 "1970-01-01T00:00:20.000Z"이라는 값이 나옵니다.
이 값에 substr(from, ?length)를 호출하여 시작점에서 원하는 길이만큼 자를 수 있습니다.
비디오 컨트롤러에서는 시간만 표시하면 되기 때문에, 11번째 글자 다음부터 8글자만 필요합니다. substr(11, 8)라고 작성하면 됩니다.
(주의) 그런데 이 기능은 더 이상 권장하는 기능은 아닙니다. 일부 브라우저는 여전히 이 기능을 지원하지만, 관련 웹 표준에서 이미 제거되었거나 삭제 중이어서 호환성 목적으로만 유지할 수 있으니 참고하시기 바랍니다.
(1-3) 비디오의 총 시간을 구하는 handleTotalTime 핸들러

handleTotalTime 핸들러는 비디오의 현재 시간을 구해 타임라인에 표시합니다.
totalTime.innerText = timeFormat(Math.floor(video.duration))
video.duration()을 사용하여 비디오의 길이를 구한 후 포맷하여 총 시간을 업데이트하면 됩니다.
timeline.max = Math.floor(video.duration)
비디오 레인지의 maximum value가 비디오 길이가 되도록 세팅합니다. min, max가 있어야 어디가 시작이고 어디가 끝인지 알 수 있기 때문에 반드시 설정해 주어야 합니다.
html에서 마크업으로 작성한 input의 step은 타임라인의 밸류가 얼마씩 증가할지 세팅하는 것을 돕습니다.
(1-4) Timeupdate 이벤트

video.addEventListener("timeupdate", handleCurrentTIme)
timeupdate는 비디오의 시간이 변할 때마다 실행됩니다.
(1-5) 현재 시간을 핸들링하는 handleCurrentTIme 핸들러

currentTime.innerText = timeFormat(Math.floor(video.currentTime))
먼저 시간 포맷하는 함수에 비디오의 현재 초(s)를 보내줍니다.
timeline.value = Math.floor(video.currentTime)
비디오의 현재 시간이 변할 때마다 타임라인의 레인지 값도 변하게 하면 됩니다.
(2) 타임라인(비디오 진행 상태, 클릭 시 점프)

비디오가 얼마나 진행되었느냐에 따라 표시되는 range 값도 달라집니다. 사용자가 range 값의 다른 부분을 클릭하면 해당하는 초로 새로 고침 되어야 합니다.
(2-1) input 이벤트

timeline.addEventListener("input", handleTimeline)
input 이벤트는 value 값이 바뀔 때마다 이벤트가 실행되기 때문에 이 이벤트를 사용하여 타임라인의 range 값을 구할 수 있습니다.
(2-2) handleTimeline 핸들러

video.currentTime = value;
Input 이벤트의 target.value로 range의 value을 가져오고, 비디오의 현재 시간을 input 이벤트에서 가져온 value로 업데이트합니다.
(3) 전체 화면

(3-1) click 이벤트

fullScreenBtn.addEventListener("click", handleFullScreen)
전체 화면 버튼을 클릭하면 handleFullScreen 핸들러를 호출합니다.
(3-2) handleFullScreen 핸들러

const fullscreen = document.fullscreenElement로 풀스크린을 정의합니다.
If else 구문을 이용해 fullscreen 상태라면 document.exitFullscreen()로 풀스크린을 벗어나고, 풀스크린 아이콘으로 모양을 바꾸면 됩니다.
fullscreen 상태가 아니라면 videoContainer.requestFullscreen()로 풀스크린으로 들어가고, 풀스크린을 벗어나는 아이콘으로 모양을 바꾸면 됩니다.
(주의) 풀스크린을 요청할 때는 element에 요청하지만, 종료할 때는 document에 요청합니다.
(4) 단축키: Space를 눌러 일시 중지, 'F'를 눌러 전체 화면 모드로 들어가기, Esc 키를 눌러 전체 화면 모드에서 나오기

(4-1) keyup 이벤트

window.addEventListener("keyup", handleKeyboard)
keyup 이벤트를 사용하면 눌러지는 event.key로 눌러지는 키값을 알 수 있습니다. 이 이벤트를 사용하여 비디오 플레이어에서 단축키를 사용하면 됩니다.
(4-2) handleKeyboard 핸들러

const handleKeyboard = (e) => {}
event.key 값을 사용하기 위해 event를 인자로 보냅니다.
“Space” 키를 눌러 일시정지/재생이 되도록 하려면, (e.key === " ")일 때 handlePlayAndStop()를 호출하면 됩니다.
“F” 키를 눌러 전체 화면 모드로 들어가게 하려면, (e.key === "f")일 때 videoContainer.requestFullscreen()를 호출하고 풀스크린 아이콘 모양을 변경해 주면 됩니다.
“Esc” 키를 눌러 전체 화면 모드에서 나오려면, (e.key === "Escape")일 때 document.exitFullscreen()를 호출하고 풀스크린 아이콘 모양을 변경해 주면 됩니다.

Register View Event

ended event
audio 또는 video 미디어가 끝까지 재생 완료 된 시점에 발생합니다.
ended 이벤트는 미디어(오디오나 비디오)의 끝 부분에 도달했거나 더 이상 사용할 수 있는 데이터가 없어서 재생 또는 스트리밍이 중지되면 시작됩니다.
https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/ended_event

HTMLMediaElement
https://developer.mozilla.org/ko/docs/Web/API/HTMLMediaElement

Data Attributes
HTML5 특정 요소와 연관되어 있지만 확정된 의미는 갖지 않는 데이터에 대한 확장 가능성을 염두에 두고 디자인되었습니다. data-* 속성은 표준이 아닌 속성이나 추가적인 DOM 속성, Node.setUserData()과 같은 다른 조작을 하지 않고도, 의미론적 표준 HTML 요소에 추가 정보를 저장할 수 있도록 해줍니다.
https://developer.mozilla.org/ko/docs/Learn/HTML/Howto/Use_data_attributes

Recorder Setup

ediaDevices.getUserMedia()
MediaDevices 인터페이스의 getUserMedia() 메서드는 사용자에게 미디어 입력 장치 사용 권한을 요청하며, 사용자가 수락하면 요청한 미디어 종류의 트랙을 포함한 MediaStream (en-US)을 반환합니다. 스트림은 카메라, 비디오 녹화 장치, 스크린 공유 장치 등 하드웨어와 가장 비디오 소스가 생성하는 비디오 트랙과, 마이크, A/D 변환기 등 물리적과 가상 오디오 장치가 생성하는 오디오 스트림, 그리고 그 외의 다른 종류의 스트림을 포함할 수 있습니다.
보통, MediaDevices 싱글톤 객체는 다음과 같이 navigator.mediaDevices를 사용해 접근합니다.
navigator.mediaDevices.getUserMedia(constraints);
https://developer.mozilla.org/ko/docs/Web/API/MediaDevices/getUserMedia

constraints
요청할 미디어 유형과 각각에 대한 요구사항을 지정하는 MediaStreamConstraints 객체. constraints 매개변수는 두 개의 구성 요소, video와 audio를 가지는 객체로, 요청할 미디어 유형에 대해 설명합니다. 둘 중 적어도 하나는 지정해야 합니다.
{ audio: true, video: true }

regenerator-runtime
Regenerator로 컴파일된 생성기 및 비동기 함수를 위한 독립 실행형 런타임입니다.
npm i regenerator-runtime
import regeneratorRuntime from "regenerator-runtime";
https://www.npmjs.com/package/regenerator-runtime

Video Preview

HTMLMediaElement srcObject

HTMLMediaElement 인터페이스의 srcObject 속성은 HTMLMediaElement와 연결된 미디어의 소스 역할을 하는 객체를 설정하거나 반환합니다.
그 객체는 MediaStream, MediaSource, Blob 또는 파일(Blob에서 상속됨)일 수 있습니다.

사용 예시
이 예에서 카메라의 MediaStream은 새로 생성된 요소에 할당됩니다.

const mediaStream = await navigator.mediaDevices.getUserMedia({video: true});
const video = document.createElement('video');
video.srcObject = mediaStream;

https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/srcObject

const handleStart = async () => {
  const stream = await navigator.mediaDevices.getUserMedia({
    audio: false,
    video: true,
  });
  video.srcObject = stream;
  video.onplay();
};

stream을 가져오고 있고 video에 srcObject를 부여하고 있다
srcObject는 video가 가질 수 있는 무언가를 의미해

srcObject는 video가 가질 수 있는 무언가를 의미해
srcObject는 MediaStream, MediaSource, Blob, File을 실행할 때 video에 주는 무언가를 의미하기도 함

Recording Video

MediaRecorder
MediaStream Recording API의 MediaRecorder 인터페이스는 미디어를 쉽게 녹화할 수 있는 기능을 제공합니다. MediaRecorder() 생성자를 사용하여 생성됩니다.
https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder

MediaRecorder()
기록할 MediaStream이 지정된 새 MediaRecorder 개체를 만듭니다.

stream
기록될 MediaStream입니다. 이 소스 미디어는 navigator.mediaDevices.getUserMedia()를 사용하여 생성된 스트림이나 audio, video 또는 canvas 요소에서 가져올 수 있습니다.

MediaRecorder.start()
미디어 녹화를 시작합니다. 이 메서드는 선택적으로 밀리초 단위의 값을 가진 타임슬라이스 인수를 전달할 수 있습니다.

MediaRecorder.stop()
저장된 데이터의 최종 Blob을 포함하는 dataavailable 이벤트가 발생하는 시점에서 기록을 중지합니다.

MediaRecorder ondataavailable
MediaRecorder.stop()이 실행될 때 발생하는 이벤트이다.
https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/ondataavailable

Recording Video part Two

URL.createObjectURL()

URL.createObjectURL() 정적 메서드는 주어진 객체를 가리키는 URL을 DOMString으로 반환합니다. 해당 URL은 자신을 생성한 창의 document가 사라지면 함께 무효화됩니다.

object
객체 URL을 생성할 File, Blob, MediaSource 객체.
const objectURL = URL.createObjectURL(object)

https://developer.mozilla.org/ko/docs/Web/API/URL/createObjectURL

createObjectURL - 브라우저 메모리에서만 가능한 url을 만들어줘
그냥 파일을 가리키는 url
실제로 url을 만든게 아니라 브라우저의 메모리 상에 파일을 저장해두고 브라우저가 그 파일에 접근할 수 있는 URL을 주는거야
우리가 녹화한 이 비디오는 브라우저의 메모리에 있음

Introduction(FFmpeg & WebAssembly)

FFmpeg
오디오 및 비디오를 기록, 변환 및 스트리밍하는 완벽한 크로스 플랫폼 솔루션입니다. FFmpeg는 인간과 기계가 만든 거의 모든 것을 디코딩, 인코딩, 트랜스코딩, mux, demux, 스트리밍, 필터링 및 재생할 수 있는 최고의 멀티미디어 프레임워크
https://www.ffmpeg.org/

FF는 비디오로 하고 싶은 것은 뭐든 할 수 있어
오디오를 추출할 수 있고, 오디오 형식을 변경할 수 있어
비디오를 가지고 gif를 만들 수도 있어
FF를 실행하려면 백엔드에서 실행해야만 한다 이거 비싸
그래서 FFmpeg WebAssembly
우리가 프론트엔드에서 매우 빠른 코드를 실행할 수 있게 해줌
WebAssembly에서 제공하는 브라우저 및 노드용 FFmpeg

ffmpeg.wasm는 비디오를 변환하기 위해 사용자의 컴퓨터를 사용함
유투브는 업로드 된 비디오를 그들의 비싼 서버에 변환할거임
지금 우리가할 건 사용자의 브라우저에서 비디오를 변환하는거임

ffmpeg.wasm은 FFmpeg의 순수한 Webassembly/Javascript 포트
그것은 비디오 및 오디오 녹음, 변환, 스트리밍 등을 브라우저 내부에서 할 수 있어.
FFmpeg WebAssembly를 사용하는 이유는 FFmpeg를 사용해서 브라우저로 하여금 비디오 파일을 변환하기 위함이다.
npm install @ffmpeg/ffmpeg @ffmpeg/core
https://github.com/ffmpegwasm/ffmpeg.wasm
https://www.npmjs.com/package/@ffmpeg/ffmpeg

WebAssembly
WebAssembly(Wasm)는 스택 기반 가상 머신을 위한 이진 명령 형식
Wasm은 프로그래밍 언어를 위한 이식 가능한 컴파일 대상으로 설계되어 클라이언트 및 서버 응용 프로그램을 위해 웹에 배포할 수 있습니다.

웹 어셈블리는 자바스크립트의 무덤일까?
https://www.youtube.com/watch?v=KjgDxBLv0bM

맥에서 FFMpeg설치하기
brew install ffmpeg

Quiz

  1. Why do we need to convert our file? Because not all devices understand WebM
  2. What is ffmpeg? A program to manipulate videos.
  3. With ffmpeg we can extract the audio from a video. Yes. We can do anything with a video.
  4. With ffmpeg we can extract images from a video. Yes. We can do anything with a video.
    5. What is WebAssembly? A standard that allows us to run Non-JS code on the browser.
    6. With WebAssembly we don’t have to use Javascript on the browser. Yes.
  5. WebAssembly allows us to run faster code on the browser. Yes.

Transcode Video

ffmpeg.FS(method, ...args): any
ffmpeg.wasm의 입출력 파일은 ffmpeg.wasm이 소비할 수 있도록 먼저 MEMFS에 저장해야 합니다.
https://github.com/ffmpegwasm/ffmpeg.wasm/blob/master/docs/api.md#ffmpegfsmethod-args-any

fetchFile(media): Promise
다양한 리소스에서 파일을 가져오기 위한 도우미 기능입니다. 때로는 처리하려는 비디오 / 오디오 파일이 원격 URL과 로컬 파일 시스템의 어딘가에 있을 수 있습니다.이 도우미 함수는 파일로 가져오고 ffmpeg.wasm이 사용할 Uint8Array 변수를 반환하는 데 도움이 됩니다.
https://github.com/ffmpegwasm/ffmpeg.wasm/blob/master/docs/api.md#fetchfilemedia-promise

ffmpeg.load
ffmpeg.load()를 호출하면 기본적으로 http://localhost:3000/node_modules/@ffmpeg/core/dist/를 검색하여 필수 파일을 다운로드합니다. (ffmpeg-core.js, ffmpeg-core.wasm, ffmpeg-core.worker.js). 해당 파일이 거기에 제공되었는지 확인해야 합니다. 해당 파일이 다른 위치에 있는 경우 호출할 때 기본 동작을 다시 작성할 수 있습니다.
https://github.com/ffmpegwasm/ffmpeg.wasm#why-it-doesnt-work-in-my-local-environment

await ffmpeg.run("-i", "recording.webm", "-r", "60", "output.mp4");
recording.webm은 파일이야
recording.webm input을 받아서 output.mp4로 변환해주는 명령어를 사용했음
이건 영상을 초당 60프레임으로 인코딩 해주는 명령

Thumbnail

기억해 이 파일은 FS의 메모리에 만들어지는거
You can extract images from a video, or create a video from many images
비디오에서 이미지를 추출하거나 여러 이미지에서 비디오를 만들 수 있습니다. 이것은 비디오에서 초당 하나의 비디오 프레임을 추출하고 foo-001.jpeg, foo-002.jpeg 등의 파일로 출력합니다. 이미지는 새로운 WxH 값에 맞게 크기가 조정됩니다. 제한된 수의 프레임만 추출하려면 위의 명령을 -frames:v 또는 -t 옵션과 함께 사용하거나 -ss와 함께 사용하여 특정 시점부터 추출을 시작할 수 있습니다.
ffmpeg -i foo.avi -r 1 -s WxH -f image2 foo-%03d.jpeg
https://www.ffmpeg.org/ffmpeg.html

Blob()
Blob() 생성자는 새로운 Blob 객체를 반환합니다. 해당 블롭의 콘텐츠는 매개변수로 제공한 배열을 이어붙인 값입니다.
var aBlob = new Blob( array[, options]);
array는 ArrayBuffer, ArrayBufferView (en-US), Blob, DOMString 객체 또는 Blob 안에 들어갈 이러한 객체가 혼합되어 있다. DOMString은 UTF-8로 인코딩 된다.

var oMyBlob = new Blob(aFileParts, {type : 'text/html'}); // the blob

https://developer.mozilla.org/ko/docs/Web/API/Blob/Blob

Installation

Express Flash
Express 애플리케이션용 플래시 메시지
플래시는 플래시 메시지를 정의하고 요청을 리디렉션하지 않고 렌더링할 수 있는 기능이 있는 connect-flash의 확장입니다.
npm i express-flash

사용 예시

app.get('/', function (req, res) {
req.flash('info', 'Welcome');
res.render('index', {
title: 'Home'
})
});
app.get('/addFlash', function (req, res) {
req.flash('info', 'Flash Message Added');
res.redirect('/');
});

https://www.npmjs.com/package/express-flash

connect-flash
https://www.npmjs.com/package/connect-flash

redirect할 때 사용자에게 이유를 설명하고 싶으면 메시지를 보낼 수 있게 됐음

1단계. 서버에 express-flash라는 미들웨어 설치
2단계. 메시지를 남길 수 있도록 사용자를 redirect하는 곳에 req.flash를 사용
(redirect하는 곳엠나 쓰는건 아니고, 어디든 쓸 수 있음)

Sending Messages

app.use(flash());

미들웨어를 만들어 사용하고

req.flash("error", "Log in first.");

이 미들웨어는 req.flash()라는 함수를 사용할 수 있게 해주고

req.flash("ok", "flash의 내용")처럼ㅁ나 써주면 됨

Building the Backend

"build:server": "babel src/init.js -d build",

directory의 의미는 빌드한 코드를 어디에 저장할 지를 얘기하는거얌

npm run dev:assets
npm run dev:server

Heroku

heroku login

git add .

git commit -m "Add Message"

git push origin main

git push heroku main

0개의 댓글