당연하게 사용되는 파일 업로드 기능. 드래그 앤 드롭 한 번이면 끝나는 간단한 동작 같지만, 그 내부에는 생각보다 재미있는 원리들이 숨어있다. 최근 스토리지 시스템을 만들어 보는 개인 프로젝트를 진행하며 알게 된 원리인데 이번 글에서 간단하게 한 번 풀어보고자 한다.
"파일"이란 건 뭘까?
파일은 결국 숫자의 나열이다. 우리가 흔히 말하는 이진 데이터로 이루어진 것이 바로 파일이다.
- 텍스트 파일: "hi~" → [236, 149, 136, 235, 133, 149, 237, 149, 152, 236, 132, 184, 236, 154, 148]
- 이미지 파일: 각 픽셀의 RGB 값들 → [255, 0, 0, 128, 255, 200, ...]
- 영상 파일: 여러 이미지 + 오디오 데이터의 조합
이처럼 모든 파일이 결국 0과 1로 이루어진 숫자의 연속이라는 걸 알 수 있다.
파일의 실제 모습
브라우저에서 파일을 읽으면 실제로 이렇게 보인다.
return uploadFile({ file, ownerId, accountId, path }).then(
async (uploadFile) => {
if (uploadFile) {
// 파일의 이진 데이터 확인하기
try {
// 1. ArrayBuffer로 파일 읽기
const buffer = await file.arrayBuffer();
const bytes = new Uint8Array(buffer);
console.log('=== 파일 이진 데이터 분석 ===');
console.log('파일명:', file.name);
console.log('파일 크기:', file.size, 'bytes');
console.log('파일 타입:', file.type);
// 2. 처음 20바이트 확인 (파일 시그니처)
const firstBytes = bytes.slice(0, 20);
console.log('처음 20바이트 (10진수):', Array.from(firstBytes));
console.log('처음 20바이트 (16진수):',
Array.from(firstBytes).map
(b => b.toString(16).padStart(2, '0')).join(' ')
);
// 3. 파일 시그니처로 실제 파일 타입 확인
const signature = Array.from(firstBytes.slice(0, 4))
.map(b => b.toString(16).padStart(2, '0'))
.join('')
.toUpperCase();
const fileSignatures = {
'FFD8FF': 'JPEG 이미지',
'89504E47': 'PNG 이미지',
'474946': 'GIF 이미지',
'504B0304': 'ZIP/Office 문서',
'25504446': 'PDF 문서',
'52494646': 'RIFF (WAV/AVI)',
'49443': 'MP3 파일'
};
const detectedType = Object.entries(fileSignatures)
.find(([sig]) => signature.startsWith(sig))?.[1] || '알 수 없는 타입';
console.log('파일 시그니처:', signature);
console.log('감지된 파일 타입:', detectedType);
// 4. 전체 버퍼 정보
console.log('전체 ArrayBuffer:', buffer);
console.log('Uint8Array 길이:', bytes.length);
// 5. 메모리 사용량 체크 (대략적)
const memoryMB = (bytes.length / 1024 / 1024).toFixed(2);
console.log('메모리 사용량:', memoryMB, 'MB');
console.log('=== 분석 완료 ===');
}
catch (error) {
console.error('이진 데이터 읽기 실패:', error);
}
// console.log('uploaded file:', uploadFile);
setFiles((prevFiles) =>
prevFiles.filter((f) => f.name !== file.name)
);
}
}
)
<개인 프로젝트에서 따온 파일 업로드 코드>
이 숫자들이 바로 파일의 "내용물"인 것이다.
그렇다면 파일 업로드란?
파일 업로드는 이런 과정을 거친다.
- 내 컴퓨터의 숫자들을 읽어서
- 메모리라는 임시 공간에 복사하고
- 네트워크를 통해 서버로 전송해서
- 서버 저장공간에 똑같이 복사하는 것
여기서 중요한 점은 원본 파일은 그대로 남아있고, 복사본이 서버에 생기는 것이다.
왜 메모리를 거쳐야 할까?
메모리의 역할은 '임시 작업 공간'이라고 볼 수 있다. 파일을 바로 네트워크로 보낼 수는 없기 때문에 중간에 메모리라는 작업 공간이 필요한 것이다.
내 노트북 파일 → 메모리 (RAM) → 네트워크 → 서버 저장소
파일은 저장소에 잠자고 있는 상태라고 생각하면 편하다. CPU가 이 파일로 네트워크로 전송을 하든 뭘 하든 작업을 하려면 반드시 메모리로 깨워서 가져와야 한다.
비유로 더 상세하게 봐보도록 하자. 음...
창고에 있는 박스를 다른 곳으로 보내려면, 먼저 작업대로 가져와서 포장을 해야한다. 마찬가지로 파일을 네트워크로 보내려면 메모리로 가져와서 HTTP 형태로 포장하는 작업이 필요하다.
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="photo.jpg"
Content-Type: image/jpeg
[실제 이진 데이터가 여기에...]
------WebKitFormBoundary7MA4YWxkTrZu0gW--
<파일 데이터 HTTP 포장 예시 from chatGPT>
왜 저장소에서 바로 네트워크로 보낼 수 없을까?
- 저장소는 느리고, 메모리는 빠르기 때문이다.
- 저장소는 순차적 접근이 가능하지만 메모리는 랜덤 접근이 가능하다.
- CPU가 메모리에만 직접 소통할 수 있게 설계되어 있다.
크게 이 세 가지 이유 때문에 반드시 메모리를 거치는 게 좋지만 여기서 문제가 생긴다.
만약 2GB의 영상 파일을 업로드한다면?
2GB를 통째로 메모리에 올리면 그만큼 메모리 공간이 할당되어 다른 프로그램들이 느려지게 된다. 용량이 더 크다면 브라우저가 뻗을 수도 있을 것이다.
이 문제에 해결책은 조금씩 나눠서 처리하는 '스트리밍'이라는 방식이다. 파일을 작은 조각인 청크단위로 나눠서 처리하게 된다.
[ 2GB 영상 파일 ]
↓
[ 1MB 조각1 ] → 메모리 → 네트워크 → 서버
[ 1MB 조각2 ] → 메모리 → 네트워크 → 서버
[ 1MB 조각3 ] → 메모리 → 네트워크 → 서버
...
[ 2048개 조각 ] 완료!
이렇게 하면
- 메모리는 항상 1MB만 사용할 수 있고
- 다른 프로그램에 영향이 거의 없고
- 업로드 진행률도 실시간으로 파악이 가능하고
- 중간에 에러가 나도 전체를 다시 시작할 필요가 없게된다.
스트리밍 방식을 쓰냐 안 쓰냐는 사용자 경험에 중대한 영향을 미치기 때문에 이런 똑똑한 해결책이 필요하다. 이 덕분에 우리가 몇 GB짜리 영상도 문제없이 유튜브에 올릴 수 있고 구글 드라이브에 큰 파일 업로드도 가능해지는 것이다.
끝내는 말
이번 개인 프로젝트를 진행하면서 파일 업로드에 자세한 과정에 대해 학습해보았는데, 파일 업로드라는 평범한 기능 하나에도 이렇게 많은 원리들이 숨어있다는 게 신기했다. 겉으로는 단순해 보이는 드래그 앤 드롭 하나가 실제로는 이진 데이터 읽기, 메모리 관리, 네트워크 전송, 서버 저장이라는 과정을 거치고 있었던 것이다.
특히 스트리밍이라는 해결책이 없었다면 우리는 아직도 몇 MB짜리만 겨우 업로드/다운로드하며 "왤캐 느리냐"라고 투털거리고 있지 않았을까 한다. 항상 느끼는 거지만 기술의 발전이란 결국 이런 작은 문제들을 하나씩 해결해나가는 과정인 거 같다.
다음번에 파일 업로드 진행률 바가 조금씩 올라가는 걸 보면 '내 파일이 1MB씩 올라가고 있구나' 생각할 거 같다 ㅋㅋㅋ 당연하게 여겨왔던 기능들 뒤에 숨어있는 원리를 이해하면, 개발할 때도 더 깊이있는 사고가 가능하지 않을까?
'Frontend Dev Log' 카테고리의 다른 글
클라우드 스토리지의 Bucket(버킷)은 뭘까? (0) | 2025.07.03 |
---|---|
웹 사이트에 UTM 추적 붙이기 (0) | 2025.06.27 |
Dialog 닫은 뒤 클릭이 안 되는 이유? pointer-events: none 이슈 해결기 (1) | 2025.06.18 |
Tailwind CSS v4 + eslint-plugin-tailwindcss 충돌 이슈에 대해 (0) | 2025.06.17 |
cn() 함수: 왜 모든 shadcn, radix UI 컴포넌트에 들어갈까? (0) | 2025.06.09 |