본문 바로가기

코딩강의/인스타그램클론(expo-노마드코더)

User module(2)

🍔 핵심 내용

 

🥑 file 업로드

apollo server에서는 기본적으로 파일 업로드를 지원해준다. 이를 이용하여 파일 업로드를 해보자.

기존 우리가 만든 스키마를 써서는, apollo server에서 제공하는 파일 업로드를 사용 할 수 없으니, 
우리가 만든 스키마 + apollo server 스키마 이렇게 되도록 만들어 보자.

 

 

🍔 코드 리뷰

 

🥑 schema.js

import { loadFilesSync, makeExecutableSchema, mergeResolvers, mergeTypeDefs } from "graphql-tools";

const loadedTypes = loadFilesSync(`${__dirname}/**/*.typeDefs.js`);
const loadedResolvers = loadFilesSync(`${__dirname}/**/*.resolvers.js`);

export const typeDefs = mergeTypeDefs(loadedTypes);
export const resolvers = mergeResolvers(loadedResolvers);

// const schema = makeExecutableSchema({ typeDefs, resolvers }); (삭제)
// export default schema; (삭제)

마지막 두줄을 삭제 하였다. 마지막 두 줄이, apollo server의 모든 스키마는 저 것으로 한정 짓게 된다. 그래서 삭제 한 것이다. 그래서, typeDefs와 resolvers만 따로 빼오도록 하자.

 

🥑 server.js

require("dotenv").config();
import { ApolloServer } from "apollo-server";
import { typeDefs, resolvers } from "./schema"
import { getUser } from "./users/users.utils";

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: async ({ req }) => {
    return {
      loggedInUser: await getUser(req.headers.token),
    }
  }
});

const PORT = process.env.PORT;

server
  .listen()
  .then(() => console.log(`🚀 Server is running http://localhost:${PORT} ✅`));

apolloServer 값에 typeDefs와 resolvers를 넣어 두었다. 이를 통해, typeDefs + resolvers + apollo server에서 제공하는 스키마까지 쓸 수 있게 되었다. 서버를 열어 스키마 부분을 확인해 보면, 안보이던, Upload가 제공 되고 있다~!

 

 


🍔 핵심 내용

 

🥑 file 업로드 두 번째

바로 전 내용에 스키마에 upload를 설정 하였다. 하지만, 우리 Playground서버에서는 테스트로 file을 upload해볼 수가 없다. 그래서 altair graphQL Clinet 라는 서비스를 이용해보자. 

 

(실제 배포용 서비스에서 파일 업로드는, 서버에 해서는 안되고 aws에 해두어야 한다. 클라이언트 -> 서버 -> aws 이렇게 전달이 가야하고 서버에서는 aws에서 리턴해주는 url값을 받아야 한다. 하지만 이는 테스트용이기 때문에 일단 서버에 파일 올리는 방법으로 해보자.)

 

기본적으로, playground 서버와 거의 유사하다. 일단 테스트를 위해, header 셋팅 부분에 token 값을 넣은 후

mutation을 진행해보자. 여기서 altair에서는 file 업로드를 위해 변수를 지정해 줄 수가 있다. 위 $변수명 참고

그리고 하단 add files 부분에 해당 변수명을 넣고 파일을 올려보면 된다.

 

🍔 코드 리뷰

 

🥑 editProfile.resolvers.js

import client from "../../client";
import bcrypt from "bcrypt";
import { protectedResolver } from "../users.utils";

const resolverFn = async (_, {
    firstName,
    lastName,
    userName,
    email,
    password: newPassword,
    bio,
    avatar
}, { loggedInUser }) => {
    console.log(avatar)
    let uglyPassword = null;
    if (newPassword) {
        uglyPassword = await bcrypt.hash(newPassword, 10);
    }
    const updatedUser = await client.user.update({
        where: { id: loggedInUser.id },
        data: {
            firstName,
            lastName,
            userName,
            email,
            bio,
            ...(uglyPassword && { password: uglyPassword }),
        }
    })
    if (updatedUser.id) {
        return {
            ok: true
        }
    } else {
        return {
            ok: false,
            error: "프로필을 업데이트 할 수 없습니다."
        }
    }
}

export default {
    Mutation: {
        editProfile: protectedResolver(resolverFn)
    },
};

avatar를 추가해주었고 (typeDefs에도 추가해 주었음) console.log(avatar)를 지정해 보고 altair를 실행해보면 아래와 같이 파일이 정상적으로 잘 업로드 된 것을 확인 해 볼 수 있다.

 


