오늘 내가 배운 것
1. 문제 발생
2. 원인 및 목적
1. 문제 발생
HTTP 413 Error 발생
2. 원인 및 목적
Next.js를 이용하고 있는 프로젝트는 클라이언트 측에서 빠르게 업로드를 하기 위해서 S3에 업로드를 하고 URL을 반환받아서 DB에 저장하는 방식을 선택했다.
하지만 요청할 때 보내는 데이터의 크기가 크기 때문에 413 Error가 발생하였다. 요청 크기에 제한이 있어서 발생할 수 있는 문제이다.
내가 시도한 방법과 해결한 방법을 적어놨는데, 이 방법이 효율적인지는 알 수 없지만 방법 중에 하나이기 때문에 언젠가 쓸 수 있는 방법이라고 생각한다.
2-1. NextJS pages/api 라우터 이용 ( multer, config ) ( 실패 )
클라이언트는 NextJS를 이용하기 때문에 pages/api 라우터를 이용해서 S3에 업로드하려고 했다.
- 업로드할 동영상 파일을 선택한 뒤, formData 형식으로 변환한다.
- 변환된 formData를 multer를 이용해 api 라우터를 통해 S3에 업로드한다.
위와 같은 과정으로 클라이언트단에서 업로드를 처리하려 했으나 API에서 Request를 받는 과정에서 413 Error가 발생했다.
413 에러는 Payload Too Large로 요청 데이터의 크기가 너무 커서 발생하는 에러이다.
제일 먼저 검색해서 나온 해결 방법은 'experimental' 속성을 이용하여 크기 제한을 늘리는 것이었다.
// next.config.js
module.exports = {
experimental: {
serverActions: true,
serverActionsBodySizeLimit: "500mb",
},
}
// pages/api - video.js
// pages/api 라우터 부분은 대략적으로 흐름만 확인..
export const config = {
api: {
bodyParser: false,
responseLimit: false,
},
}
export const upload = multer({
storage: multerS3({
s3: s3 as S3,
bucket: 'mybucket', // 버킷 이름
key(req, file, callback) {
const ext = path.extname(file.originalname)
const basename = path.basename(file.originalname, ext)
callback(null, `interview/${basename}_${Date.now()}${ext}`)
},
}),
limits: {
fileSize: 500 * 1024 * 1024, // 500MB
},
}).single('file')
export default (req: MulterRequest, res: NextApiResponse) => {
if (req.method === 'POST') {
upload(req as any, res as any, (err) => {
if (err) {
return res.status(400).send(err)
}
if (!req.file) {
return res.status(400).send('No file uploaded')
}
return res.status(200).json({ url: req.file.location })
})
} else {
res.status(405).end()
}
}
하지만 실. 패.
위에 내용을 보면 bodyParser을 비활성화하는 로직이나 multer에서 걸릴만한 fileSize를 늘려주는 방법을 했지만 제대로 동작하지 않았다.
여기서 검색해서 얻은 결과는 pages/api 방식은 요청을 보내는데 제한이 있다는 점이었고, 이 부분에 대해서 제한을 해제하는 방법이 쉽지 않거나 없다는 점이다. (나는 못 찾았다.. 시도한 여러 방법이 다 실패.. )
그래서 라우터에서 업로드를 하는 것이 아닌 바로 클라이언트에서 S3로 직접 업로드하는 방식을 택했다.
서버의 요청 크기제한 문제를 피할 수 있고, 서버의 부하를 줄일 수 있다고 생각했다.
2-2. Client 측에서 미리 서명된 S3 URL을 이용하여 업로드하기
API 라우터를 이용해서 S3에 업로드를 할 위치 및 서명된 url을 생성한다. (이때, AWS의 키나 정보들이 서버 사이드로 동작하기 때문에 보안 측면에서 조금 이점이 있다고 생각한다.)
서명된 URL은 S3에 업로드할 파일의 위치와 파일명을 포함하고 있으며, 이 URL을 이용하여 클라이언트에서 업로드를 진행한다.
AWS S3와 같은 스토리지 서비스에서 자주 사용되는 방법인데, 서명된 URL을 이용하여 업로드를 진행하면 서버에서 업로드를 하지 않기 때문에 서버의 부하를 줄일 수 있고, 클라이언트에서 직접 업로드를 하기 때문에 빠르게 업로드를 할 수 있다.
또한 만료 시간을 설정할 수 있기 때문에 보안 측면에서도 안전하다. (만료 시간이 지나면 해당 URL은 더 이상 사용할 수 없다.)
생성된 URL을 이용하여 데이터를 업로드하기 위해서 'put'요청을 하고, 다운로드(view)를 위해서 'get'요청을 한다.
밑에 코드는 upload를 위한 코드이기 때문에, 해당 url로 put 요청을 하는 코드이다.
// pages/api - video.ts
import AWS from "aws-sdk"
import { NextApiRequest, NextApiResponse } from "next/types"
AWS.config.update({
region: "my-region",
accessKeyId: "aws-key",
secretAccessKey: "aws-secretsKey",
})
const s3 = new AWS.S3()
export default (req: NextApiRequest, res: NextApiResponse) => {
const newFileName = `${Date.now()}_${req.body.fileName}`
const params = {
Bucket: "my bucket",
Key: `interview/${newFileName}`, // 버킷안에 interview 디렉토리의 newFileName이름으로 저장
Expires: 60, // 서명된 url은 1분간 유효하다.
}
s3.getSignedUrl("putObject", params, (err, url) => {
if (err) {
res.status(500).json({ error: "Internal Server Error" })
} else {
// url은 서명된 url이며, fileUrl은 DB에 저장 될 S3 버킷의 pathName이다.
res.status(200).json({ url, fileUrl: `/interview/${newFileName}` })
}
})
}
// Client Component - submitHandler에서 아래의 방법으로 업로드하고, 반환받은 fileUrl을 DB에 저장한다.
try {
setLoading(true)
const config = {
headers: { "Content-Type": "video/*" },
api: { bodyParser: false },
}
const {
data: { url: signedURL, fileUrl }, // api 라우터에서 signedURL과 path로 저장할 fileUrl을 반환 받는다.
} = await axios.post("/api/upload/video-url", { fileName: file.name }) // 업로드 했을 때 file의 상태로 데이터를 가지고 있고, file.name을 이용해서 url을 만들 기 위해서 api라우터로 보내준다.
const data = await axios.put(signedURL, file, config) // 서명된 url에 put 요청으로 file을 보낸다.
const submitData = await updataCourseApplyStep2({ id, interview_url: fileUrl })
setLoading(false)
if (submitData.updated) setIsStep(step + 1)
} catch (e) {
console.log(e)
}
드디어 성. 공.
클린 코드, 효율적인 코드인지는 모르겠지만 해당 방법을 통해서 body의 내용이 큰 동영상 파일을 업로드할 수 있었다.
backend를 이용한 방법이나 AWS Ramda를 이용하는 방법도 생각해 봤지만 클라이언트단에서 하는 방법을 생각했고, AWS Ramda를 익히고 적용하기엔 조금 오래 걸릴 것 같아서 위의 방법을 이용했다.
'시작 > TIL(Today I Learned)' 카테고리의 다른 글
231110 - 카카오 로그인 (feat. KOE010) (0) | 2023.11.10 |
---|---|
231109 - Monorepo를 이용한 Next.js 프로젝트 구성하기 (feat. pnpm) (1) | 2023.11.09 |
230925 - React 컴포넌트의 유연성과 최적화 ( 조건부 렌더링, 다이나믹 컴포넌트, 고차컴포넌트(HOC)) (0) | 2023.09.25 |
230920 - Next param (0) | 2023.09.21 |
230919 - Next Component, Cache (0) | 2023.09.19 |
댓글