Search
▪️

SNS 서비스 만들기

dotenv 사용하기

실제로 개발을 하다 보면, 노출 되어서는 안 되는 값들이 존재하고, 따라서 이를 편하게 관리할 수 있게 하는 Package가 있다. '.env'라서 dotenv라고 부르며 해당 npm install dotenv를 통해서 Package를 설치하여 이용한다.
Package를 설치했다면, '.env'라는 File을 생성한다. 사용할 ENV 변수들을 생성해둔 '.env' File 내에 선언한다.
Import할 때는, require('dotenv').config()를 통해서 수행한다. 이를 통해서 .env에 정의한 변수들을 이용할 수 있다. (위 config() Method를 수행하게 되면, dotenv Package는 '.env'에 선언해둔 변수들을 process.env에 들어가게 해준다. 따라서 '.env'에 선언한 변수에 접근하기 위해서 process.env.$variableName으로 접근한다.)

Many To Many 이해하기

Sequelize에서 Many To Many를 정의하기 위해서 belongsToMany() Method를 이용했었다. 이 때, Many To Many에 대해서 기록하는 일종의 Matching Table이 필요한데, 이를 through Key 값으로 Matching Table의 이름을 할당한다.
이와 같이 Matching Table을 두게 되면 MySQL에는 Matching Table 역시 생성되며, 두 Model에 대한 Matching 정보를 기록하게 된다.
그렇다면 두 Model에 대한 Many To Many는 Matching Table에 기록되어 관리된다고 했을 때, 하나의 Model을 통해서 Many To Many를 구성하게 되는 경우는 어떻게 되는가? → 똑같이 through라는 Key를 통해 Matching Table을 두되, 하나의 Model간 이뤄지므로 구분이 필요하다. 따라서 각 belongsToMany() Method의 인자로 받는 Model을 Alias하고 Foreign Key를 지정한다. 아래 예시와 같으나, Code를 참고하도록 한다.
modelA.belongsToMany(modelB, {through: $matchingTable, as: $aliasOfModelB, foreignKey: $idOfModelA})
** Many To Many의 경우는 어떤 것에 대해서 belongsToMany() Method를 써도 무방하다. 하지만 One To One, One to Many의 경우 순서가 중요하다.

passport 세팅과 passport-local 전략

일반적으로 Social Login과 더불어 Local Login을 지원하기 위해서 사용하는 것이 passport Package이다. passport Package에서 Local Login을 위해서 passport-local, passport Package에서 Social Login을 위해서 passport-$snsName을 이용한다.
또한 crypto의 Module보다 더 나은 보안을 위해 bcryptjs 혹은 bcrypt Package를 이용한다. (bcrypt의 경우 Native Module로써 C++로 작성되었고, bcryptjs의 경우 Pure JavaScript Module이다. 따라서 bcrypt가 더 빠르지만, 경우에 따라서 추가 Dependencies가 필요할 수도 있다.)
passport Package를 app.js에 Import하여 사용하도록 한다. 또한 passport의 폴더 구조는 index.js를 두고, 다른 Login Method의 Script를 index.js에 Import하여 활성화 시키는 방식이다. app.js부터 Tracking하여 Code를 참고하자.
Social Login의 원리는 다음과 같다. 타사에서 사용자의 인증을 대신 해주어, 신뢰성을 가진 사용자임을 검증해주는 방식이다. 예를 들면, Social Login으로써 카카오를 진행한다고 가정하자. Social Login 버튼을 통해서 카카오에 로그인을 하게 되면 카카오에서 로그인 정보에 따라서 인증 절차를 밟은 뒤, Valid한 사용자가 맞다면 사용자의 정보 일부분을 특정 Code와 함께 Return 하게 된다. 따라서 사용자의 정보를 활용하게 되는 것이다.
정리하여 보자면, app.js에는 passport Package 및 passport 폴더의 index.js를 Import하게 되고, passport.initialize() 및 passport.session() Method를 사용하는 Middleware를 두게 한다. initialize()의 경우, Passport의 설정들을 초기화하는 역할을 한다. session()의 경우, Login시 Session을 생성하게 되면 해당 Session 정보들을 활용하는 Method이다. (따라서 express-session의 설정보다 하위에 있어야 한다.)
위와 같이 app.js에 설정이 끝났다면, passport 폴더의 index.js에서 사용되는 Strategy들의 로직 정의가 필요하다. Strategy에 사용되는 passport.use() Method는 안에 Strategy Object를 인자로 주며 Strategy의 usernameField와 passwordField들을 req.body내에 존재하는 Field Name과 연결해줘야 한다. (Strategy Object는 passport-local 혹은 passport-$sns를 Import한 후, '.Strategy'를 통해서 이용할 수 있다.)
req.body의 Field Name과 Strategy Object의 usernameField, passwordField와의 연결이 끝났다면, 로직을 수행할 Callback Function을 작성하여 인자로 주면 된다. 해당 Callback Function의 특이한 점은, done() Method를 인자로 갖는다는 점이다. Strategy의 done() Method에는 Error, Pass, Fail을 처리할 수 있는 로직이 있기 때문에 이를 적극 활용하도록 한다.
done() Method는 (error, pass, fail)과 같은 순서로 Format을 이룬다. 따라서 done()에 단순 인자로 (error)를 넣으면 Login 검사도 하지 않고 Error에 의한 것으로 Fail 처리를 한다. 이와 달리 done()에 (null, 사용자 정보)를 인자로 넣으면 Pass를 처리, (null, false, Fail 정보)를 인자로 넣으면 Fail 처리를 하게 된다. (사용자 정보와 Fail 정보는 Object로 준다.)

