Skip to main content

36장. 파일 업로드와 외부 서비스 연결하기

[6부] 파일 업로드와 외부 서비스 연결하기

[6부] 파일 업로드와 외부 서비스 연결하기

[6부] 파일 업로드와 외부 서비스 연결하기

내 앱이 갑자기 '진짜 서비스'처럼 보이는 순간

지금까지 우리는 회원가입, 로그인, 글 작성, 수정, 삭제까지 만들었다. 여기까지 왔으면 이미 꽤 훌륭하다. 그런데도 어딘가 조금은 연습용 앱처럼 느껴질 수 있다. 왜 그럴까?

이유는 간단하다. 아직 앱이 바깥세상과 활발하게 연결되지 않았기 때문이다. 사용자가 이미지를 올리고, 우리 서비스가 외부 저장소에 파일을 보관하고, 다른 API에서 데이터를 받아와 화면에 보여주기 시작하면 앱은 훨씬 더 제품답게 느껴진다. 프로필 이미지, 글 썸네일, 첨부파일, 지도 정보, 번역, 요약, 결제, 알림. 이런 기능들은 모두 "외부와 연결된 앱"에서 나온다.

이번 장에서는 두 가지를 함께 익힌다. 첫째는 파일 업로드다. 둘째는 외부 서비스 연결이다. 겉보기에는 다른 기능 같지만, 구조는 놀랄 만큼 비슷하다. 사용자 요청을 받고, 우리 서버나 스토리지가 처리하고, 결과를 다시 화면에 보여준다는 점에서 같은 패턴으로 이해할 수 있다.

파일 업로드는 사실 '파일 저장'보다 '경로 관리'가 중요하다

초보자는 파일 업로드를 생각하면 보통 "이미지를 어디에 저장하지?"부터 떠올린다. 물론 그것도 중요하다. 하지만 실제 서비스에서는 파일 자체보다 그 파일의 위치를 어떻게 기록하고 다시 불러오는지가 더 중요하다.

흐름은 보통 이렇다. 사용자가 파일을 고른다. 프론트엔드가 파일 크기와 형식을 1차로 확인한다. 그다음 파일을 서버나 스토리지로 보낸다. 업로드가 성공하면, 우리는 데이터베이스에 파일 자체를 넣는 대신 파일 경로 또는 URL을 저장한다. 이후 목록 화면이나 상세 화면에서는 그 경로를 읽어서 이미지를 보여준다.12

즉, 게시글에 썸네일을 붙이는 기능은 실제로는 이런 식이다.

게시글 테이블에 thumbnail_url 같은 칼럼이 있다. 사용자가 이미지를 업로드한다. 스토리지에서 파일 주소를 돌려준다. 그 주소를 게시글 데이터와 함께 저장한다. 나중에 글 목록을 불러올 때 그 주소를 <img>나 next/image에 넣어 보여준다.

이 흐름을 이해하면, 프로필 이미지든 첨부파일이든 거의 같은 방식으로 확장할 수 있다.

개발용 저장과 운영용 저장은 다르게 생각하자

학습 단계에서는 Next.js 프로젝트 안의 public/uploads 같은 폴더에 파일을 저장하는 방법도 충분히 좋은 연습이 된다. 동작 원리를 이해하기 쉽고, 눈으로 결과를 확인하기도 편하다. 다만 이런 방식은 로컬 개발 환경에서는 잘 보여도, 실제 배포 환경에서는 문제가 생기기 쉽다. 특히 Vercel 같은 환경에서는 실행 중 파일 시스템에 쓴 내용이 영구적으로 유지되지 않기 때문에, 업로드 파일을 안정적으로 보관하는 방식으로는 적합하지 않다.1

그래서 운영 환경에서는 보통 전용 파일 저장소를 쓴다. 이 책에서는 Supabase를 이미 사용하고 있으니, 가장 자연스러운 선택은 Supabase Storage다. Storage는 파일을 보관하는 공간이고, 버킷은 그 파일들을 담는 큰 통이라고 생각하면 된다. 폴더는 정리용 서랍이다. 데이터베이스가 텍스트와 숫자를 잘 다룬다면, Storage는 이미지·문서·영상 같은 파일을 맡는 창고에 가깝다.24

우리 서비스에 붙여볼 가장 쉬운 예: 글 썸네일 업로드

예를 들어 우리가 만드는 서비스에 글 대표 이미지를 붙인다고 해보자. 사용자는 글 작성 폼에서 이미지를 선택한다. 업로드가 끝나면 미리보기가 보인다. 글 저장 버튼을 누르면 게시글 내용과 함께 이미지 주소가 저장된다. 이후 목록에서는 작은 카드 이미지로, 상세 화면에서는 큰 대표 이미지로 쓸 수 있다.

