본격적인 인스타그램 클론코딩 시작이다.
시작에 앞서 전에 만들었던 prisma폴더와 movies폴더와 db를 삭제해준다. ==> 이렇게 하면 기존의 설치했던 것들을 그대로 가져올 수 있으나 나는 프로젝트를 새로 생성해 보겠습니다.
git init
npm init -y
package.json 생성된다.
VSC에서 해당 폴더를 열고 package.json에서 main은 사용하지 않으므로 지워주고 script에서 test도 지워주고 dev만 빈칸으로 추가해준다.
apollo와 graphql을 설치해준다.
npm i apollo-server graphql
js가 변화하고 저장됨에 따라 자동으로 서버를 재시작하게 해주는 nodemon을 설치할 것이다.
npm i nodemon --save-dev
package.json의 script의 dev에 nodemon의 명령어를 추가해준다.
"scripts": {
"dev": "nodemon --exec node server.js"
},
npm install --save-dev @babel/core
npm install @babel/preset-env --save-dev
npm install @babel/node --save-dev
babel.config.json 파일을 만들고 아래처럼 추가한다.
// babel.config.json
{
"presets": [
"@babel/preset-env"
]
}
package.json의 script의 dev를 아래와 같이 수정한다.
// package.json
"scripts": {
"dev": "nodemon --exec babel-node server.js"
},
터미널에서 prisma를 설치한다.
npm install prisma --save-dev
npx prisma init
npm install @prisma/client
새로운 파일들이 생긴다. prisma는 default로 postgresql과 대화하게 되어있다.
이 작업을 마치면 prisma/schema.prisma파일과 .env파일이 생성됨
package.json에 migrate와 prisma studio 관련 세팅을 추가.
// package.json
"scripts": {
~~~
"migrate": "npx prisma migrate dev",
"studio": "npx prisma studio"
},
Prisma studio 설치
prisma studio는 visual editor이다.
npx prisma studio
graphql-tools 설치
**graphql-tools 설치는 제 강의 기준(2022-01-12)에는 업데이트가 되면서 설치 방식이 변경되었다.
npm i graphql-tools <= 설치해도 되나 아래세개를 따로 설치해줘야 동작함
npm i @graphql-tools/schema
npm i @graphql-tools/load-files
npm i @graphql-tools/merge
npm install dotenv
.env를 읽기 위해서는 dotenv설치가 필요하다.
InstaClone을 위한 prisma를 다시 생성
npx prisma init
User 모델 생성
//schema.prisma
model User{
id Int @id @default(autoincrement())
firstName String
lastName String? (선택사항)
username String @unique
email String @unique
password String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
username과 email은 중복되서는 안되기 때문에 @unique 사용
User 관련 typeDefs와 resolvers 구현
// users.typeDefs.js
import { gql } from "apollo-server";
export default gql`
type User {
id: String!
firstName: String!
lastName: String
username: String!
email: String!
createdAt: String!
updatedAt: String!
}
type Mutation{
createAccount(
firstName: String!
lastName: String
username: String!
email: String!
password: String!
): User
}
type Query{
seeProfile(username: String!): User
}
// users.mutations.js
import bycrypt from "bcrypt";
import client from "../client";
export default {
Mutation: {
createAccount: async (_, {
firstName,
lastName,
username,
email,
password,
}) => {//prisma 는 promise를 리턴 하므로 async, await 사용해야한다.
try {
const existingUser = await client.user.findFirst({
where: {
OR: [//fillter는 대문자를 사용햐여한다.
{ username },
{ email },
]
}
});
if (existingUser) {// 이미 있는 user이면 error
throw new Error("This username/email is already taken.");
}
const uglyPassword = await bycrypt.hash(password, 10);
return client.user.create({
data: {
username,
firstName,
lastName,
email,
password: uglyPassword,
}
});
} catch (err) {
return err;
}
}
}
}
위 users.mutations.js 정리
Prisma를 이용한 작업은 Promise를 리턴하기 때문에 코드에서 즉시 이뤄지지 않는 것을 뜻한다.
성공한다면 성공했을때의 data를 실패한다면 실패했을때의 data를 리턴
따라서 코드가 실행되면 정보를 확인하러 DB로 가고 다시 돌아와야 하기 때문에 기다려야 한다.
이를 위해 async await를 사용
단 return할때는 prisma가 끝날때까지 자동으로 기다려주기 때문에 await를 사용할 필요가 없다.
OR : 이는 조건들의 Array을 필요로한다.
위 코드에서는 OR에 username=username이란 조건과 email=email이란 조건이 존재하는 Array를 매칭해줬다.
따라서 Prisma는 username이 존재하거나 email이 존재 할때 그 user를 가져온다.
이를 이용해 Error작업을 해줬다.
Hash Password
npm i bcrypt
findFirst와 findUnique의 차이
findFirst는 모든 필드를 이용해서 필터링 가능함
findUnique는 unique필드만 이용해서 필터링 가능하다. ex) id,@unique로 지정한 필드 ...
DB에서 User를 Read한다.
여기서 findUnique는 unique한 필드에만 적용할 수 있다.
//users.queries.js
import client from "../client";
export default {
Query: {
seeProfile: (_, { username }) => client.user.findUnique({
where: {
username,
}
})
}
};
http://localhost:4000 접근하면 사용할 수 있음.
http://localhost:5555 => Prisma Studio
token은 서버가 프론트엔드에 연결되어 있지 않을 때나 다른 장소에 있을 때 사용한다.
여기서는 JWT를 이용한다.
누구나 token안을 볼 수 있다. 따라서 token에는 비밀정보를 담으면 안된다.
token의 목적은 토큰안에 정보를 넣고, 그 토큰이 우리가 싸인했던 토큰인지 확인하는 것이다.
sign을 할 때 private_key와 함께 싸인을 하게되는데 이 key는 절대 노출되면 안된다. 노출되면 그 key 주인의 토큰을 함부로 만들 수 있다.
구현
- 가져온 username과 일치하는 user를 DB에서 찾는다. (없다면 error를 리턴)
- user가 존재한다면 password를 비교한다. await bcrypt.compare(password, user.password);
- password까지 일치한다면 로그인이 성공할 경우 -> 토큰 발급 await jwt.sign({ id: user.id }, process.env.SECRET_KEY);
.env 에 Secret key 추가 (https://randomkeygen.com => CodeIgniter Encryption Keys에서 하나 선택)
SECRET_KEY=m66GLSw301mqFwwEb5yo2Y7uDSsAC9va
//users.typeDefs.js ==> 코드 추가
type LoginResult {
ok: Boolean!
token: String
error: String
}
type Mutation {
~~~~
login(username: String!, password: String!): LoginResult!
}
//users.mutations.js
login: async (_, { username, password }) => {
const user = await client.user.findFirst({ where: { username } });
if (!user) {
return {
ok: false,
error: "User not found"
}
}
const passwordOk = await bcrypt.compare(password, user.password);
if (!passwordOk) {
return {
ok: false,
error: "Incorrect password"
}
}
const token = jwt.sign({ id: user.id }, process.env.SECRET_KEY);
return {
ok: true,
token,
};
}
Secret Key 발급 사이트 -> https://randomkeygen.com
JWT 해석 : https://jwt.io
//users.typeDefs.js ==> 코드 추가
type EditProfileResult {
ok: Boolean!
error: String
}
type Mutation {
~~~~
editProfile(
firstName: String
lastName: String
username: String
email: String
password: String
): EditProfileResult!
}
//users.mutations.js
login: async (_, { username, password }) => {
const user = await client.user.findFirst({ where: { username } });
if (!user) {
return {
ok: false,
error: "User not found"
}
}
const passwordOk = await bcrypt.compare(password, user.password);
if (!passwordOk) {
return {
ok: false,
error: "Incorrect password"
}
}
const token = jwt.sign({ id: user.id }, process.env.SECRET_KEY);
return {
ok: true,
token,
};
}
위에는 한 파일에 다 적어서 동작시키고 아래는 코드를 분리해서 동작시킴
users 폴더 아래에 각 동작에 맞는 세부폴더를 생성한다.
users > editProfile > editProfile.mutations.js, editProfile.typeDefs.js
생성하고 users.mutations.js, users.typeDefs.js에 있는 코드를 나눈다.
//editProfile.typeDefs.js ==> 코드 추가
import { gql } from "apollo-server";
export default gql`
type EditProfileResult {
ok: Boolean!
error: String
}
type Mutation {
editProfile(
firstName: String
lastName: String
username: String
email: String
password: String
): EditProfileResult!
}
`;
//editProfile.mutations.js ==> 일단 되는지 확인하는 코드를 작성
export default {
Mutation: {
editProfile: () => console.log("hi"),
}
}
이제는 xxx.queries.js, xxx.mutations.js로 나눌 필요가 없다.
합쳐서 xxx.resolvers.js로 한다.
editProfile.mutations.js ==> editProfile.resolvers.js 로 변경한다.
schema.js에서 **/*.{queries,mutations}.js => **/*.resolvers.js
로 변경한다.
users 폴더 아래에 createAccount, editProfile, login, seeProfile 폴더를 생성한다.
createAccount.resolvers.js, createAccount.typeDefs.js 파일 생성
editProfile.resolvers.js, editProfile.typeDefs.js 파일 생성
login.resolvers.js, login.typeDefs.js 파일 생성
seeProfile.resolvers.js, seeProfile.typeDefs.js 파일 생성
이제 users.typeDefs.js와 users.mutations.js에 있던 코드를 각 파일로 분리한다.