유튜브 클론코딩[MONGODB AND MONGOOSE]

김동현·2022년 1월 8일
0

nodeJS

목록 보기
4/14

post

  • 백엔드에 데이터를 보내는 방법으로 get과 post가 있다.
    req.params가 url의 파라미터를 받아온다면 form 형식의 데이터를 post로 받아올땐 req.body로 받아온다.



    undefined 자료형의 프로퍼티를 읽을 수 없다고 한다. 그 대상이 title 이다. title이 undefined라는 말은 req.body가 undefined라는 말이다.

  • form형식의 데이터를 post 요청방식으로 백엔드로 보낼때 그 데이터들을 req.body에 넣어주는 중간 작업이 필요하다. 즉, html의 form형태를 body-parser해서 object형식으로 번역해주는 미들웨어가 필요하다.

    고맙게도 express 패키지에 해당 미들웨어가 내장되어 있다. app.use(express.urlencoded({ extended: true }));

  • 앞서 app.get() 혹은 router.get()을 사용했었는데 여기서 get()은 GET요청방식일때 실행되는 메서드이다. POST요청방식일땐 실행되지 않는다. 그러므로 app.post() 혹은 router.post() 와 같이 POST요청방식일때 실행되는 post()메서드를 서버에 정의해놓아야 한다.

  • 같은 url에 get과 post 두개의 방식으로 ACCESS하기 위해서는
    라우터.get(url, getController)
    라우터.post(url, postController)
    둘 다 정의 되어 있어야 하는데 하나로 합쳐서
    라우터.route(url).get(getController).post(postController) 로도 표현가능하다.
    한 가지 요청방식이 요구될 때는 전자의 방법을 사용하도록 하고, 두 가지 이상의 요청방식이 요구될 때는 후자의 방법을 사용하도록 한다.

  • ES6의 깔끔한 코드로 인해
    const id = req.params.id;
    const { id } = req.params; 로 바꿔 사용가능하다.

  • pug의 태그 속성안에서는 #{변수}사용이 불가능하다. 백틱스트링이나 문자열 연결 연산자로 사용한다.

  • 절대경로와 상대경로

    • a태그의 href속성이 /로 시작하면 절대경로, /로 시작하지 않으면 상대경로이다.
      <예> 현재 url이 localhost/videos/1 이라면
      a(href="/edit") => localhost/edit
      a(href="edit") => localhost/videos/edit 이다.

      위의 코드에서는 절대경로를 사용했다.
  • res.redirect(url)은 해당 url로 리다이렉트 하는 메소드이다.

Introduction MongoDB

  • 몽고DB는 sql베이스가 아닌 JSON-like-Document 베이스이다.

  • 몽고DB설치시 공식 홈페이지에서 다운받아도 되지만 윈도우10의 경우 chocolatey(윈도우유저들을 위한 패키지 매니저)를 설치해서 다운할 수 있다. 설치후 환경변수 등록을 해준다.

  • wsl에서 몽고DB 설치시 아래 링크 참고
    https://docs.microsoft.com/ko-kr/windows/wsl/tutorials/wsl-database

Connecting to Mongo

  • mongoose : NodeJS와 MongoDB를 이어주는 다리 역할

  • mongo가 정상설치 되었다면 cmd에서 mongod 입력시 아래와 같이 나와야 한다.

  • mongo를 입력하면 몽고쉘로 접속된다. 연결된 url을 복사해둔다.

  • help명령어를 입력하면 사용할 수 있는 명령어 목록들이 나온다.

  • db.js파일을 만든후 아래의 코드를 입력한다.

  • 잘 연결되었는지 에러가 나는지 확인을 위해 db에 이벤트를 추가했다.
    on과 once의 차이점은 on은 여러번의 이벤트에 반응하고 once는 단 한번의 이벤트에만 반응한다.

  • 이 파일은 서버와 독립된 파일이다. server.js파일에서 import를 해줘야 서버실행시 같이 실행된다. 파일 그 자체를 import하는 거여서 이름이 필요없고 export 키워드도 필요없다.

  • mongoose.connect()메서드의 인자로 몽고DB의 url이 들어가는데 마지막에 /wetube는 DB의 이름이다.

CRUD Introduction

  • CRUD : create, read, update, delete

