TIL 04 - 인스타그램 클론코딩 (4) User - Image Upload

MOON·2021년 5월 9일
1
post-thumbnail

노마드코더 인스타그램

노마드코더 인스타그램 클론코딩 바로가기 https://nomadcoders.co/instaclone

User관련 Action

  • Create Account ✅
  • Login ✅
  • See Profile ✅
  • Edit Profile ✅
  • Follow User
  • Unfollow User
  • Change Avatar (Image Upload )
  • See Followers with offset Pagination
  • See Following with Cursor-based Pagination
  • Search Users

Image Upload

graphql에서 upload하는 방법

  • apollo에서 지원하는 file upload를 사용
  • 현재 코드는 graphql-tools를 이용했기때문에 upload type이 지원 X
  • upload type 사용하기 위해서는 apollo server에서 스키마를 생성해줘야한다.

graphql-tools를 이용한 schema (현재 코드)

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

const typeDefs = mergeTypeDefs(loadedTypes);
const resolvers = mergeTypeDefs(loadedResolvers);
export const schema = makeExecutableSchema({typeDefs,resolvers});

apollo server에서 schema 생성 (Upload Type을 사용하기 위한 변경 코드)

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

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

// server.js 

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

apollo server를 생성할때 graphql-tools를 이용해 생성한 schema 자체를 받아 오는게 아니라
schema 생성에 필요한 typeDefs와 resolvers를 받아와 apollo server에서 schema를 생성한다.

Altair Graphql Client

  • 기존의 PlayGround도 좋지만 file Upload가 불가능 한 단점이 있다.
  • file Upload를 테스트 하기 위해서는 Altair를 설치 해야 한다.
  • mac : brew install altair-graphql-client
  • 설치하기 싫다면 홈페이지에서 브라우저로도 사용 가능 https://altair.sirmuel.design

File Upload는 어디에 해야할까 ?

2가지 방법
1. 서버에 Upload
2. aws에 업로드

서버에 Upload하는 방법은 절때 실무에서는 해서는 안되는 방법이다.
그러나 여러가지 방법을 알기위해 서버에 Upload 해보자

서버에 Upload

node.js에서 Upload는 Stream과 Pipe로 구현가능
altair에서 파일을 upload하고 editProfile한 뒤 console.log 찍어보면 다음과 같이 나온다.

Promise {
   {
      filename:"15.jpg",
      mimetype:"image/jpeg",
      encoding:"7bit",
      createReadStream:[Function:createReadStream]
   }
}

Promise가 return 되기 때문에 async await를 이용
Promise에서 우리가 사용할 것은 filename과 createReadStream

readStream과 writeStream을 Pipe로 연결해주기

import fs from "fs"

const {filename,createReadStream} = await avatar;
const readStream = createReadStream();
const writeStream = fs.createWriteStream(
   process.cwd() + "/uploads/"+filename
); 
readStream.pipe(writeStream);

-> 코드설명

readStream은 현재 우리의 upload된 파일의 data가 들어오는 stream이고
writeStream은 data의 목적지가 되는 stream이다.
현재는 서버내에 uploads에 파일을 업로드 할 것이기 때문에 uploads의 경로가 필요하다.
이를 위해 process.cwd()를 사용했다. process.cwd()를 console.log해보면 현재 project의 root 경로를 나타내준다.
이렇게 생성한 두 stream을 pipe를 이용해 연결해줘 유저가 파일을 업로드 했을때 우리의 서버 내의 uploads폴더로 오게 되는 것이다.

다시 한번 말하지만 절때 실무에서는 이방법 사용하지 않는다.

파일이 업로드되는 흐름을 정리하자면

유저가 서버에 파일을 업로드하면 나는 서버의 파일을 aws로 파일을 업로드하고 aws는 나에게 url을 가져다 준다.

express 서버를 이용한 ApolloServer 구현하기

이미지를 찾을 수 없는 이유는 현재 우리의 서버는 ApolloServer로 서버가 APolloServer 안에 있기 때문에 static 파일을 활성화 할 수 없는 것이 그 이유이다.
따라서 기존에 사용하던 외부에 노출되있는 express 서버를 이용한다.
express 서버를 생성하고 그 위에 graphqlexpress 서버를 올리는 것이기 때문에 middleware를 추가하던지 바꿀 수있는 모든 것을 할 수 있게 된다.
전의 방법은 apolloserver가 모든 것을 만들었다고 하면 이제는 직접 모든 것을 만들고 그위에 apolloserver를 올리기만 하는 것이다.

npm i express apollo-server-express

//server.js

const app = express();
app.use(logger("tiny"));
server.applyMiddleware({app});
app.listen({port:PORT},()=>{
   console.log(`Server is running on http://localhost:${PORT}/graphql`
});

-> 코드설명

app은 express 서버 이기때문에 어떠한 것이던지 할 수 있다. 따라서 morgan의 logger를 app에 붙여주었고 기존의 server는 apollo-server가 아닌 apollo-server-express로 만든 서버이기 때문에 http://localhost:4000/graphql 이라는 url을 가진다.
기존의 서버 구현 방법과 다른 것이라고는 express 서버를 만들었고 거기에 apollo-server-express를 붙여줬고 약간의 listen방법의 변화 이것 뿐이고 나머지는 다른게 없다.
그러나 이제 우리는 서버가 apolloserver 내부에 갇혀있는게 아니기때문에 어떠한 작업도 서버에 해줄 수 있게 된 것이다.

Static File 활성화


//server.js

app.use("/static",express.static("uploads"));

-> express 서버는 uploads의 모든 파일을 static으로 활성화 하기 때문에 url로 이미지를 검색했을때 이미지가 뜨는 것을 볼 수 있고 /static을 추가해줘 다음과 같은 url로 이미지를 찾을 수 있다.
http://localhost:4000/static/TIL.png

DB로 avatarURL 전송

// editProfile.resolvers.js

let avatarURL = null;
if(avatar){
   const {filename,createReadStream} = avatar;
   const readStream = createReadStream();
   const uploadFileName = `${loggedInUser.id}-${Date.now()}-${filename}`
   const writeStream = fs.createWriteStream(
      process.cwd()+"/uploads/"+uploadFileName;
   );
   readStream.pipe(writeStream);
   avatarURL = `http://localhost:4000/static/${uploadFileName}`;
}

const updatedUser = await client.user.update({
   where:{
      id:loggedInUser.id,
   },
   data:{
      ...(avatarURL && {avatar:avatarURL},
   },
});

-> 패스워드 업데이트하는 것과 같은 방법으로 구현해준다. 또한 파일의 이름은 유일해야 하기 때문에 파일이름을 Custom 해줬다.

에러 발생

file을 업로드하고 얻은 createReadStream 함수를 실행한 결과를 콘솔에 띄웠을때 다음과 같은 경고문이 발생했다.
(node:53998) [DEP0135] DeprecationWarning: ReadStream.prototype.open() is deprecated
(Use node --trace-deprecation ... to show where the warning was created)

-> 해결방법

니꼬쌤 말로는 14버전 이상의 node 사용시에 발생하는 이상한 에러라고 한다.

// package.json
// 1. 맨 아래에 추가 
"resolutions": {
    "fs-capacitor": "^6.2.0",
    "graphql-upload": "^11.0.0"
  }
  
// 2. preinstall script 생성 

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

그다음 node-modules를 삭제 후 npm cache clean --force로 캐시 초기화
npm i 하면 에러가 해결되는 것을 볼 수 있다.

0개의 댓글