유튜브 클론코딩[USER PROFILE]

김동현·2022년 2월 7일
0

nodeJS

목록 보기
6/14

Edit Profile GET

  • 유저 프로필을 업데이트 하는 페이지를 만든다.

  • 비밀번호 수정페이지는 따로만든다.
    왜냐하면 비밀번호를 가지고 있는 유저(일반 로그인 회원)와 비밀번호가 없는 유저(깃헙 로그인 회원)가 있기 때문이다.
    처리해주는 작업이 필요하니까 따로 만든다.

  • 로그인이 되면 미들웨어에서 로그인한 유저정보 세션을 locals에 복사했다. locals는 pug에서 공유가능하다. 따라서 locals에 저장된 loggedInUser를 이용해서 페이지를 작성한다.

  • 로그인하지 않으면 Edit Profile 링크버튼이 생기지 않는다.
    따라서 비 로그인 유저는 클릭으로 Edit Profile페이지에 접속할 수 없다.
    하지만 url에 직접 링크를 입력해서 접속해보자.

오류가 난다. 로그인이 되지 않았기 때문에 locals에 세션정보가 넘어가지 않았고 그렇기 때문에 생긴 오류이다.
세션을 아래와 같이 수정해보자.
즉, 로그인을 하지 않아도 빈 객체가 넘어가도록 수정해보자.


접속이 잘 된다.
로그인 하지 않는 유저는 이 페이지에 접근할 수 없어야 한다.
미들웨어를 사용하여 막아보자.

Protector and Public Middlewares

  • 2개의 미들웨어를 만든다.

    1. 로그인 안했을때 회원전용 페이지에 접속시 로그인페이지로 리다이렉트

    2. 로그인 했을때 비회원 전용 페이지에 접속시 홈페이지로 리다이렉트

  • get()또는 post() 메서드의 인자로 오는 컨트롤러 앞쪽에 미들웨어를 넣어준다.

    route().get().post()코드에서 get과 post 둘다 미들웨어를 사용하려면 위 사진처럼 각각 입력해 줘도 되고

    위처럼 all()함수를 사용해도 된다.

  • /join페이지와 /login페이지에도 각각 미들웨어를 적용한다.

  • videoRouter/edit, /delete, /upload 페이지는 회원계정에만 보여져야 하므로 보호해야 하는 페이지이다.

Edit Profile POST

  • postEdit 컨트롤러를 작성한다.
    /edit페이지의 폼 정보를 토대로 DB의 유저내용을 업데이트 한다. 그럴려면 유저의 id를 알아야 하는데 그 정보는 세션으로부터 온다.
    최신문법으로 req 오브젝트 안의 session과 body를 한번에 받을수 있다.


    컨트롤러를 수정한 후에 실행해보면 페이지가 잘 보인다.

    이메일의 8054를 8053으로 바꾼후 클릭한다.

    DB상에서는 바꼈는데 웹페이지에서는 기존의 이메일을 보여준다.
    왜냐하면 유저정보는 세션으로부터 얻어오는데 DB상의 users 컬렉션의 이메일은 바뀌었지만 DB상의 sessions 컬렉션의 유저정보는 그대로이기 때문이다.

    즉 세션의 유저정보를 받아오는 것인데, 세션은 로그인할때 건들고 그 이후로는 건들지 않았다.
    세션을 업데이트 해야한다.

    세션을 업데이트 하는 방법으로는 크게 2가지가 있다.

    1. 첫 번째는 직접 업데이트 하는 것이다.

      중간의 ...req.session.user 안의 내용을 전부 전해준다.
      왜 이런 작업을 거치냐면 req.session.user 에는 email,name,username,location말고도 avatar_url 등 여러 필드들이 있는데, 이 작업을 거치지 않으면 사라지기 때문이다.

      ...req.session.user를 첫줄에 적어줌으로써 모든 필드들을 전달하고, 나머지 필드들은 덮어쓰기 형태로 전달된다.

      render('edit-profile')로 인해 edit-profile.pug 페이지가 랜딩되는데 이 pug파일에서는 locals안의 user내용을 사용한다.
      locals는 미들웨어에서 변경되기때문에 변경된 유저정보를 랜더링되지 않는다.
      redirect()로 바꾸자.

    2. 두 번째는 findByIdAndUpdate()메소드의 리턴객체를 이용하는 것이다.

      기본상태에서는 업데이트되기 전의 상태를 반환한다고 한다. 하지만 옵션으로 {new:true}를 넣으면 업데이트 후의 객체를 반환한다고 한다.

      이메일과 username은 중복되면 안되니까 중복방지용 코드도 작성한다. 이전에 작성한 코드를 재활용한다.

  • 위 코드는 usernameemail 둘 다 DB에 없어야 변경되는 코드이다.
    만약 email만 변경하려고 한다면 username은 기존 DB에 저장되어있기때문에 변경이 실패된다.

