2021년 8월 3일
지금까지 만든 웹 페이지는 비디오를 업로드 할 때 다음과 같은 정보를 입력하도록 했습니다. 물론 기초를 잡기 위해 임시로 만든 업로드 폼이며, 본격적인 클론 코딩을 시작하면 좀 더 세련되게 다듬을 예정입니다.
해쉬태그의 경우 나머지 두 개의 입력값보다 복잡한 처리를 거쳤습니다. 최초 업로드와 기존 데이터 수정의 경우에 모두 적용해주었는데요,
#
를 붙이면(예를 들어 #바다
) => 그대로 DB에 적용#
를 안 붙이면(예를 들어 바다
) => #
를 붙여 DB에 적용이런 처리를 다음과 같은 자바스크립트의 삼항 연산자로 구현해주었습니다.
여담으로, 이제부터 포스팅에 코드를 첨부할 때 비쥬얼 스튜디오 코드의 PolaCode
라는 익스텐션을 사용하기로 했습니다.😉 돌이켜보면,
순서대로 포스팅에 소스코드를 넣는 방법이 변했습니다. Carbon은 사진 크기가 너무 크고, Markdown은 문법 강조 기능이 부족해서 아쉬웠는데 PolaCode는 앞으로 오래 사용할 것 같은 기분이 듭니다.
다시 삼항 연산자 이야기로 돌아와서, 저 코드는 총 두 번을 반복하여 써야 합니다. 첫 번째는 업로드 때, 두 번째는 수정 때입니다. 따라서 코드 재사용성을 높이기 위해 고안할 수 있는 첫 번째 방법은 해쉬태그 문자열을 처리하는 함수를 만들어 업로드와 수정 페이지 랜더를 담당하는 컨트롤러에게 넘겨주는 것입니다. 그러나 mongoose를 사용하고 있기 때문에 더 좋은 방법을 사용할 수 있습니다.
몽구스에서는 각 스키마에 static()
을 통해 함수를 달아줄 수 있습니다. 예를 들어, videoSchema
에 함수를 달아주려면 videoSchema.static("함수이름", 함수)
와 같이 사용하면 됩니다. 이렇게 스키마에 함수를 달아주면 나중에 Video
모델을 활용해 Video.함수이름()
과 같이 함수에 접근할 수 있게 됩니다. videoController.js
파일에서는 이미 Video
모델을 import
하고 있으므로, 훨씬 간편하게 해쉬태그 처리를 구현할 수 있을 것입니다.
Video.js
파일에서 videoSchema
를 정의한 코드 밑에 다음과 같이 작성합니다.
함수 이름은 formatHashtags
로 설정합니다. 나중에 Video.formatHashtags
와 같이 함수에 접근할 수 있게 됩니다. 그리고 함수를 이전과 동일한 논리로 해쉬태그를 처리할 수 있게 정의합니다.
이제 videoController.js
파일에 있는 postUpload
와 postEdit
컨트롤러에 작업을 해줍니다. 원래 있던 해쉬태그 문자열 처리 부분을 삭제하고 다음과 같이 Video
모델에서 위에서 만든 함수에 접근합니다. 비디오 document를 생성하는 부분만 가져왔습니다.
사용자가 입력한 hashtags
데이터는 req.body
에서 파싱되어 formatHashtags()
함수를 거쳐 데이터베이스에 저장됩니다. 이제 해쉬태그 문자열을 처리하는 부분을 다듬었으니, 홈 페이지와 시청 페이지에서 비디오 정보와 함께 해쉬태그도 함께 보여주도록 하겠습니다. 비디오 리스트 안에서 각각의 비디오 객체의 정보를 화면에 표시하는 것은 video
mixin이 하는 역할이므로 /mixins/video.pug
를 수정합니다.
다른 부분은 이전 코드와 동일합니다. p
태그로 이루어진 비디오 설명 밑에 pug 반복문을 사용해서 해쉬태그들을 리스트로 배열합니다. 홈 페이지에 들어가보면 사용자가 입력한 해쉬태그들이 정상적으로 보입니다.
이제 각각의 비디오 시청 페이지에 '비디오 삭제' 버튼을 만들고 DB에서 비디오를 삭제할 수 있는 기능을 구현해보겠습니다. 우선, 제일 간단한 것부터 시작해보겠습니다. 홈 페이지에서 비디오 제목을 누르면 가게 되는 watch.pug
파일에 삭제 버튼을 달아줍니다.
맨 밑에 a
태그로 삭제 버튼(링크)을 만들어주었습니다. 사용자가 이 버튼을 누르면 /videos/비디오ID/delete
요청을 서버에 보내게 됩니다. 이 요청을 videoRouter.js
파일에서 잡아주도록 하겠습니다. 해당 URl의 get
요청을 처리하는 코드를 마지막 줄에 작성해줍니다.
그리고 videoRouter.js
의 import
구문에 getDelete
를 추가합니다. 이제 아직 존재하지 않는 getDelete
컨트롤러를 videoController.js
파일에 만들어줍니다.
사용자가 삭제 버튼을 누르면 해당 비디오가 DB에서도 사라져야 합니다. 삭제 기능은 /video/비디오ID/delete
요청으로 서버에 보내지므로 URL에 포함된 비디오 ID 파라미터를 통해 비디오 데이터의 ID를 알 수 있습니다. 따라서 몽구스의 findByIdAndDelete()
API를 사용해 간단하게 DB에서 해당 비디오를 삭제할 수 있습니다.
req.params
에서 URL에 포함된 비디오 ID를 얻어옵니다. 그리고 ID를 기반으로 DB에서 동영상을 검색해 삭제합니다. 이 역시 외부 DB 서버에서 이루어지는 작업이므로 await
를 사용해 삭제를 기다려줍니다. 삭제가 끝나면 사용자를 홈 페이지로 리디렉트합니다.
지금까지 업로드, 수정, 삭제 기능까지 구현했습니다. 마지막으로 키워드를 통해 DB에서 비디오 데이터를 검색할 수 있는 기능을 구현해보겠습니다. 검색 기능을 구현하는 순서는 다음과 같습니다.
base.pug
를 확장하여 검색 페이지를 만듭니다.base
의 nav
태그 내부에 /search
로 이동할 수 있는 a
태그를 만듭니다./search
요청을 globalRouter
에서 받습니다.videoController.js
파일에 만듭니다.search.pug
파일에는 다음과 같은 내용이 들어가야 합니다.
우선 extends
를 사용하여 base.pug
를 템플릿으로 확장합니다. 검색된 비디오를 나열할 때 비디오 정보를 표시하는 양식은 mixins/video.pug
에 만들어두었으므로 include
를 통해 불러옵니다.
그리고 base
의 content
블럭에 검색 키워드를 입력하고 제출할 수 있는 폼을 만듭니니다. 키워드와 매치되는 검색 결과는 여러개일 수 있으므로 pug의 반복문을 사용하여 비디오 데이터 배열에서 비디오 데이터를 하나씩 가져와 video
mixin에 대입합니다.
상대적으로 가벼운 작업입니다. base.pug
의 nav
태그 내부 리스트에 검색 페이지로 가는 버튼을 만들어줍니다. 이왕 만들어주는 김에 홈으로 돌아가는 버튼도 만들겠습니다.
이제 사용자가 비디오 검색 버튼을 누르면 /search
요청이 서버로 들어옵니다. globalRouter
에서 그 요청을 받도록 하겠습니다. globalRouter.get("/search", search);
를 이용해서 요청을 받습니다.
이렇게 해서 글로벌 라우터가 처리하는 경로는 총 4개가 되었습니다. /search
요청은 search
컨트롤러로 처리할 예정이므로 글로벌 라우터의 import
문에서 search
를 추가로 가져옵니다.
이제 마지막으로 가장 중요한 컨트롤러를 만들어주겠습니다. 비디오 검색 페이지를 담당하는 컨트롤러는 다음과 같은 기능을 해야 합니다.
우선 사용자가 검색 페이지에서 제출한 폼에서 검색 키워드를 받아와야 합니다. const { keyword } = req.query;
로 사용자가 입력한 키워드를 변수에 저장합니다.
지금까지 POST
방식을 사용하는 비디오 업로드나 수정 폼에서는 데이터를 req.body
에서 얻어왔습니다. 이는 포스트 요청으로 보낸 데이터가 HTTP body 안에 포함되기 때문입니다. GET
요청에서는 사용자의 입력값이 name
과 value
의 쌍으로 URL 안에 쿼리 형태로 들어가기 때문에 req.query
에서 데이터를 가져옵니다. console.log(keyword);
를 통해 검색 버튼을 눌렀을 때 keyword
가 가져오는 값을 확인해보면 검색어를 잘 가지고 오는 것을 확인할 수 있습니다.
이제 검색 키워드를 바탕으로 비디오를 찾아 videos
배열에 넣어주겠습니다. DB에서 비디오를 찾는 기능은 mongoose의 Model.find()
API를 통해 구현합니다. find()
는 쿼리를 인자로 받습니다. 비디오 제목을 기준으로 검색하게 할 것이므로 title
을 쿼리로 넘겨줍니다.
그러나 {title: keyword}
와 같이 키워드를 바로 타이틀에 연결하는 건 그리 세련된 방법이 아닙니다. 만약 이런 방법을 사용한다면 비디오 제목을 풀네임으로 입력해야만 해당 비디오가 검색될 것입니다.
제목의 일부만 입력해도 검색될 수 있게 정규표현식을 사용하겠습니다. 몽고DB는 Evaluation Query Operators
를 지원하는데, 그중 쿼리 안에 정규식을 사용할 수 있는 연산자가 $regex
입니다. {title: {$regex: 정규표현식}}
과 같이 title
을 정규표현식 패턴으로 검색할 수 있습니다.
자바스크립트에서 정규표현식을 사용하는 방법은 두 가지입니다. 하나는 literal notation
이고 다른 하나는 constructor funtion
입니다.
literal notation
: /ab+c/i
constructor function
: new RegExp('ab+c', 'i')
literal notation
같은 경우 정규식이 한 번 컴파일 된 이후에는 다시 컴파일되지 않습니다. 즉, 사용자가 어떤 검색어를 입력할지 모르는 상황에서 사용하기에는 적합하지 않습니다.
constructor function
을 통한 방법은 RegExp()
의 첫 번째 인자인 문자열을 변수로 처리해 정규식을 상황에 맞게 변경할 수 있습니다. 참고로 두 번째 인자인 i
는 대문자와 소문자를 구분하지 않겠다는 뜻입니다.
이제 constructor function
을 사용해 정규식을 쿼리 안에 만들어주겠습니다.
이제 사용자가 입력한 키워드가 비디오 제목 어디에 위치하고 있던 검색이 가능합니다. first video
와 second video
가 있을 때 video
만 입력하면 두 비디오 데이터가 모두 검색됩니다.
보이는 것처럼 홈 페이지에 비디오가 먼저 만든 순서대로 나옵니다. 일반적이라면 최신 비디오가 제일 상단에 위치해야 하므로 홈 페이지에서 비디오를 DB로부터 가져올 때 정렬 순서를 바꿔주도록 하겠습니다. 홈 페이지를 담당하는 컨트롤러 이름은 trending
입니다. 이왕 건드리는 김에 컨트롤러 이름도 home
으로 바꾸겠습니다. globalRouter
에 있는 이름도 바꿔줍니다.
home
컨트롤러는 mongoose의 Model.find()
API에 빈 객체를 넘겨 비디오 데이터를 전부 불러옵니다. 데이터 정렬은 find()
쿼리의 메소드인 sort()
를 사용해 구현합니다. sort()
는 객체를 인자로 받습니다. 정렬 대상이 되는 스키마와 정렬 방법을 쌍으로 주면 됩니다. 지원하는 정렬 순서는 다음과 같습니다.
asc
, ascending
, 1
desc
, descending
, -1
저는 만들어진 시간의 역순으로 배열하고 싶으므로 {createdAt: "desc"}
을 인자로 넘겨줍니다.
이제 홈 화면에서 비디오들이 최신 순서대로 정렬됩니다.
지금까지 프로젝트의 기본 틀을 구현하고 데이터베이스의 CRUD를 구현해보았습니다. 다음 포스팅부터는 로그인 및 유저 인증에 대해 다루어보겠습니다.🔐
<참고 문서>