Video Model

  • 스키마로 틀을 만든후 모델을 생성한다.

  • Models폴더에 따로 관리한다.

    • String은 {type:String}의 간편버전이다.

    • 모델의 이름은 대문자로 시작한다.(몽구스 사용할때의 권장사항)

    • 이 파일 역시 server.js파일에서 import해줘야 실행된다.

Our First Query

  • 모델 Video를 쓰기위해 server.js 파일에 db.js와 Video.js파일이 import되어있다.
    하지만, server관련 파일에 db관련 파일이 import되는 건 옳지 않다.
    또한, 모델이 많아질수록 import도 많아지므로 정리해줘야 한다.
    init.js파일을 만들어서 db를 포함해 초기화 시켜주는 소스를 넣도록 한다.

  • server파일엔 server관련 환경설정세팅을 하는 코드만 남기고 나머지는 모조리 init파일에 옮겼다.
    app변수가 필요하므로 server파일을 import시켰다.
    물론 server파일에서 app변수 export하였다.

  • 지금 서버를 실행시키는 파일이 server.js에서 init.js로 바뀌었다. 현재 nodemon은 server.js파일을 실행시키므로 package.json에서 server를 init으로 바꿔준다.

  • 컨트롤러 파일에서 디비 모델을 사용하기위해 import를 해준다.

  • 모델에서 특정 데이터를 찾고싶다면 Video.find()처럼 사용하면 된다.
    관련 문서 링크는 https://mongoosejs.com/docs/guide.html

  • 참고로 find()메소드 안에 중괄호는 모든 데이터를 뜻한다.

  • find의 사용법으로는 2가지가 있다. 그중 첫번째는 콜백함수를 쓰는것이다.

  • 주의할 점은 콜백함수에 리턴을 사용하면 안된다는 것이다. 순서가 바뀌므로 주의해야 한다.

    • a, b, c, d 를 출력함으로써 순서를 살펴보면

      a가 출력된 후 콜백함수를 건너뛰고 d가 출력되었다. 그 뿐만 아니라 바로 뒤에 logger 메세지가 나온것을 보면 랜더링까지 완료가 된 후에 콜백함수가 실행되는것을 볼 수 있다.
      만약 콜백함수안에 render()을 리턴하는 코드를 넣는다면 콜백함수가 끝나고 난 후 랜더링이 되므로 좋지 않다.
      지금이야 속도차이가 거의 안나지만 나중에 제대로 DB를 사용할때 모든 데이터를 로드한 후 랜더링하게 되므로 좋지 않다.

async await

  • 콜백함수의 장점은 에러를 바로 볼 수 있다는 점이다.

  • 하지만 함수안에 함수가 사용되는 구조자체가 코드가 복잡해지도록 하는 단초가 된다. (예: 함수안의 함수안의 함수안의 함수안의...)

  • 그걸 방지하기 위해 promise라는것이 등장했다.
    promise는 콜백함수의 최신버전이다.
    promise는 asyncawait 키워드를 사용한다.

    async : "애들아 이 함수에는 뭔가 약간 구린 코드가 있어
    그러니까 일단 내 볼일 볼때까지 그냥 니들일은 알아서들
    하고 있엉~~"
    await : "ㅋㅋㅋ 내가 바로 그 구린녀석이다. 나도 내할일 하고
    나서 결과 줄테니까 다른 넘들은 다 각자 할일 하고 있고
    단 내뒤에 있는 넘들은(비동기 함수내에 있는 내뒤에 코드들임, 다른 함수들은 뒤에 있어도 비동기기 때문에 이미 실행됨) 일단 대기타라."

  • await는 결과값을 받을때까지 대기한다.
    자바스크립트가 어디서 기다리는지 직관적으로 보이는게 장점이다.

  • 규칙상 await는 async 함수안에서만 가능하다.

  • async, await에서 에러는 다음과 같이 표현할 수 있다.(try/catch이용)

Returns and Renders

  • return res.render()에서 굳이 return 키워드를 사용하지 않고, 실행해도 똑같이 동작한다. 그럼 return은 왜 사용하는가?

  • res.render() 코드는 2번 이상 사용하면 오류를 발생시킨다.

  • res.render()가 아니더라도 sendStatus(), redirect(), end()등을 포함, 중복사용시 express에서 오류를 발생시킨다.

  • 일반적으로 return은 어떤 값을 반환하기 위해 사용된다. 하지만 함수를 종료하는 기능때문에 쓰이는 경우도 있다. 이 경우는 후자의 경우이다.
    즉, return res.render()의 진짜의미는 반환의 의미보다는 res.render() 호출후 함수를 종료하라는 의미이다.
    혹여나 중복으로 사용하여 오류를 발생시킬수 있으니 실수 방지 목적으로 사용된다. return사용이 필수는 아니다.