Change Password

  • edit-profile 페이지에 change password 링크를 추가한다.

    userControllergetChangePasswordpostChangePassword를 추가한다.
    getChangePasswordchange-password.pug를 랜더링하고 postChangePassword는 홈으로 리다이렉트 한다.

    userRouter에서 url을 등록한다.

    change-password 뷰를 만든다.

    깃허브를 통해 계정을 만든 경우에는 비밀번호 변경이 안된다.
    비밀번호가 없기 때문이다.
    그럴경우 방법이 2개 있다.

    1. 첫 번째 방법 : change-password페이지를 볼 수 없게 한다.

    2. 번째 방법 : 비밀번호 변경을 못하게 한다.


근데 생각해보니, 깃헙계정에게는 패스워드 수정버튼이 안보이게 하는게 더 낫겠다.

깃헙계정이 아닌 일반계정만 패스워드 수정 링크가 보이도록 하고 postChangePassword 컨트롤러를 작성한다.

새 비밀번호와 새 비밀번호(확인) 이 다르면 안되고, 기존의 비밀번호도 틀리면 안된다.

세션에서 id를 가져온후 DB에서 id를 통해 유저를 찾는다.
그 후 패스워드를 업데이트 한다.

해쉬된 비밀번호를 DB에 넣어야 하는데 save()메소드는예전에 작성한 User 도큐먼트에서 pre('save',...) 메소드를 실행시킨다.

해쉬된 비밀번호가 DB에 업데이트 된다.

비밀번호 변경후엔 로그아웃 페이지를 리다이렉트 한다.
만약 비밀번호 변경후에 로그아웃을 하지않는 코드를 작성하려 한다면, 세션도 업데이트를 해줘야 한다.

File Uploads

  • 유저의 프로필 사진 파일을 업로드 해보자
    input 태그를 이용해서 파일을 업로드 할 수 있다.

    file속성을 사용하는 input태그는 accept라는 속성을 가진다.


    프로필 사진을 업로드 하는 것이기 때문에 이미지 파일만 가능하도록 한다.

    이 파일을 post형태로 서버에 넘겨주기 위해서는 multer 패키지가 필요하다.

    사용법으로는 위와 같다.
    form태그에 enctype="multipart/form-data"를 꼭 써넣어줘야 한다.

  • HTML5에서 enctype은 총 세가지가 있다고 한다.

    • application/x-www-form-urlencoded : 일반적인 form데이터를 보내는데 사용한다.
      &으로 값들을 구분하고 = 로 키와 값을 연결하는 방식을 취한다.
      영어 알파벳이 아닌 글자들은 퍼센트로 인코딩(percent encoding)되어, 이진파일을 보내는데에 적합하지 않다.

    • multipart/form-data : 바이너리 파일을 보낼 때 사용한다고 한다.

    • text/plain : 디버깅용이다. 공백을 +로 인코딩한다.
      실제 서비스 에서는 사용을 지양한다.

  • multipart란 말 그대로, 하나 또는 그 이상의 다른 타입의 데이터들이 하나의 body에 결합된 형태다.

  • 위의 이미지에 나온 설명을 풀이해보자.

    • (req,res,next) 처럼 일반적인 미들웨어가 아니고 multer()생성자에 config오브젝트를 전달함으로 생성되는 특이한 형태의 미들웨어이다.

    • dest는 파일의 목적지를 뜻한다.
      즉 파일을 업로드 하면 서버의 uploads폴더에 저장된다.

    • upload.single()은 미들웨어로써 사용되었다.
      컨트롤러의 앞에 사용된걸 보면 알 수 있다.

  • 미들웨어를 추가하자.


    single() 메서드의 인자인 "avatar"form안의 input태그의 name이다.

  • single("fieldname") 메서드는 이름이 fieldname인 단일 파일을 수락한다.
    단일 파일은 req.file에 저장된다.

  • req.fileconsole.log()로 출력해보자

    이런 형태가 나온다.
    filename은 랜덤 스트링으로 작성된 path이다.
    multer는 파일명(path)을 완전히 랜덤으로 설정한다.

    참고로 미들웨어로 인해서 업로드 폴더에 먼저 파일이 저장된 후 컨트롤러가 실행되기 때문에 프로필 편집이 실패해서 DB에 사용자 정보가 업데이트가 되지 않아도 서버의 uploads폴더에는 이미지가 업로드 된다.

  • uploads폴더는 깃허브에 업로드 되면 안되기 때문에 .gitignore파일에 등록한다.

  • 그런데 아래의 코드에는 문제가 있다.

    file형태의 input태그에 데이터가 입력된 후 post되면 req.file이 생기지만 데이터를 넣지않고 post를 하면 req.file은 없다. undefined가 된다.

    그러니, 기존의 avatarUrl을 가져오고, file이 있다면 file.path를 넣고, 없다면 기존의 avatarUrl을 유지하도록 한다.

    path가 상대경로로 되어있기 때문에 앞에 /를 넣어서 절대경로로 만든다.

    참고할 점은 DB에 파일을 저장하면 안된다.
    DB에는 파일을 저장하지 않는다.
    그럼 뭘 저장하는가? 파일의 위치만 저장한다.
    파일 자체는 아마존의 하드드라이브 같은 곳에 저장한다.

    여기서는 서버내의 폴더(uploads)에 저장한다
    좋은 방법은 아니다.
    서버는 종료되고 시작하기 때문에 파일이 날아갈 수 있다.
    서버가 사라졌다 다시 돌아와도 파일은 그대로 있도록 해야한다.

    그런데 이미지가 보이지 않는다.

    이미지의 url에 들어가보면

    이렇게 나온다.
    라우터에 url이 정의되어 있지 않아서이다.