Login Logout 구현

위에서 구현한 로직을 passport.authenticate()라는 Method로 이용할 수 있는데, 이 Method의 Callback Function의 인자는 done() Method의 인자들과 직결된다. (즉, done() Method가 전달해주는 것이다.)
passport Package를 이용하게 되면, Request에 login이라는 Method가 자동으로 추가된다. 즉, Strategy를 수행하면서 done()을 통해서 받은 (error, pass, fail)값들을 authenticate() Method의 Callback Function에서 확인하고, 확인 결과 이상이 없다면 login() Method를 활용하게 되는 것이다.
passport를 통해서 자동으로 추가된, Request의 login() Method에는 사용자 정보와 Error처리 Callback Function을 인자로 받는다. 이렇게 login() Method를 통해서 사용자가 Login이 되었다면, Session에 사용자의 State가 기록된다. 또한 이렇게 사용자의 State가 Session에 기록이 되었다면, Request의 user Field에서 확인할 수 있다.
** passport Package가 Request에 자동으로 추가해주는 Method는 login()외에도, logout() Method, isAuthenticated() Method등이 있다.
** Logout 시에는 Request의 logout() Method만 이용하는 것이 아니라, Session에 대해서도 지울 수 있다. (req.session.destory()를 통해서 말이다.) 하지만 Logout 할 때, Session에 대해서 지우지 않아도 무방하다. 다른 Session들도 같이 지워지기 때문이다.

passport serializeUser / deserializeUser