Creating a Video

  • new Video를 통해 object를 생성했는데 DB에는 저장되지 않는다.

  • 홈으로 리다이렉트 한뒤 home 페이지를 보자.
    참고로 home 페이지에는 DB에서 내용을 받아와서 출력해주는 기능이 있다.

    폼에서 데이터를 입력후 Upload Video 버튼을 누르면 post 요청으로 postUpload 컨트롤러가 실행되고 홈으로 리다이렉트 된다.

  • home 페이지에 아무런 정보가 보이지 않는다.
    DB에 저장되지 않았단 뜻이다.
    생성된 video오브젝트를 DB에 커밋해야 한다.
    video.save()를 해주면 하나의 도큐먼트로 DB에 저장된다.

  • save() 메소드는 promise를 리턴한다.
    따라서 awaitasync를 사용해 줘야 한다.
    promise의 대기시간이 끝나면 업데이트한 document를 리턴하므로 코드는 아래와 같이 작성한다.

  • 콘솔창에서 db에 접근하면 저장된 것을 확인 할 수 있다.

  • collection는 document의 묶음이라고 생각하면 된다.
    관계형 DB에서의 테이블과 같다고 볼 수 있다.
    document는 관계형 DB에서의 행으로 볼 수 있다.

  • collection이름이 videos인 이유는
    mongoose.model("Video", videoSchema); 이 코드에서 Video뒤에 s가 자동으로 붙여지고 자동으로 소문자화 시키기 때문이다.

  • db에 저장하는 방식으로는 2가지가 있는데 하나는 위와 같이 save() 메서드를 이용하는 것이고 나머지 하나는 create() 메서드를 사용하는 것이다.
    다음의 경우는 create()의 방식이며, 특징은 자바스크립트 object를 만들지 않고 바로 디비에 저장한다는 것이다.
    따라서 object.save() 를 하지 않아도 바로 DB에 커밋이 된다.

  • 혹여 스키마에서 정의한 데이터 타입과 다른 타입의 데이터를 저장시키려 한다면 오류가 발생할 수 있다.
    <예> string타입에 5를 넣으면 "5"가 들어가지고 Number타입에 "abc"를 넣으면 오류가 발생한다.
    오류가 발생되면 더 이상 진행이 되지 않으므로 try/catch문으로 서버가 멈추는걸 방지하면 좋다.

Exceptions and Validation

  • 만약 스키마에 정의된 필드를 하나를 빼고 저장한다면 어떻게 될까?
    정답은 "그 필드의 값만 빼고 나머지 값은 저장된다" 이다.
    그러니, 꼭 필요한 값이라면 스키마에서 required 옵션을 넣어야 한다.

    required가 있는 필드에 값을 보내지 않는다면 validation에러가 뜬다.


    에러를 콘솔에 출력하고 싶다면 catch문에서 발생된 error 오브젝트를 출력하면 되고, 메시지만 출력하고 싶다면 error 오브젝트의 _message 속성을 출력하면 된다. render() 메서드로 사용자 페이지에 나타나게 할 수 도 있다.


  • 기본값이 정해져 있다면 매번 document를 생성할때 마다 중복되는 과정이 생긴다.

    그 경우엔, 스키마에서 정의할때 바로 정의해주면 좋다.

    스키마에서 default 값을 설정할 수 있다.
    단, 스키마에 기본값을 정의할 때 만약 함수를 이용하고 싶다면 소괄호를 붙이지 않는다. 위의 경우를 보면 Date.now에서 ()를 붙이지 않았다.
    그 이유는 소괄호 까지 넣어버리면 함수호출을 뜻하기 때문에 스키마 정의시 함수호출이 일어나기 때문이다.
    우리는 document 생성시에만 자동으로 호출되기를 바라기 때문에 소괄호를 넣지않는다.
    몽고DB와 Mongoose는 똑똑한(?) 친구들이어서 document생성시 자동으로 소괄호를 넣어서 함수호출을 한다.

    default값을 설정했기때문에 document생성 코드에서 기본값을 주던 코드를 뺀다.