Static Files

  • 브라우저가 서버의 어떤 폴더든 접근이 가능하다면 당연히 보안상 좋지 않다.
    그래서 우리가 직접 브라우저가 어떤 페이지와 폴더를 볼 수 있는지 알려줘야 한다.

    /uploads url로 접근하면 서버 내의 uploads폴더를 보여주는 코드이다.

  • express.static(root, [options]):
    Express에 내장된 미들웨어 기능이다.
    정적 파일을 제공하며 serve-static을 기반으로 한다.
    root 인수는 static asset을 제공할 root 디렉토리를 지정한다.
    이 함수는 req.url(여기서는 파일의 path을 뜻한다)을 제공된 root 디렉토리와 결합하여 제공할 파일을 결정한다.

User Profile

  • 유저 프로필 페이지를 만들자.
    먼저 base.pug에서 자신의 프로필 페이지로 가는 링크 버튼을 만든다.

    미리 정의해 놓은 /users/:id로 접근이 되며 see 컨트롤러가 실행이 된다.
    profile.pug 파일을 만들고 랜더한다.

  • 페이지에 자신의 정보가 나와야 한다.
    id를 통해 유저 정보를 서칭한다.
    단, 세션으로 id를 가져오는게 아니라 url의 파라미터로 id를 가져와야 한다.
    그 이유는 세션으로 정보를 가져오면 로그인된 자기 자신만 id를 가져올 수 있기 때문이다.
    이 페이지는 모두에게 보여져야 하므로 url의 파라미터를 통해 가져온다.