위 과정대로 passport와 Strategy에 대한 Import, passport.initialize(), passport.session()을 app.js에서 수행했고, Login에 대한 로직을 Strategy에 작성함과 동시에 Request의 Body의 Field Name에 연결이 되었고, 마지막으로 Strategy 이용에 대한 passport.authenticate()의 로직을 작성했다면, Serialize와 Deserialize가 필요하다.
SerializeUser와 Deserialize User는 Strategy를 관장하는 index.js에서 사용하게 된다. 가장 중요하다!
Request의 login()의 인자로 사용자를 넣어주고, 이를 통해서 Login을 하게 되면 Session에 사용자 정보가 들어간다. (즉, Session에 사용자 정보를 두기 위해서 사용자를 인자로 넣어주는 것이다.) 이 때, 사용자의 정보를 Session에 넣을 때 실행되는 것이 passport.serializeUser() Method인 것이다. 실제로 사용자의 모든 정보를 Session에 두기에는 사용자가 늘어나면 늘어날수록 부하가 커진다. 따라서 이 중에서 ID 값만을 Session에 둘 수 있도록 돕는 것이 SerializeUser인 것이다. Code를 참고하자.
그렇다면 DeserializeUser는 무엇인가? → 사용자가 Login이 된 상태에서, 즉 SerializeUser를 통해서 Session에 사용자의 ID만 저장이 된 상태에서 사용자가 Request를 보내게 되면 passport.session()에 한 번 걸리고, 이 상태에서 passport.deserializeUser() Method가 실행된다. DeserializeUser는 Session내에 존재하는 사용자의 ID값을 DB에서 조회 후에, 사용자의 정보들을 Request의 User에 담아주는 역할을 하게 되는 것이다.
예를 들어서 확인해보면, {id : 1, name: Jason, age: 26}와 같은 정보가 Request의 login() Method의 인자로 들어오면서 Login함과 동시에 Strategy의 SerializeUser를 통해서 Session에는 ID 값인 1만 저장하게 하는 것이다. 그리고 사용자가 Login한 상태에서 Request를 보내게 되면 passport.session()에서 Strategy의 DeserializeUser를 통해, Session에 저장되어 있는 ID 값인 1을 데이터베이스에서 조회한 후 {id : 1, name: Jason, age: 26}를 취득하여 Request의 user Field에 채우게 되는 것이다.
** SerializeUser의 경우 Login하여 Session을 만들 때, 한 번만 호출하게 된다. 하지만 DeserializeUser의 경우, 매 Request마다 실행되게 된다. 따라서 DeserializeUser의 경우 DB 조회에 대한 효율성을 높여야 하므로 Caching이 필요하다. (별도의 Caching처리를 모른다면, 메모리에 상주 시키는 것으로도 구현할 수 있다.)

Kakao Login (passport-kakao)

일반적으로 SNS를 통해서 Login을 하게 되면 사용자 정보 Profile 이외에도, SNS에서 인증을 거쳤다는 Access Token 및 Refresh Token 등을 발급하게 된다. (Strategy의 Option이 서로 다르니 Code를 참고하자.)
/auth/kakao라는 Route Path를 통해서 Kakao로 인증 요청을 보내면, Kakao에서 인증을 마친 후에 Redirect Url인 /auth/kakao/callback으로 Access Token, Refresh Token, Profile 등을 Return하게 된다. (Path Name은 달라져도 무방하다.) Kakao의 경우 clientID 발급 및 Application 관리를 developers.kakao.com에서 총괄한다.
Request를 보냈을 때, Response에서 데이터만 받게 되는 REST API구조이므로 REST API Key를 활용한다. (clientID로 작동한다.)

multer로 이미지 업로드하기

multer Package를 통해서 Multipart와 Form Data에 대해서 읽을 수 있게 되는데, 이를 위해선 어느 곳에 저장할지 Storage에 대한 Option을 줘야 한다.
이 Storage Option은 어느 Directory에 File을 저장할지(destination), File 이름은 어떻게 되는지(filename)에 대해서 설정을 갖고 있다. (multer Package의 경우 File 이름을 무작위로 아니라, 확장자도 붙여주지 않기 때문이다.)
destination, filename 두 Field에서 사용되는 cb라는 Callback Function은 인자로 (Error, Result)를 갖는다.
multer()에 대한 설정이 끝났다면, 해당 Method에 Chaining Method로 single(), array(), fields(), none() Method를 사용할 수 있다. (single의 경우 Image 한 개 (Field Name), array의 경우 Image를 여러 개 (단일 Field), fields의 경우 Image를 여러 개 (여러 Field), none의 경우 Image를 사용하지 않고 multer를 이용할 수 있다.)

게시글 업로드 구현하기

서로 Relation을 갖고 있는 Model간 Matching Table에 기록하기 위해선 아래와 같은 Method들을 활용한다.
$instanceOfModelA.get$modelB() ⇒ Matching Table을 조회 $instanceOfModelA.add$modelB() ⇒ Matching Table에 생성 $instanceOfModelA.set$modelB() ⇒ Matching Table에 수정 $instanceOfModelA.remove$modelB() ⇒ 제거