More Schema

  • 모든 스키마 타입들

    관련문서 : https://mongoosejs.com/docs/schematypes.html

  • string타입의 옵션들

    예를들어 {type: String, uppercase: true}로 스키마를 정의한다면
    document를 생성할때 abcd로 만들어도 ABCD로 저장된다.

  • 만약, 스키마에서 maxLength 와 minLength를 설정한다면 html의 input 태그에서도 maxlength와 minlength옵션을 설정해주도록 한다.
    해커들이 html페이지를 해킹해서 DB에 비정형화된 데이터를 넣는다면 데이터의 일관성을 해칠수 있기 때문이다.

Video Detail

  • /videos/id 로 접속하면 페이지를 찾을수 없다.

    라우터를 보자

    정규표현식을 사용해서 숫자만 id로 인식되도록 되어있다.
    해결방법으로는 /upload페이지를 맨 위로 올리거나 정규표현식을 id에 맞게 바꾸는 2가지의 방법이 있다.

    여기서는 후자의 방법으로 진행한다.
    먼저 document에 자동으로 부여되는 id의 규칙을 알기위해 몽고DB 공식페이지를 참조한다.

    문서 링크 : https://mongodb.github.io/node-mongodb-native/api-bson-generated/objectid.html

    id값이 24글자의 16진수로 이루어져 있다고 나와있다.
    정규표현식으로 나타내면 [0-9a-f]{24} 이다.

    다시 페이지에 접근하면 watch컨트롤러가 실행되는것을 볼 수 있다.

    위의 오류는 watch.pug 파일에 값을 넘겨주지 않아서 생긴 오류이다.
    watch.pug파일에서 어떤 값들이 사용되는지 보자.

    video오브젝트가 사용되는데 전해지지 않았다.

    video 오브젝트를 전해주기 전에, 먼저 video 오브젝트를 DB에서 꺼내야 한다.

    request의 파라미터로 받은 id는 db에 저장된 비디오의 id이므로 DB에서 findById()로 검색한다.

    db접근은 외부와의 통신이기 때문에 async/await 키워드를 써주고 DB에서 꺼내온 video객체의 몸통을 확인하기 위해 화면에 뿌려준다.

    watch.pug파일로 적절한 데이터를 보내고 watch.pug파일도 수정해준다.


    원하는 화면이 나온다.

  • 공식 홈페이지에 보면 .exec()가 뒤에 붙는데 쿼리문을 작성할때 사용되는 메소드이다. 쿼리문을 쓰지 않을꺼면 생략해도 결과는 같다.

  • 사이트의 이동은 사이트에 표시된 버튼으로 이동할 수 있지만 사용자가 url을 직접 수정하여 이동할 수도 있다.
    만약 url의 id부분을 수정했는데 존재하지 않는 비디오페이지라면 어떻게 될까? 당연히 페이지가 표시되지 않는다.

    url의 id파라미터 수정 -> id파라미터를 db에서 검색 -> 결과가 나오지 않음 -> 페이지 랜더링에 문제생김

    이 과정은 db에러는 아니다. 이 에러부분을 코딩으로 막아보자.

    보통 에러부분을 먼저 거르고 뒤에 정상코드를 적는다.

    if안의 return을 빼먹으면 render()가 두번 나올수 있으므로 에러시 함수를 빠져나오게 하는 return은 필수이다.

