일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 이노베이션아카데미
- 스플릿키보드
- psql extension
- 도커
- 자료구조
- raycasting
- c++
- GraphQL
- 42서울
- schema first
- 정렬
- 어셈블리어
- Cloud Spanner
- 텍스트북
- SFINAE
- 동료학습
- uuid-ossp
- 부동소수점
- enable_if
- 레이캐스팅
- 쿠버네티스
- 파이썬
- mistel키보드
- 42seoul
- 창업
- 어셈블리
- 프라이빗클라우드
- 엣지컴퓨팅
- adminbro
- 스타트업
- Today
- Total
written by yechoi
[GraphQL] 파일 업로드 with NestJS, React 본문
GraphQL 서버를 이용해 파일을 업로드하는 방법을 알아본다.
클라이언트 사이드에서는 프레임워크로는 React, 상태관리 라이브러리로 Apollo Client를 사용하고 있다.
서버 사이드에서는 프레임워크로는 NestJS, 서버는 NestJS 내장 서버를 사용한다.
클라이언트 사이드
1. 파일 업로드 버튼 만들기
우선 파일 업로드 버튼을 만들어보자. 기본적으로 input 태그를 사용하면 브라우저별로 아래와 같은 버튼이 만들어진다.
버튼을 예쁘게 만들고 싶어서, label 태그의 for 속성을 활용해 카메라 버튼이 눌리면 input 태그가 눌리도록 변경했다.
<div className="upload-button-container">
<label htmlFor="input-file">
<span role="img" aria-label="camera">
📷
</span>
</label>
<input type="file" id="input-file" onChange={fileUpload} />
</div>
2. Apollo-client 업로드 링크 설정
파일을 올리기 위해서는 Apollo Link 설정을 해줘야 한다. Apollo Client 가 기본적으로 업로드 기능을 지원하는 게 아니다.
Apollo Link는 Apollo Client와 GraphQL 서버 사이의 데이터 흐름을 커스텀할 수 있도록 하는 라이브러리다.
const httpLink = createUploadLink({
uri: GRAPHQL_URL,
headers: {
authorization: bearerAuthorization(getCookies('access_token')),
'keep-alive': 'true',
},
});
const wsLink = new WebSocketLink({
// subscription 을 위해 만들어놓은 링크로 이번 글의 설명과는 무관함
});
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return definition.kind === 'OperationDefinition' && definition.operation === 'subscription';
},
wsLink,
httpLink,
);
export const createClient = new ApolloClient({
link: splitLink,
// 이하 생략
});
위 코드처럼 createUploadLink
로 업로드 링크를 생성하고, ApolloClient에 link 부분에 물려주면 된다.
링크가 여럿일 경우 위처럼 split
으로 쿼리에 따라 쪼개줄 수 있다.
createUploadLink
는 기존의 HttpLink
를 대체할 수 있다. 즉 기존에 생성해둔 HttpLink
가 있다면, 이를 createUploadLink
로 바꾸면 된다.
서버 사이드
이 부분에서 deprecated 된 부분이나 에러가 있는데도 유지보수 안되는 부분들이 있어서 애를 많이 먹었다.
깃헙 이슈에서 이 코멘트를 보고 해결했다.
핵심은
- apollo-server-core에 내장된 graphql-upload은 오래된 버전을 탑재
- 옛 버전의 graphql-upload는 최근의 Node.js 및 기타 패키지와 충돌함
- apollo-server는 graphql-upload를 지원하지 않을 것이라고 함
- 내장된 것을 쓰지 말고 직접 graphql-upload 패키지를 받아서 사용하라
일부 블로그들이 apollo-server-core에서 GraphqlUpload
를 import 하라고 하는데, 이는 최신 Node랑 충돌하므로 절대 ❌
1. resolution 없애기
이 충돌을 해결하려고 fs-capacitor 와 graphql-upload 를 package.json resolution에 넣으라는 얘기도 있는데,
만약 resolution에 fs-capacitor와 graphql-upload를 넣어뒀다면 지워주자.
2. 내장 upload 비활성화하고 graphqlUploadExpress middleware 사용하기
import { graphqlUploadExpress } from "graphql-upload"
import { MiddlewareConsumer, Module, NestModule } from "@nestjs/common"
@Module({
imports: [
GraphQLModule.forRoot({
uploads: false, // 내장 upload handling 비활성화
}),
],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(graphqlUploadExpress()).forRoutes("graphql")
}
}
3. resolver 설정하기
GraphQLUpload
는 무조건❗️ graphql-upload에서 import 한다.
받아온 file로 원하는 작업을 해주면 된다. 아래 코드는 ./images/${filename}
에 받아온 파일을 저장하고, 저장한 파일명을 반환하는 코드다.
import { GraphQLUpload, FileUpload } from 'graphql-upload';
@Mutation(() => String)
async uploadFile(
@Args('file', { type: () => GraphQLUpload })
file: FileUpload,
): Promise<string> {
const { createReadStream, filename } = await file;
const stream = createReadStream();
return new Promise(async (resolve, reject) =>
stream
.pipe(createWriteStream(`./images/${filename}`))
.on('finish', () => resolve(`/${filename}`))
.on('error', () => reject('')),
);
}
4. 클라이언트 사이드에서 resolver 사용하기
다시 클라이언트 사이드로 돌아가서, 이 mutation을 적용해보자.
export const UPLOAD_FILE = gql`
mutation uploadFile($file: Upload!) {
uploadFile(file: $file)
}
`;
export const FileUploadButton = () => {
const [uploadFile] = useMutation(UPLOAD_FILE);
const fileUpload: React.ChangeEventHandler<HTMLInputElement> = async (e) => {
const files = e.target.files;
if (files && files.length === 1) {
const file = files[0];
const saveLocation = await uploadFile({
variables: {
file: file,
},
}).catch(() => {
console.log('upload file error');
return;
});
}
};
return (
<>
<div className="upload-button-container">
<label htmlFor="input-file">
<span role="img" aria-label="camera">
📷
</span>
</label>
<input type="file" id="input-file" onChange={fileUpload} />
</div>
</>
);
};
끝!