S3๋ฅผ ํ๋ก ํธ์์ ๊ด๋ฆฌํ๋ ์ด์
nextjs๋ฅผ ํ์ฉํด์ S3๋ฒํท์๋ค๊ฐ ์ด๋ฏธ์ง๋ฅผ ํ๋ก ํธ์์ ์ง์ ๊ด๋ฆฌํ๊ฒ ๋์๋ค.
S3๋ฅผ ํ๋ก ํธ์์ ๊ด๋ฆฌํ๋ฉด ์ข๋ ๋น ๋ฅธ ์๋ต์ ๋ง๋ค ์ ์๋ค. ๊ทธ ์ด์ ๋ ๋ฌด์์ผ๊น ์๊ฐํด๋ณด์๋ฉด ๋ค์๊ณผ ๊ฐ๋ค.
- ํด๋ผ์ด์ธํธ์์ ๋ฐฑ์๋๋ก ์ด๋ฏธ์ง ๋ฐ์ดํฐ๋ฅผ ๋์ ธ์ฃผ๊ณ ์๋ฒ์์ ๊ทธ๊ฑธ ๋ฐ๊ณ aws์ ๋๊ฒจ์ฃผ๊ณ ํด๋ผ์ด์ธํธํํ ๋งํฌ๋ฅผ ๋๊ฒจ์ค์ผํ๋ค. ์๋ฒ๊ฐ ๋ฐ์๋ฉด ์ด๋ฏธ์ง๋ฅผ ๊ฐ๊ณตํด์ ์ฃผ๋ ์๊ฐ์ด ๊ธธ๋ค๋ ๋ป์ด๋ค. ์๋ฒ๊ฐ ๋ฐ์๋ ๋ ๋ฐ์ ํ๋ก ํธ์์ ๋์ aws์ ์ ๋ก๋๋ฅผ ํด์ฃผ๋ ๊ฒ์ด๋ค.
- ํด๋ผ์ด์ธํธ์์ Img์ ๋ก๋๋ฅผ S3๋ฒํท์ ํด์ฃผ๊ณ s3 url์ ์๋ฒํํ ๋ณด๋ด์ผํ๋๋ฐ ์ด๋ react-query๋ฅผ ์ฌ์ฉํด์ optimistic UI๋ฅผ ์ ์ฉํด์ฃผ๋ฉด ์ข ๋ ๋น ๋ฅด๊ฒ ์ ์ฉ๋ ์ด๋ฏธ์ง๋ฅผ ๋ณผ ์ ์๋ค.
๋จ์ ์ผ๋ก๋ ๋ธ๋ผ์ฐ์ ์์ ์ฐ๋ฆฌ์ ์ฝ๋๋ฅผ ํ์ธํ ์ ์์ด ๋ณด์์ด ์ข์ง ์์ ์ ์๋๋ฐ ํ์ฌ ๋ด ์ํฉ์ nexjs api router๋ฅผ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ๋ธ๋ผ์ฐ์ ์์ ์ฝ๋๋ฅผ ํ์ธํ์ง ๋ชปํด ์ด๋ฐ ๋จ์ ์ ํด๊ฒฐ๋๊ฑฐ๋ผ๊ณ ์๊ฐ์ด ๋๋ค.
S3๋ฅผ ์ค์ ํ ๋ ์์์ผ ํ ๊ฒ๋ค
๊ธฐ๋ณธ์ ์ผ๋ก s3๋ฅผ ์ค์ ํ๋ ๋ฐฉ๋ฒ์ ์ธํฐ๋ท์ ๋ค ๋์์์ํ ๋ ์ด๋ฒ์ s3๋ฅผ ์ค์ ํ๋ฉด์ ์ค์ํ๊ฒ ์๊ฐํ๋ ๊ฒ๋ค์ ์ ๋ฆฌํด ๋ณด๋ ค๊ณ ํ๋ค.
๊ถํ ์ค์ ์ข ๋ฅ
- ์ฌ์ฉ์๋ฅผ ์์ฑํ๊ณ ์ฌ์ฉ์์ ๋ฒํท ๊ถํ ์ก์ธ์ค๋ฅผ ๊ด๋ฆฌํ๋ IAM
- ๊ถํ ์๋ ์ฌ์ฉ์์ ๋ํด ๊ฐ๋จํ ๊ฐ๋ณ ๊ฐ์ฒด(์ค๋ธ์ ํธ)๋ฅผ ์ก์ธ์ค ๊ฐ๋ฅํ๊ฒ ๋ง๋๋ ์ก์ธ์ค ์ ์ด ๋ชฉ๋ก(ACL)
- ๋จ์ผ S3 ๋ฒํท ๋ด ๋ชจ๋ ๊ฐ์ฒด์ ๋ํ ๊ถํ์ ์ธ๋ถ์ ์ผ๋ก ๊ตฌ์ฑํ๋ ๋ฒํท ์ ์ฑ
(bocket policy)
- ์์ URL์ ์ฌ์ฉํ์ฌ ๋ค๋ฅธ ์ฌ์ฉ์์๊ฒ ๊ธฐ๊ฐ ์ ํ(์์ ๊ถํ) ์ก์ธ์ค๋ฅผ ๋ถ์ฌํ๋ ์ฟผ๋ฆฌ ๋ฌธ์์ด ์ธ์ฆ(pre-signed URL)
ACL
ACL์ ๋ฒํท์ด๋ ๊ฐ์ฒด์ ๋ํด ์์ฒญ์์ ๊ถํ ํ์ฉ ๋ฒ์๋ฅผ ์ด๋๊น์ง ์ค์ ํ ๊ฒ์ธ๊ฐ์ ๋ํด ๊ฐ๋จํ๊ฒ ์ค์ ํ ์ ์๋ค.
์์ฒญ์๋ ์ผ๋ฐ ํผ๋ธ๋ฆญํ ์ฌ์ฉ์, ๊ณ์ ์ฃผ์ธ, ๋ฆฌ์์ค ๊ทธ๋ฃน, ํน์ ์ฌ์ฉ์๊ฐ ๋ ์ ์๋ค.
๊ฐ๊ฐ์ ๋ฒํท๊ณผ ๊ทธ ์์ ํฌํจ๋ ๊ฐ์ฒด๋ ACL๋ก ์ฐ๋๋๋ฏ๋ก ACL๋ก ๊ฐ์ฒด ์ ๊ทผ์ ์ ์ดํ๋๊ฒ ๊ฐ๋ฅํ๋ค
๋ฒํท์ ์ฑ
Bucket Policy๋ ๋ฒํท์ ์ฌ์ฉํ ๊ถํ์ ๊ฐ์ง ์ฌ๋ฌ ๋ช ์ ์ฌ์ฉ์ ๋ณ๋ก ๊ฐ๊ฐ์ ํ์์ ๋ํ ๊ถํ ๋ฒ์๋ฅผ ์ค์ ํ ์ ์๋ค. ๋๊ตฐ๊ฐ๋ ์ฝ๊ธฐ๋ง ๊ฐ๋ฅํ๊ณ ๋๊ตฐ๊ฐ๋ ์ฝ๊ธฐ, ์ฐ๊ธฐ ๋ชจ๋ ๊ฐ๋ฅํ ์ํ๋ก ์ค์ ํ ์ ์๋ค.
> **acl๊ณผ ๋ฒํท์ ์ฑ ์ฐจ์ด**
ACL์ด๋ ๋ฒํท ์ ์ฑ ์ด๋ ๋๋ค ๋ฒํท์ ๋ํ ์์ธ์ค๋ฅผ ์ ํํ๊ฑฐ๋ ํ์ฉํ๋ ๊ถํ ์ค์ ์ด๋ค. ๋ฒํท์ ์ฑ ์ ๋ฒํท์๋ํด์๋ง ๊ถํ์ ์ค์ ํ ์ ์์ง๋ง, ACL์ ๋ฒํท ๋ฟ๋ง ์๋๋ผ ๊ฐ๋ณ ๊ฐ์ฒด์๋ ๊ฐ๋ฅํ๋ค. ๋ฒํท์ ์ฑ ์ JSON์ ํตํด ์ธ๋ถํ๋ ๊ถํ์ ์ค์ ํ ์ ์์ง๋ง, ACL์ ๋ฒํท ์ ์ฑ ๋งํผ ์ธ๋ถํ๋ ์์ธ์ค ๋ชจ๋๋ฅผ ์ ๊ณตํ์ง ์๋๋ค.
์ก์ธ์ค ์ฐจ๋จ
- ์ ACL(์ก์ธ์ค ์ ์ด ๋ชฉ๋ก)์ ํตํด ๋ถ์ฌ๋ ๋ฒํท ๋ฐ ๊ฐ์ฒด์ ๋ํ ํผ๋ธ๋ฆญ ์ก์ธ์ค ์ฐจ๋จ→ ์ง์ ๋ ACL์ด ํผ๋ธ๋ฆญ์ด๊ฑฐ๋, ์์ฒญ์ ํผ๋ธ๋ฆญ ACL์ด ํฌํจ๋์ด ์์ผ๋ฉด PUT ์์ฒญ์ ๊ฑฐ์ ํ๋ค.
- ์์์ ACL(์ก์ธ์ค ์ ์ด ๋ชฉ๋ก)์ ํตํด ๋ถ์ฌ๋ ๋ฒํท ๋ฐ ๊ฐ์ฒด์ ๋ํ ํผ๋ธ๋ฆญ ์ก์ธ์ค ์ฐจ๋จ→ ๋ฒํท์ ๋ชจ๋ ํผ๋ธ๋ฆญ ACL๊ณผ ๊ทธ ์์ ํฌํจ๋ ๋ชจ๋ Object๋ฅผ ๋ฌด์ํ๊ณ , ํผ๋ธ๋ฆญ ACL๋ฅผ ํฌํจํ๋ PUT ์์ฒญ์ ํ์ฉํ๋ค.
- ์ ํผ๋ธ๋ฆญ ๋ฒํท ๋๋ ์ก์ธ์ค ์ง์ ์ ์ฑ ์ ํตํด ๋ถ์ฌ๋ ๋ฒํท ๋ฐ ๊ฐ์ฒด์ ๋ํ ํผ๋ธ๋ฆญ ์ก์ธ์ค ์ฐจ๋จ→ ์ง์ ๋ ๋ฒํท ์ ์ฑ ์ด ํผ๋ธ๋ฆญ์ด๋ฉด PUT ์์ฒญ์ ๊ฑฐ์ ํ๋ค. ์ด ์ค์ ์ ์ฒดํฌํ๋ฉด ๋ฒํท ๋ฐ ๊ฐ์ฒด์ ๋ํ ํผ๋ธ๋ฆญ ์ก์ธ์ค๋ฅผ ์ฐจ๋จํ๊ณ ์ฌ์ฉ์๊ฐ ๋ฒํท ์ ์ฑ ์ ๊ด๋ฆฌํ ์ ์์ผ๋ฉฐ, ์ด ์ค์ ํ์ฑํ๋ ๊ธฐ์กด ๋ฒํท ์ ์ฑ ์ ์ํฅ์ ์ฃผ์ง ์๋๋ค.
- ์์์ ํผ๋ธ๋ฆญ ๋ฒํท ๋๋ ์ก์ธ์ค ์ง์ ์ ์ฑ ์ ํตํด ๋ถ์ฌ๋ ๋ฒํท ๋ฐ ๊ฐ์ฒด์ ๋ํ ํผ๋ธ๋ฆญ ์ก์ธ์ค ์ฐจ๋จ→ ํผ๋ธ๋ฆญ ์ ์ฑ ์ด ์๋ ๋ฒํท์ ๋ํ ์ก์ธ์ค๊ฐ ๊ถํ์ด ์๋ ์ฌ์ฉ์์ AWS ์๋น์ค๋ก๋ง ์ ํ๋๋ฉฐ, ์ด ์ค์ ํ์ฑํ๋ ๊ธฐ์กด ๋ฒํท ์ ์ฑ ์ ์ํฅ์ ์ฃผ์ง ์๋๋ค.
api router ์ค์ ์ฝ๋
const s3Client = new S3Client({
region: process.env.S3_IMAGE_UPLOAD_REGION as string,
credentials: {
accessKeyId: process.env.S3_IMAGE_UPLOAD_ACCESS_KEY as string,
secretAccessKey: process.env.S3_IMAGE_UPLOAD_SECRET_ACCESS_KEY as string,
},
})
export async function POST(request: Request) {
try {
const formData = await request.formData()
const file = formData.get('file') as File | null
if (!file) {
return NextResponse.json({ error: 'ํ์ผ์ด ํ์ํฉ๋๋ค' }, { status: 400 })
}
const mimeType = file.type
const buffer = Buffer.from(await file.arrayBuffer())
const s3ImgUrl = await uploadFileToS3(buffer, file.name, mimeType)
return NextResponse.json({ s3ImgUrl }, { status: 200 })
} catch (err) {
return NextResponse.json({ error: 'Error uploading file' })
}
}
์ ์ผ ๋จผ์ IAM์ ์ ๋ฅผ ์์ฑํ๋ฉด์ ๋ฐ๊ธ๋ฐ์ ํค๋ค์ s3client ์ธ์คํด์ค์ ๋ฑ๋ก์ ํด์ค๋ค.
POST๋ฉ์๋๋ฅผ ํ์ฉํด ์ด๋ฏธ์ง ์ ๋ก๋๋ฅผ ํด์ฃผ๊ณ post๋ฉ์๋๋ฅผ ํตํด ๋ค์ด์จ ์ด๋ฏธ์ง ํ์ผ์ ์ถ์ถํด์ค ๋ค์ Buffer.from() ๋ฉ์๋๋ฅผ ํ์ฉํด ์ด๋ฏธ์ง ํ์ผ์ Buffer ๋ฐ๊ฟ์ค๋ค. ๊ทธ๋ฐ ๋ค์ uploadFileToS3๋ผ๋ ํจ์๋ฅผ ํ๋ ์์ฑํด์ฃผ์๋๋ฐ ๋ค์๊ณผ ๊ฐ๋ค.
async function uploadFileToS3(file: Buffer, fileName: string, type: string) {
const fileBuffer = file
const params = {
Bucket: process.env.S3\_IMAGE\_UPLOAD\_BUCKET\_NAME,
Key: \`${fileName}\`,
Body: fileBuffer,
ContentType: type,
}
const command = new PutObjectCommand(params)
await s3Client.send(command)
const s3ImgUrl = \`https://${process.env.S3\_IMAGE\_UPLOAD\_BUCKET\_NAME}.s3.${process.env.S3\_IMAGE\_UPLOAD\_REGION}.amazonaws.com/${fileName}\`
return s3ImgUrl
}
PutObjectCommand ์ธ์คํด์ค๋ฅผ ํ์ฉํด์ s3์ ๋ณด๋ด์ค ์ปค๋งจ๋ ๊ฐ์ฒด๋ฅผ ์์ฑํด์ฃผ๊ณ s3Client.send๋ฅผ ์ฌ์ฉํด ํ์ผ ๊ฐ์ฒด๋ฅผ ์
๋ก๋ ํด์ค๋ค. ๋ณดํต s3 ์ด๋ฏธ์ง url ํ์์ ๋ค์๊ณผ ๊ฐ๋ค
> ๋ฒํท์ด๋ฆ.s3.๋ฒํท๋ฆฌ์ .amazonaws.com/ํ์ผ ์ด๋ฆ
๊ทธ๋์ s3ImgUrl์ ๋ฐํํด์ Post ๋ฉ์๋์ ๋ฐํ๊ฐ์ผ๋ก ์ ๋ฌํด์ค๋ค.
Optimistic UI ์ ์ฉ
const { mutate } = useMutation({
mutationFn: postImgUrl,
onMutate: async (postImgArg) => {
await queryClient.cancelQueries({ queryKey: \['profile', email\] })
const previousData = queryClient.getQueryData(\[
'profile',
email,
\])
queryClient.setQueryData<UserProfile>(\['profile', email\], (old) => {
if (old) {
const imgUrl =
type === 'profile'
? { profileImg: postImgArg.fileImgSrc }
: { backgroundImg: postImgArg.fileImgSrc }
return { ...old, ...imgUrl }
}
return old
})
return { previousData }
},
onError: (err, postImgArg, context) => {
queryClient.setQueryData(\['profile', email\], context?.previousData)
},
onSuccess: (err, postImgArg, context) => {
fetch('/api/s3-upload', {
body: JSON.stringify({
deleteS3ImgUrl: \`${type === 'profile' ? context.previousData?.profileImg : context.previousData?.backgroundImg}\`,
}),
method: 'DELETE',
})
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: \['profile', email\] })
},
})
mutateํจ์์๋ ์ด๋ฏธ์ง url์ ์ ๋ฌํด์ฃผ๋ฉด ๋๋๋ฐ ์ด๋ ์ด๋ฏธ์ง url์ ์ฐ๋ฆฌ๊ฐ ์ด์ ์ ๋ณธ post api router ํตํด ์ป์ ์ ์๋ค.
๋๊ด์ ์ ๋ฐ์ดํธ๊ฐ ์งํ๋๋ฉด refetchOnMount ์์ฑ์ ํ์ฉํด ๋ฐ์ดํฐ๋ฅผ refetchํ๋๋ฐ ์ด๋ ๋๊ด์ ์ ๋ฐ์ดํธ ๋ฐ์ดํฐ๋ฅผ ์๋ ๋ฐ์ดํฐ๊ฐ ๋ฎ์ด ์์ธ ์ ์๋๋ฐ ์ด๊ฒ์ cancelQuries๋ฅผ ํ์ฉํ์ฌ ๋ฐฉ์งํด์ค๋ค.
setQueryData๋ฅผ ํ์ฉํด ์ง์ ์บ์ ๋ฐ์ดํฐ๋ฅผ ์กฐ์ ํด์ฃผ๋ฉด ๋๋ค.
previousdata๋ฅผ onmutate์์ ๋ฐํํด์ฃผ๋ ์ด์ ๋ ๋ง์ฝ error๊ฐ ์์ ๋ rollback์ ํด์ฃผ๊ธฐ ์ํจ์ด๋ค.
onError์์๋ ๋กค๋ฐฑํด์ฃผ๊ธฐ ์ํ ๋ก์ง์ ์์ฑํ ๊ฒ์ด๊ณ
onSuccess์ผ๋๋ ์ต๊ทผ ๋ฑ๋ก๋ ์ด๋ฏธ์ง๊ฐ ์๋๋ผ ์ด์ ์ ์ฌ์ฉํ๋ ์ด๋ฏธ์ง๋ฅผ ์ง์ฐ๊ธฐ ์ํด์ onMutate๋ ๋ฐํํ Previou ๋ฐ์ดํฐ๋ฅผ context์ธ์๋ฅผ ํ์ฉํด์ ์ด์ฉํ ์ ์๋ค. ๊ทธ๋์ ์ด์ ์ด์ง๋ฏธ url์ ์ง์์ฃผ๊ธฐ ์ํด DELETE ๋ฉ์๋๋ฅผ ์ฌ์ฉํด์ api router๋ฅผ ํธ์ถํ๋ฉด ๋๋ค.
Delete api router
export async function DELETE(request: Request) {
const data = (await request.json()) as { deleteS3ImgUrl: string }
const pattern = /(?<=amazonaws\\.com\\/)\[^/\]+$/
const match = data.deleteS3ImgUrl.match(pattern)
if (match) {
const fileName = match\[0\]
const bucketParams = {
Bucket: process.env.S3\_IMAGE\_UPLOAD\_BUCKET\_NAME,
Key: \`${fileName}\`,
}
const command = new DeleteObjectCommand(bucketParams)
s3Client.send(command)
}
return NextResponse.json({ message: 'good delete' }, { status: 200 })
}
์ด์ post api router์ ๋ค๋ฅธ์ ์ DeleteObjectCommand ์ธ์คํด์ค๋ฅผ ํ์ฉํ์ฌ ์ปค๋งจ๋ ๊ฐ์ฒด๋ฅผ ๋ง๋ ๋ค ์ ๋ฌํ๋ฉด ๋๋ค.
'๊ฐ๋ฐ > FRONTEND' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
Easyfetch ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๊ตฌํ๊ธฐ (0) | 2024.08.23 |
---|---|
Render Props๋ฅผ ํตํ UX ๊ฐ์ (1) | 2024.08.13 |
์๋น์ค๋ฅผ ์ํ ๋ก์ง ์์ฑํ๊ธฐ (0) | 2024.08.06 |
Next, MSW ๊ทธ๋ฆฌ๊ณ ESlint (0) | 2024.08.06 |
Next.js์์ ์์ ํ ๋ก๊ทธ์ธ ๊ตฌํ ์ฌ์ (without NextAuth) (0) | 2024.07.10 |