Edit Video

  • DB에 저장되어 있는 데이터를 Update하기 위한 방법으로는 2가지가 있다.
    첫 번째 방법으로는 오브젝트를 받아와서 업데이트를 한 후에 save()하는 것이다.

    위의 경우 form이 몇개 없어서 코드가 몇줄 되지 않지만, form이 많아질 경우 코드 또한 많아지는 단점이 있다.

    두 번째 방법으로는 몽구스에서 지원하는 함수를 사용하는 것이다.


    두번째 방법이 더 좋다.
    단, 오류가 날 경우도 있으니 방지해준다.

  • 사소한 문제가 있다. upload 컨트롤러와 edit컨트롤러에서 hashtags부분의 코드가 겹친다. 정리가 필요한데 좋은 방법이 있다.

    hashtags 부분의 코드는 폼에 입력된 해시태그를 문자열로 받아와서 자바스크립트에서 array로 변형시켜준 뒤 DB에 넣어주는 조금 복잡한(?) 과정이 있다.

    이 과정은 mongoose의 미들웨어 (또는 Hook이라고 함) 로 처리할 수 있다.
    즉, 몽고DB에 값을 저장하기 전에 특정 작업을 할 수 있다.

  • express와 같이 mongoose의 미들웨어도 모델이 만들어지기 전에 정의를 해야 한다.
    upload 컨트롤러를 보자

    hashtags에 원래 하던 작업을 지우고 간단히 만들었다.
    Hook을 만들어서 처리해보자.

    모델을 만들기 전에 정의했다.
    this라는 오브젝트가 자동으로 설정되는데 이는 도큐먼트 자기자신을 뜻한다. 콘솔로 보면 이렇다.

    hashtags를 처리해주는 코드를 넣는다.

    pre()메서드의 첫 번째 인수로 받은 save는 메서드 이름이다.
    save()메서드가 실행될때 두 번째 인수로 받은 함수를 실행한다.

    create()메서드는 내부적으로 save()메서드가 트리거 되어 실행되지만 findByIdAndUpdate()메서드는 내부적으로 findOneAndUpdate()메서드가 실행된다.
    즉, create()에서는 pre()가 작동하고, findByIdAndUpdate()에서는 pre()가 작동하지 않는다.

    첫 번째 인수로 ["save", "findOneAndUpdate"]와 같이 배열로 받을 수 있는데 이렇게 하면 create()findByIdAndUpdate() 둘 모두 pre()를 작동시킨다.

    하지만 save일때와 findOneAndUpdate일때의 this 구조가 달라진다. 그러므로 위와 같이 일률적으로 this를 사용하면 에러가 난다.
    <예> save일때 this.hashtags에는 값이 들어있지만 findOneAndUpdate일때는 this.hashtags라는 값이 없다.

    우리는 둘 다 제대로 동작하며 반복되지 않는 코드가 필요하다.

  • mongoose의 static()함수를 정의해보자.
    이 함수는 한마디로 도큐먼트 안에 정의되는 함수라고 보면 된다.


    이렇게 하면 uploadedit둘다 하나의 코드로 사용 가능하다.
    뿐만아니라 Video모델 내에 함수를 만들기 때문에 따로 import할 필요가 없다.

Delete Video

  • 삭제 기능을 만들어보자.
    삭제 버튼과 라우터 및 컨트롤러를 만들자.
    컨트롤러 같은 경우 DB에서 id로 도큐먼트를 찾아 삭제후 홈으로 리다이렉트를 하면 된다.

    mongoose에 보면 deleteremove 두개가 있는데 공식홈페이지에서는 웬만하면 delete를 사용하라고 나와있다.
    그 이유로는 몽고db는 롤백이 안되어서 remove를 하면 돌이킬수가 없다고 한다. 그래서 delete를 사용하라고 한다.
  • 몽고DB는 자체 쿼리 엔진을 가지고 있다.

  • 검색 페이지를 만들고 라우터, 컨트롤러를 만든다.
    검색 폼은 GET방식으로 설정한다.

    req.query는 get방식으로 넘긴 변수들이 담긴 오브젝트이다.
    여기서 잠깐 다시한번 짚고 넘어가자면
    req.params는 url의 파라미터를,
    req.query는 get 파라미터를,
    req.body는 post파라미터를 의미한다.

  • 컨트롤러를 완성시켜보자.

    find의 인자로 필터 오브젝트를 넘겨주었다. 필터 내용으로는 타이틀을 대상으로 정규표현식을 사용했다.
    RegExp(keyword, "i")는 키워드가 포함된 타이틀을 뜻하고
    RegExp("^"+keyword, "i")는 키워드로 시작하는 타이틀을 뜻하고
    RegExp(keyword+"$", "i")는 키워드로 끝나는 타이틀을 뜻한다.

    $regex는 regex Operator라고 부르며 몽고디비옵션에서 찾아볼 수 있다.
    <예> $gt:3은 greater than 3 으로, 3보다 크다 라는 뜻을 지녔다.

profile
프론트에_가까운_풀스택_개발자

0개의 댓글