Video Owner

  • 비디오를 보여주는 페이지를 보면 보통 업로더도 보여준다.
    즉, video DB와 user DB가 서로 연결되어 있다.

  • 비디오 모델에서 owner 필드를 만들고 User 모델의 _id를 참조하도록 한다.
    User 모델의 _id의 형태는 ObjectId 이다.

  • ObjectId 형태는 몽구스에서만 지원하기 때문에 저런식으로 사용하였다. String, Number와 같은 타입은 자바스크립트에서 지원해주기 때문에 mongoose.String과 같이 사용하지 않는다.

    영상을 업로더 할때 로그인된 사용자 정보(즉, 세션의 유저 정보)의 id를 비디오 모델에 같이 넣는다.

    비디오 디비의 owner 필드로 유저 데이터를 찾은다음 watch 파일에 넘겨준다.

    현재 로그인한 유저와 소유자가 같다면 편집과 삭제 링크가 보이도록 한다.

    String()을 넣은 이유는 _id의 데이터 타입이 ObjectId이기 때문에 문자열로 변환시켜 비교하기 위함이다.
    또는 몽구스의 ObjectId 문서에 보면 toStirng()메소드가 지원되는데 이걸로 비교해도 된다.

  • 위와 같은 코드도 좋지만 한 가지 단점이 있다.
    await Video.findById(id)
    await User.findById(video.owner)
    와 같이 DB에 두 번 접근하는 것이다.

    Video모델에는 owner필드가 있고 이건 User모델의 id필드를 참조한다는건 미리 정의해놓았다. 이런 경우, Video만 찾으면 그에 해당하는 User까지 자동으로 찾게 해주는 몽구스의 스마트한 기능이 있다.
    populate() 메소드가 그 기능을 한다.

    video를 콘솔에 출력해보면

    owner 필드에 유저 데이터 자체가 들어간걸 볼 수 있다.
    그러므로 populate()를 사용하면 video와 user 따로 찾을 필요가 없다.

  • watch.pug도 알맞게 수정해준다.

  • 업로더 이름을 누르면 업로더가 올린 영상들을 볼 수 있게 해보자.
    즉, 그 업로더의 프로필 페이지에 접근하도록 하자.

    링크를 걸어주고

    find() 로 하나가 아닌 여러개의 자료를 필터링하여 찾고

    pug 코드를 재사용해서 수정하면

    정상적이게 동작하는걸 볼 수 있다.

  • see 컨트롤러에서 DB에 2번 접근 하는걸 막기위해서는 여기서도 populate()를 사용하면 된다.
    즉, User모델에 리스트 타입의 videos 필드를 만들고, 영상을 업로드 할때마다 업데이트 해주면 된다.
    이렇게 되면 User모델과 Video모델이 서로 참조하는 형식이 된다.

    비디오를 업로드 하는 컨트롤러에서 유저를 찾아 비디오의 id를 유저정보에 추가해준다.

    로그인이 풀리는게 아닌이상 세션도 같이 동기화 한다.

    see 컨트롤러에서 populate()메소드를 이용하여 DB에 1번만 접근하도록 수정한다.

    profile.pug에서 변수를 수정해준다.

Bugfix

  • 위 코드대로 하면 버그가 생긴다.
    비디오를 업로드 할때 유저DB에 비디오의 id를 추가하는 작업에서 user.save()가 실행된다.

    하지만 위의 이미지에서 보다시피, save()할때마다 유저의 비밀번호를 해싱한다. 이러면 어떻게 되느냐?
    아래와 같은 흐름으로 진행하면 문제가 발생한다.

    1. 로그인
    2. 영상 업로드 => 이때 유저의 비밀번호가 자동으로 한 번 더 해싱 된다.
    3. 로그아웃
    4. 로그인(실패) => 비밀번호가 해싱되어서 비밀번호가 달라짐.

그렇다면 비밀번호 변경할때만 해싱되도록 하려면 어떻게 해야하나?

isModified()메소드를 사용한다.

  • 또 하나의 버그가 더 있다.
    바로 업로더가 아니더라도 동영상을 편집/삭제할 수가 있다.
    pug에서 업로더가 아니면 편집/삭제 링크를 안보이게 했지만 url에 직접 들어가면 편집/삭제가 가능하다.
    백엔드에서 처리해줘야 한다.

    세션으로부터 받아온 로그인 유저의 _id와 video데이터의 owner가 다르다면 홈으로 리다이렉트 한다.
    즉, 로그인된 계정이 업로더가 아니라면 페이지에 접근을 불가능하게 한다.

    그런데 업로더 본인인데도 홈으로 리다이렉트 된다. 그 이유는 !== 연산때문이다.
    이 연산은 값 뿐아니라 타입까지 비교하기 때문이다. _id는 string타입이고 video.owner는 object이다.

    가독성 좋게 둘다 String() 메소드를 적용시켜서 문자열로 만들었다.

    참고로 status(403)에서 403은 forbidden(금지) 코드번호이다.

    getEdit 컨트롤러 뿐만 아니라 postEdit 컨트롤러, deleteVideo 컨트롤러에도 똑같이 적용한다.

  • deleteVideo 컨트롤러에서 User DB에 저장된 video의 id들도 삭제한다. 그 후 자동 로그아웃을 하는게 아니라면 세션의 user정보도 동기화 해준다.

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

0개의 댓글