ํ์ผ๊ณผ JSON ๋ฐ์ดํฐ๋ฅผ ๋์์ ๋ณด๋ด๊ธฐ! 1ํธ์์๋ ํ์ผ๊ณผ JSON ๋ฐ์ดํฐ๋ฅผ ๋์์ ๋ณด๋ด๊ธฐ ์ํด ์ ํฉํ Content-Type
์ด ๋ฌด์์ผ์ง ๊ณ ๋ฏผํด๋ณด์๋ค.
1ํธ์์ ๋ด๋ฆฐ ๊ฒฐ๋ก ์ Multipart/form-data
๋ฅผ ํตํด ๋ ๊ฐ์ง ๋ฐ์ดํฐ๋ฅผ ๋์์ ๋ณด๋ด๋ ๊ฒ์ด๋ค. ํ์ผ์ ์ด๋ฏธ ํํ Multipart/form-data
๋ก ์ฃผ๊ณ ๋ฐ๊ณ ์๋ค. ๊ด๊ฑด์ JSON ๋ฐ์ดํฐ๋ฅผ Multipart/form-data๋ก ๋ณด๋ด๋ ๊ฒ์ด๋ค.
์ด๋ฒ์๋
Express
๋ก ์๋ฒ๋ฅผ,React
๋ก ์น ํด๋ผ์ด์ธํธ๋ฅผ ๊ตฌ์ถํ์ฌ ์ค์ ๋์์ ํ์ธํด๋ณด๊ฒ ๋ค.
๐ ํ์ผ ์ ๋ก๋์ ๋ํด ์ ์์๋ ๋ถ์ ๋ฐ๋ก (๐ ํ์ผ๊ณผ JSON ๋์์ ์ฃผ๊ณ ๋ฐ๊ธฐ)๋ก ์ด๋ํด์ฃผ์ธ์!
์ฐ์ ์ ํ์ผ๋ง Multipart/form-data
๋ก ์ฃผ๊ณ ๋ฐ๋ ๊ธฐ๋ฅ์ ๊ตฌํํด๋ณธ๋ค.
ํ๋ก์ ํธ๋ฅผ ์์ฑํ๊ณ ํ์ผ ์ ๋ก๋์ ํ์ํ ๋ชจ๋์ ์ถ๊ฐํ๋ค.
npm init -y
npm i express multer cors -S
์๋ฒ ๋ฉ์ธ ์ฝ๋๋ ๋ค์๊ณผ ๊ฐ๋ค.
// index.js
const express = require('express');
const fs = require('fs');
const cors = require('cors');
const fileRouter = require('./routes/file');
const app = express();
app.use(cors());
app.use('/file', fileRouter);
app.listen(4000, () => {
// ํ์ผ์ด ์ ์ฅ๋ upload ๋๋ ํ ๋ฆฌ๊ฐ ์์ผ๋ฉด ์์ฑ
const dir = './upload';
if (!fs.existsSync(dir)) fs.mkdirSync(dir);
console.log('server is running!');
});
ํ์ผ ์ ๋ก๋ API๊ฐ ์ ์๋์ด ์๋ ํ์ผ ๋ผ์ฐํฐ ์ฝ๋์ด๋ค.
// routes/file.js
const express = require('express');
const multer = require('multer');
const storage = multer.diskStorage({
// ์
๋ก๋ ๋์ ๋๋ ํ ๋ฆฌ ์ง์
destination(req, file, cb) {
cb(null, 'upload')
},
// ํด๋์ ์ ์ฅ๋ ํ์ผ๋ช
filename(req, file, cb) {
cb(null, Date.now() + '-' + file.originalname)
}
});
const upload = multer({ storage: storage });
const router = express.Router();
// upload.singe(fieldname), fieldname์ ํผ์ ์ ์๋ ํ๋๋ช
router.post('/upload', upload.single('file'), (req, res) => {
res.status(201).json({message: 'file upload success!'});
})
module.exports = router;
Postman
์ผ๋ก API ํ
์คํธํด๋ณด์.
ํ ๊ฐ์ง ์ฃผ์ํ ์ ์ Header์
ContentType: multipart/form-data
๋ฅผ ์ง์ ํด์ ๋ณด๋ด๋ฉด ์๋๊ณ , ํค๋ ์์ฒด๋ฅผ ๋ณด๋ด์ง ์์์ผ ์ ์์ ์ผ๋ก ๋์ํ๋ค.
fieldname (key)
๋ฅผ file๋ก ์ค์ ํ๊ณ ์ ๋นํ ํ์ผ ํ๋๋ฅผ ์ ํํ์ฌ ์์ฒญ์ ๋ณด๋๋ค.
์์ฒญ์ ์ฑ๊ณตํ๊ณ , upload ํด๋์๋ ์ ๋๋ก ํ์ผ์ด ์
๋ก๋ ๋์์์ ํ์ธํ ์ ์๋ค.
์ด์ ์๋ฒ๋ ๊ตฌํ๋์์ผ๋ ์น ํด๋ผ์ด์ธํธ๋ฅผ ๊ตฌํํด๋ณด์.
์น๋ ๋น ๋ฅด๊ฒ CRA
๋ฅผ ์ด์ฉํด์ ๊ตฌ์ฑํด๋ณด๊ฒ ๋ค.
yarn create react-app frontend --template typescript
npm i -S axios
ํด๋ผ์ด์ธํธ ์ฝ๋๋ ๋ค์๊ณผ ๊ฐ์ด ์์ฑํ๋ค.
// App.tsx
import React, {useCallback, useState} from 'react';
import axios from 'axios';
function App() {
const [file, setFile] = useState<File | null>(null);
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files === null) return;
if (e.target.files[0]) {
setFile(e.target.files[0]);
}
}, []);
const handleClick = useCallback(async () => {
if (!file) return;
const formData = new FormData();
await formData.append('file', file);
const res = await axios.post(
'http://localhost:4000/file/upload',
formData,
{
headers: {
'Content-Type': 'multipart/form-data'
}
}
);
if (res.status === 201) console.log(res.data);
}, [file]);
return (
<div>
<input type={"file"} onChange={handleChange}/>
<button onClick={handleClick}>์
๋ก๋ ์์ฒญ</button>
</div>
);
}
export default App;
yarn start
๋ก ์น ํด๋ผ์ด์ธํธ๋ฅผ ์คํํ๋ค.
input์ ํ์ผ์ ์
๋ก๋ํ๊ณ ์
๋ก๋ ์์ฒญ ๋ฒํผ์ ๋๋ฌ์ upload API ์์ฒญ์ ๋ณด๋ธ๋ค. ๋ค์๊ณผ ๊ฐ์ด API๊ฐ ์ฑ๊ณต์ ์ผ๋ก ์ฒ๋ฆฌ๋๊ณ ,
Payload ํญ์์ multipart/form-data
ํ์์ผ๋ก ๋ฐ์ดํฐ๊ฐ ์ ๋ฌ๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
์ญ์ ์๋ฒ์ upload ํด๋์ ์๋ก์ด ์ด๋ฏธ์ง๊ฐ ๋ฑ๋ก๋ ๊ฒ๋ ํ์ธํ ์ ์์๋ค.
์ด๋ ๊ฒ ๊ธฐ๋ณธ์ ์ธ ํ์ผ์ ์ฃผ๊ณ ๋ฐ๋ ๊ธฐ๋ฅ ๊ตฌํ์ด ์๋ฃ๋๋ค!
์ด์ ๋๋์ด ๋ณธ ์ฃผ์ ์ธ ํ์ผ๊ณผ JSON์ ๋์์ ์ฃผ๊ณ ๋ฐ๋ ๊ธฐ๋ฅ์ ๊ตฌํํด๋ณด๊ฒ ๋ค.
์๋ฒ๋ ์๊น ์์ฑํ file router์ ์ผ๋ถ๋ถ๋ง ์์ ํ๋ฉด ๋๋ค. express๋ ์์ฒญ Payload์์ binary ๋ฐ์ดํฐ์ผ ๊ฒฝ์ฐ req.files
์, binary ์ด์ธ์ ๋ฐ์ดํฐ๋ req.body
๋ก ๋๋ ์ ์ ์ฅ๋๋ค.
์ด๋ฐ ๊ตฌ์กฐ ๋๋ถ์ ํด๋ผ์ด์ธํธ๋ก๋ถํฐ JSON.stringify()
๋ก json์ ๋ฌธ์์ด๋ก ๋ณํํด์ ๋ฐ์ ๋ค์ JSON.parse()
๋ฅผ ํตํด JSON ๋ฐ์ดํฐ๋ฅผ ์ป์ ์ ์๋ค.
// routes/file.js
(...)
// upload.singe(fieldname), fieldname์ ํผ์ ์ ์๋ ํ๋๋ช
router.post('/upload', upload.single('file'), (req, res) => {
console.log('uploader is ', JSON.parse(req.body.uploader).name);
res.status(201).json({message: 'file upload success!'});
})
module.exports = router;
ํด๋ผ์ด์ธํธ์์ JSON ๋ฐ์ดํฐ๋ฅผ ๋ณด๋ผ ๋๋ ํ์ผ๊ณผ ๋ง์ฐฌ๊ฐ์ง๋ก formData
์ append ํ๋ ๋ฐฉ์์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ๋ฃ์ด์ฃผ์ด์ผ ํ๋ค.
FormData.append()
์ ํ๋ผ๋ฏธํฐ ํ์
์ ๋ณด๋ฉด, value์๋ string๊ณผ Blob(binary large object) ํ์
๋ง ์ ๋ฌํ ์ ์๋ค.
๋ฐ๋ผ์ ์๋์์ uploader ๊ฐ์ฒด๋ฅผ append ํ ๋ ๋ฐ๋ก ๋ฃ์ง ์๊ณ JSON.stringify()
๋ก ๋ฌธ์์ด๋ก ๋ณํํ์ฌ ์ ๋ฌํ๋ค.
// App.tsx
import React, {useCallback, useState} from 'react';
import axios from 'axios';
interface Uploader {
name: string;
}
function App() {
(...)
const handleClick = useCallback(async () => {
if (!file) return;
const formData = new FormData();
await formData.append('file', file);
const uploader: Uploader = {name: 'huewilliams'};
await formData.append('uploader', JSON.stringify(uploader));
const res = await axios.post(
'http://localhost:4000/file/upload',
formData,
{
headers: {
'Content-Type': 'multipart/form-data'
}
}
);
if (res.status === 201) console.log(res.data);
}, [file]);
(...)
}
export default App;
์ด๋๋ก ์์ ํ๋ ๊ฒ ์ฒ๋ผ ํ์ผ์ ์
๋ก๋ํ์ฌ ์์ฒญ์ ๋ณด๋ด๋ฉด ๋ค์๊ณผ ๊ฐ์ด ์๋ฒ์์ JSON ๋ฐ์ดํฐ๋ฅผ ํจ๊ป ์ ๋ฌ๋ฐ์ ๊ฒ์ ํ์ธํ ์ ์๋ค!!
๋ด๊ฐ ์์ฑํ ์์ ๋ ๋น ๋ฅด๊ฒ ๊ตฌ์ฑํ๊ธฐ ์ํด express.js๋ก ๊ตฌํํ๋๋ฐ, Spring์ผ๋ก ๊ตฌํํ ๊ฒฝ์ฐ์๋ file๊ณผ dto๋ฅผ ํจ๊ป ์ ๋ฌ ๋ฐ์ผ๋ ค๋ฉด @RequestPart
๋ผ๋ ์ด๋
ธํ
์ด์
์ ์ฌ์ฉํ๋ฉด ๋๋ค. (๊ด๋ จ๋ ๋ ํผ๋ฐ์ค: https://emoney96.tistory.com/258)
@PostMapping("")
public SuccessReponse uploadFileWithRq(
@RequestPart UploaderRq rq,
@RequestPart MultipartFile multipartFile
) {
return fileService.uploadFile(rq, multipartFile);
}
์๋ฒ๊ฐ Spring Framework
๊ธฐ๋ฐ์ผ ๊ฒฝ์ฐ์๋ Express
์๋ ๋ค๋ฅด๊ฒ ๋์ํ๋ ์ฃผ์ํด์ผ ํ๋ค. ๊ธฐ์กด์ ํ๋ ๊ฒ ์ฒ๋ผ JSON.stringify()
๋ง ์ ์ฉํ์ฌ ๋ณด๋ผ๊ฒฝ์ฐ 415 Unsupported Media Type
Error๊ฐ ๋ฐ์ํ๋ค. ์ ํํ๋ ์๋์ ๋์์๋ application/octet-stream
๋
์ ๋๋ฌธ์ด๋ค.
appication/octet-stream
8๋นํธ ๋จ์์ ๋ฐ์ด๋๋ฆฌ ๋ฐ์ดํฐ๋ฅผ ์๋ฏธํ๋ค.
์ ๊ฐ์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ด์ ๋ ์๋ฒ์์๋ json ํ์ ์ผ๋ก rq๋ฅผ ๋ฐ๋ ๊ฒ์ผ๋ก ์ ์ํ๋๋ฐ, ํด๋ผ์ด์ธํธ์์ string์ผ๋ก ๋ณํํด์ ๋ณด๋๊ธฐ ๋๋ฌธ์ ์๋ฌ๊ฐ ๋ฐ์ํ ๊ฒ์ด๋ค.
ํด๊ฒฐ๋ฒ์ ๋ค์๊ณผ ๊ฐ๋ค.
formdata์ appendํ ๋ JSON.stringify๋ก ์์ฑํ ๋ฌธ์์ด์ Blob์ผ๋ก ๋ง๋ค๊ณ ๊ทธ ํ์
์ application/json
์ผ๋ก ์ง์ ํด์ฃผ๋ฉด ๋๋ค.
// 415 Error
await formData.append('uploader', JSON.stringify(uploader));
// Good ๐
const uploaderString = JSON.stringify(uploader);
await formData.append('uploader', newBlob([uploaderString], {type: 'application/json'}));
์ด๋ฒ ํฌ์คํ
์ ์์ฑํ๋ฉด์ ๊ฝค ๋ค์ํ Content-Type
์ ์กฐ์ฌํ๊ณ ๋ค๋ค๋ณธ ๊ฒ ๊ฐ๋ค.
์ฌ์ค ๋ฌธ์ ์ ๋ง์ฃผ์ณค์ ๋ ๋ง์ง๋ง ๋ถ๋ถ์ ํด๊ฒฐ๋ฒ๋ง ์๋ฉด ๊ตฌํ์ ํ ์ ์์ง๋ง, ์ฌ๊ธฐ๊น์ง ๋๋ฌํ ๊ณผ์ ๊ณผ ์ด๊ฒ์ ํตํด ๋ฐฐ์ด ์ง์๋ค์ด ๋ ๊ฐ์ง ๊ฒ ๊ฐ๋ค.
ํฌ์คํ ์ ์ฌ์ฉ๋ ์์ ์ฝ๋๋ ๊นํ๋ธ์ ๋ชจ๋ ์ฌ๋ ค๋์์ต๋๋ค. (https://github.com/huewilliams/velog-examples/tree/main/fileAndJson)
๋๊น์ง ์ฝ์ด์ฃผ์ ๋ถ๋ค ๊ฐ์ฌํฉ๋๋ค!! ๐ฅณ๐ฅณ
์ค ์ ๋ ์๋๋์ค๋ง ์์๋๋ฐ ์ด๋ฐ ๋ฐฉ๋ฒ์ด ์์๊ตฐ์!