🍔 핵심 내용 

 

🥑 버그 수정

 

파일 업로드 관련해서 nodejs는 버그가 있어(node 버전 12 이상만), 수정을 해주어야 한다. 바로 위에, createReadStream을 아래와 같이 실행해보면 버그가 생긴다.

 

altair로 파일 업로드 및 버그 확인

 

createReadStream함수 실행

 

해당 버그를 잡기 위해서는

 

① package.json 파일에 아래 내용 추가

 

  "resolutions": {

    "fs-capacitor": "^6.2.0",

    "graphql-upload": "^11.0.0"

  }

 

이거랑,

 

scripts 부분에 내용 추가

 

    "preinstall": "npx npm-force-resolutions",

 

② node_modules 폴더 삭제

③ npm install로 재설치


🍔 핵심 내용 

 

🥑 서버에 파일 올리기

uploads 파일을 만들어 준다. (다시 한번 더 얘기하자면, 서버에 사진 파일 같은 것을 올리는 거는 지금 학습용이다.)

 

🍔 코드 리뷰

 

🥑 editProfile.resolvers.js

import { createWriteStream } from "fs";
import client from "../../client";
import bcrypt from "bcrypt";
import { protectedResolver } from "../users.utils";

const resolverFn = async (_, {
    firstName,
    lastName,
    userName,
    email,
    password: newPassword,
    bio,
    avatar
}, { loggedInUser }) => {
    const { filename, createReadStream } = await avatar;
    const readStream = createReadStream();
    const writeStream = createWriteStream(process.cwd() + "/uploads/" + filename)
    readStream.pipe(writeStream);
    let uglyPassword = null;
    if (newPassword) {
        uglyPassword = await bcrypt.hash(newPassword, 10);
    }
    const updatedUser = await client.user.update({
        where: { id: loggedInUser.id },
        data: {
            firstName,
            lastName,
            userName,
            email,
            bio,
            ...(uglyPassword && { password: uglyPassword }),
        }
    })
    if (updatedUser.id) {
        return {
            ok: true
        }
    } else {
        return {
            ok: false,
            error: "프로필을 업데이트 할 수 없습니다."
        }
    }
}

export default {
    Mutation: {
        editProfile: protectedResolver(resolverFn)
    },
};

avatar 인자에서 filename 값과 createReadStream(함수)을 가져 온다. 그리고 nodejs에서 제공하는 createWritestream 함수를 이용하여 업로드한 파일을 uploads 폴더에 넣어 주자. mutation을 다시 실행해주면 (파일 업로드) 아래와 같이 폴더에 파일이 들어 간 것을 확인 할 수 있다.

 

 

그리고 여기서 문제는, apollo server에서는 uploads 폴더를 열어서 확인 해볼 수가 없다. 이를 위해 express 서버를 실행할 것이고, 베이스는 express 서버, 그 위에 apollo server를 얹히는 느낌으로 다시 설정 해야 한다.

 


🍔 핵심 내용

 

🥑 express 설치

npm install express  / npm install morgan / npm install apollo-server-express 설치

(morgan은 log 추적을 위한 모듈이다.)

 

🍔 코드 리뷰

 

🥑 server.js

require("dotenv").config();
import express from "express";
import logger from "morgan";
import { ApolloServer } from "apollo-server-express";
import { typeDefs, resolvers } from "./schema"
import { getUser } from "./users/users.utils";

const PORT = process.env.PORT;
const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: async ({ req }) => {
    return {
      loggedInUser: await getUser(req.headers.token),
    }
  }
});

const app = express();
app.use(logger("tiny"))
server.applyMiddleware({ app });


app.listen({ port: PORT }, () => { console.log(`🚀 Server is running http://localhost:${PORT} ✅`) });

express + apollo server 형식으로 재 구성 되었다.

 


🍔 핵심 내용

 

🥑 altair 통해 파일 업로드 -> url db 저장 로직 구성

apollo 서버에서는 파일(여기서는 이미지 사진)을 확인 할 수가 없다. 그래서 일반 express 서버를 통해 이미지 사진을 확인해 볼 것이다.

 

 

🍔 코드 리뷰

 

🥑 server.js

require("dotenv").config();
import express from "express";
import logger from "morgan";
import { ApolloServer } from "apollo-server-express";
import { typeDefs, resolvers } from "./schema"
import { getUser } from "./users/users.utils";