이 기능 하나만 추가해도 서비스 인상이 크게 달라진다. 목록이 더 풍성해지고, 사용자는 "아, 이건 그냥 텍스트 연습장이 아니라 실제 서비스구나"라고 느끼게 된다.

구현은 보통 두 패턴 중 하나를 따른다.

  • 브라우저에서 바로 Storage로 업로드하고, 성공한 경로만 DB에 저장한다.

  • 브라우저에서 우리 서버로 먼저 보내고, 서버가 검증하거나 가공한 뒤 외부 스토리지에 저장한다.

  • 업로드가 끝난 뒤 URL을 받아 화면 미리보기와 저장 처리에 사용한다.

입문 단계에서는 첫 번째 패턴이 이해하기 쉽고, 비밀 키를 다뤄야 하거나 추가 검증이 많아지면 두 번째 패턴으로 확장하면 된다.

Supabase Storage로 이해하는 업로드의 기본 패턴

Supabase Storage에서는 버킷을 만들고, 그 안에 파일을 업로드할 수 있다. 작은 파일은 표준 업로드 방식으로 다루기 쉽고, 문서 기준으로는 일반 업로드가 6MB 이하의 파일에 적합하다.3 프로필 이미지나 글 썸네일 정도라면 대부분 이 범위 안에 들어온다.

중요한 점은 같은 경로에 계속 덮어쓰지 않는 습관이다. 문서에서도 같은 파일 경로를 덮어쓰는 방식은 CDN 반영 지연 때문에 이전 이미지가 잠시 보일 수 있어 가능하면 새 경로를 쓰는 편을 권장한다.3 그래서 실무에서는 userId/20260423-abc123.png처럼 사용자 폴더와 고유한 파일명을 함께 쓰는 경우가 많다.

그리고 버킷은 공개 여부를 나눠서 생각해야 한다. 모두가 볼 수 있는 게시글 썸네일은 public 버킷이 편하다. 반대로 이력서, 계약서, 결제 증빙처럼 민감한 파일은 private 버킷에 두고 필요할 때만 서명된 URL, 즉 일정 시간만 열리는 임시 주소를 발급하는 방식이 안전하다.4

여기서 앞 장의 권한 개념이 다시 살아난다. "내 글은 내가 수정한다"가 DB 권한이었다면, "내 파일은 내 폴더에만 올린다"는 Storage 권한이다. 서비스는 점점 하나로 연결되기 시작한다.

이미지를 보여줄 때는 Next.js 설정도 챙겨야 한다

업로드가 끝났다고 바로 화면 표시가 끝나는 것은 아니다. Next.js에서 외부 이미지 URL을 next/image로 보여주려면 외부 주소를 허용하는 설정이 필요할 수 있고, 이미지에는 alt를 넣어야 하며, 보통 width와 height를 주거나 fill을 사용해야 한다.5

이 부분을 놓치면 업로드는 성공했는데 화면에는 이미지가 안 뜨는 묘한 상황을 만나게 된다. 초보자 입장에서는 "분명 URL도 있는데 왜 안 보이지?" 하고 당황하기 딱 좋다. 이럴 때는 업로드 로직보다 먼저, 브라우저에서 URL이 직접 열리는지, Next.js 이미지 설정이 맞는지, 컴포넌트에 필요한 속성이 들어갔는지를 차근차근 확인하면 된다.5

외부 API 연결은 '우리 서버가 대신 전화하는 것'이라고 이해하자

이제 파일 업로드 말고 외부 API 연결을 보자. 예를 들어 이런 기능을 붙일 수 있다.

  • 주소를 입력하면 지도 좌표를 가져오기

  • 글 제목을 바탕으로 요약 문구 추천받기

  • 업로드한 이미지를 분석해 태그를 붙이기

  • 번역 API로 다국어 문장 만들기

이때 초보자가 꼭 기억할 한 가지가 있다. 외부 API 키는 브라우저에 두면 안 된다는 점이다. 사용자의 브라우저는 누구나 들여다볼 수 있다. 따라서 외부 서비스 호출은 보통 Next.js의 서버 라우트나 서버 액션에서 처리한다. 사용자는 우리 앱에 요청을 보내고, 우리 서버가 외부 API에 대신 요청한 뒤, 필요한 데이터만 정리해서 다시 사용자에게 돌려준다.