const PORT = process.env.PORT;
const apollo = new ApolloServer({
  typeDefs,
  resolvers,
  context: async ({ req }) => {
    return {
      loggedInUser: await getUser(req.headers.token),
    }
  }
});

const app = express();
app.use(logger("tiny"))
app.use("/static", express.static("uploads"))
apollo.applyMiddleware({ app });


app.listen({ port: PORT }, () => { console.log(`🚀 Server is running http://localhost:${PORT} ✅`) });

 

중간에, /static 경로를 추가해 주었다. express 서버에서 이제 아래와 같이 static 경로가 추가 되었다.

 

 

 

🥑 editProfile.resolvers.js

import { createWriteStream } from "fs";
import client from "../../client";
import bcrypt from "bcrypt";
import { protectedResolver } from "../users.utils";

const resolverFn = async (_, {
    firstName,
    lastName,
    userName,
    email,
    password: newPassword,
    bio,
    avatar
}, { loggedInUser }) => {
    let avatarUrl = null;
    if (avatar) {
        const { filename, createReadStream } = await avatar;
        const newFilename = `${loggedInUser.id}-${Date.now()}-${filename}`
        const readStream = createReadStream();
        const writeStream = createWriteStream(process.cwd() + "/uploads/" + newFilename)
        readStream.pipe(writeStream);
        avatarUrl = `http://localhost:4000/static/${newFilename}`
        console.log(avatarUrl)
    }
    let uglyPassword = null;
    if (newPassword) {
        uglyPassword = await bcrypt.hash(newPassword, 10);
    }
    const updatedUser = await client.user.update({
        where: { id: loggedInUser.id },
        data: {
            firstName,
            lastName,
            userName,
            email,
            bio,
            ...(uglyPassword && { password: uglyPassword }),
            ...(avatarUrl && { avatar: avatarUrl })
        }
    })
    if (updatedUser.id) {
        return {
            ok: true
        }
    } else {
        return {
            ok: false,
            error: "프로필을 업데이트 할 수 없습니다."
        }
    }
}

export default {
    Mutation: {
        editProfile: protectedResolver(resolverFn)
    },
};

 

newPassword와 비슷하게, avatar도 if 구절을 이용하여 진행 해준다.

avatar 파일 중복 방지를 위해, 고유 파일명으로 저장 될 수 있도록 newFilename 값을 만들어 주고, 이를 db의 avatar 필드에 url 주소가 들어갈 수 있도록 설정해주자. 그리고 서버에도 해당 파일이 들어가도록 설정해주었다.

 

다시 한번 더 얘기하자면, 이는 upload 로직이 제대로 되는지 확인하기 위해 자체 서버에 파일을 업로드를 해보는 것이다. 실제 서비스는 aws를 이용해야 한다.

 

위와 같이 설정 한 후, 이전과 같이 altair에 파일을 올려보면 서버에 해당 이미지가 업로드 된 것을 확인 할 수 있다.

(db의 avatar 필드에도 주소값이 들어가 있음)

 


🍔 핵심 내용

 

🥑 프리즈마를 이용하여 following, follower 기능 구현하기

prisma에서는 기본적으로 2개 값의 relation값을 셋팅할 수 있다. 이를 이용해 following과 follower 기능을 만들어 보자.

 

🍔 코드 리뷰

 

🥑 schema.prisma

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id         Int      @id @default(autoincrement())
  firstName  String
  lastName   String?
  userName   String   @unique
  email      String   @unique
  password   String
  bio        String?
  avatar     String?
  followers  User[]   @relation("FollowRelation", references: [id])
  followings User[]   @relation("FollowRelation", references: [id])
  createdAt  DateTime @default(now())
  updatedAt  DateTime @updatedAt
}

followers와 following를 추가해 주었다. 사용방법은 위 내용 참고.

아래와 같이, studio 화면을 보면 확인 해 볼 수 있다. 다음 장에서는, resolver를 이용하여 셋팅해보자.

 

'코딩강의 > 인스타그램클론(expo-노마드코더)' 카테고리의 다른 글

Photos Module(1)  (0) 2021.03.31
User module(3)  (0) 2021.03.27
User module(1)  (0) 2021.02.19
backend 기본 셋업 (3)  (0) 2021.02.15
backend 기본 셋업 (2)  (0) 2021.02.11