이 흐름은 식당 주문과 비슷하다. 손님이 주방으로 직접 뛰어들어가 요리하지 않는다. 손님은 주문을 하고, 홀 직원이 주방과 연결한다. 여기서 우리 서버가 바로 그 직원 역할을 한다.

외부 연결 기능은 '가져오기'보다 '정리하기'가 더 중요하다

외부 API를 처음 붙이면 많은 사람이 응답을 그대로 화면에 보여주려 한다. 하지만 서비스 품질은 데이터를 얼마나 예쁘게 정리하느냐에서 갈린다.

예를 들어 지도 API가 위도, 경도, 행정구역 코드, 포맷된 주소, 원본 주소 후보 5개를 한꺼번에 준다고 해보자. 우리 서비스에 필요한 것이 "사용자에게 보여줄 한 줄 주소"와 "DB에 저장할 좌표값"뿐이라면, 그 두 가지만 골라 다루는 것이 좋다. 외부 응답을 우리 앱에 맞는 형태로 바꾸는 과정을 정규화라고 생각하면 된다. 어려운 말 같지만, 쉽게 말해 "남이 준 데이터를 우리 방식으로 정리하는 일"이다.

이 습관이 중요하다. 그래야 나중에 외부 서비스를 바꾸더라도 화면과 DB 구조를 크게 흔들지 않을 수 있다.

연결할수록 더 중요해지는 안전장치

외부 서비스와 연결되는 순간부터는 기능보다 방어가 더 중요해진다. 특히 파일 업로드는 사용자가 직접 무언가를 보내는 기능이기 때문에 더 조심해야 한다.

  • 업로드 전에 파일 형식과 크기를 검사한다. 이미지만 받겠다면 image/* 수준이 아니라 서버에서도 MIME 타입과 크기를 다시 확인하는 습관이 좋다.3

  • 파일명 충돌을 피하기 위해 시간값이나 UUID를 붙인다. 같은 이름의 image.png는 언젠가 반드시 사고를 부른다.

  • 공개 파일과 비공개 파일을 처음부터 분리한다. "일단 다 public으로 두고 나중에 바꾸자"는 말은 대개 나중에도 안 바뀐다.

  • API 키와 서비스 롤 키는 서버 전용 환경변수에 두고, 브라우저 코드에는 공개 가능한 키만 사용한다.

  • 실패했을 때 사용자에게는 쉬운 메시지를 보여주고, 개발자는 로그를 통해 원인을 찾을 수 있게 해둔다.

이 다섯 가지만 지켜도 초보 프로젝트에서 생기는 대부분의 큰 사고를 줄일 수 있다.

AI와 함께 만들 때는 기능을 둘로 쪼개서 요청하자

이 장의 기능은 Claude Code나 Codex에게 한 번에 통째로 맡기면 자주 꼬인다. 업로드 UI, 스토리지 연결, DB 저장, 이미지 표시, 권한 처리, 에러 메시지까지 한꺼번에 섞여 있기 때문이다.

그래서 이런 순서가 좋다.

  • 먼저 업로드 UI만 만든다. 파일 선택, 미리보기, 업로드 중 상태 표시까지.

  • 다음으로 Storage 업로드만 붙인다. 성공하면 URL을 반환받도록.

  • 마지막으로 게시글 저장 로직과 연결한다. 업로드 URL을 DB에 함께 저장하도록.

외부 API도 마찬가지다. "번역 기능 전체 만들어줘"보다 "서버 라우트에서 외부 API를 호출해 한국어 문장을 영어로 번역한 결과만 반환해줘"처럼 한 단계씩 요청하는 편이 훨씬 안정적이다.

이번 장에서 꼭 남겨야 할 감각

파일 업로드와 외부 API 연결은 겁먹을 만큼 복잡한 마법이 아니다. 결국은 입력을 받고, 밖으로 보내고, 결과를 저장하고, 다시 보여주는 흐름이다.

다만 이 순간부터 앱은 더 이상 혼자 움직이지 않는다. 스토리지, CDN, 외부 API, 환경변수, 권한 정책, 에러 처리까지 함께 고려해야 한다. 그래서 조금 더 어렵지만, 동시에 훨씬 더 서비스답다.

바로 이 지점에서 초보자는 한 단계 성장한다. "페이지를 만든 사람"에서 "작동하는 제품을 연결하는 사람"으로 넘어가는 순간이다.

참고

1How to Upload Images to Public Directory in Next.js with TypeScript

2Storage Quickstart | Supabase Docs

3Standard Uploads | Supabase Docs

4Storage | Supabase Docs

5Components: Image | Next.js

책 목록

바이브 코딩으로 완성하는 첫 풀스택 웹 서비스

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기