블로그 미디어를 Firebase Hosting에서 Storage로 옮겨 파일을 정리하려다 이미지 6개와 음성 3개를 원래 자리로 되돌렸어요. 그 파일은 다시 지웠고, 17분 17초 사이 네 번의 변경이 남았지만 방문자 피해는 확인되지 않았어요.

무슨 상황이었냐면요

이 블로그는 글과 화면을 Firebase Hosting에서 제공하고, 글에 들어가는 이미지와 음성은 Firebase Storage에 두려 했어요. Hosting은 사이트 파일을 방문자에게 보여 주고, Storage는 미디어 파일을 저장해 제공해요. 처음에는 일부 미디어도 Hosting에 있어서 파일을 옮기면 예전 주소가 끊길 수 있었어요.

글을 쓰는 Codex와 검수하는 Claude가 따로 있고, 실행기가 검사와 공개 절차를 이어요. 당시 글은 아직 공유되지 않았는데도 옛 주소를 지키는 작업이 이어졌어요. 여기서 ‘커밋’은 그때그때 저장한 변경 기록이에요. 작업의 출발점은 미디어가 놓일 곳을 정리하는 일이었는데, 중간부터 확인되지 않은 옛 링크를 보존하는 일이 앞에 섰어요.

화려한 장애 복구 이야기는 아니에요. 방문자가 실제로 깨진 이미지를 봤다는 기록도 없어요. 다만 파일을 옮기는 AI 에이전트가 ‘혹시 누군가 이 주소를 받았을지 모른다’는 가설을 확인하기 전에 설정과 파일을 잇달아 바꾼 과정은, 자동화 작업에서 다시 만날 수 있는 함정이에요.

처음에는 어디를 의심했나

오류가 난 것은 아니었어요. 새 글은 이미 Storage 주소로 파일을 잘 읽고 있었어요. AI가 걱정한 것은 이전 두 글에 쓰던 옛 Hosting 주소가 누군가에게 공유됐을 수 있다는 점이었어요. 파일을 옮기면 그 주소로 만든 공유 미리보기가 끊길 수 있다고 본 거예요. 이 걱정은 그 뒤 문서에 ‘공유 미리보기 호환’이라는 말로 남았어요. 그런데 그 주소를 받은 사람이 실제로 있는지는 아무도 확인하지 않은 채였어요.

처음 의심한 곳은 옛 링크의 호환성이었어요. 주소가 바뀌면 누군가 저장해 둔 링크가 깨질 수 있다는 생각 자체는 점검할 만해요. 하지만 이 경우에는 ‘누가 그 주소를 받았는지’와 ‘현재 어느 글이 그 주소를 쓰는지’를 먼저 대조해야 했어요. 기록된 대화에는 글이 아직 공유되지 않았다고 나와요. 공유를 가정해서 만든 대책과 실제로 확인된 사용 현황 사이에 빈칸이 있었던 거예요.

새 주소에서 파일을 읽는다는 사실도 옛 주소의 상태를 알려 주지는 않아요. 방문자가 옛 주소로 요청했을 때 어떤 응답을 받는지는 별도의 문제예요. 그 요청 결과를 적은 기록 없이 주소 보존책을 먼저 만드는 순간, 작업의 성공 기준이 ‘실제로 열리나’에서 ‘설정이나 파일을 남겼나’로 바뀌었어요.

진범은 있을지도 모르는 링크를 먼저 지킨 데 있었어요

9월 27일 밤 10시 53분 8초, 커밋 6cbe5c7(Move blog media to Firebase Storage)에서 이미지 6개와 음성 3개, 모두 9개 파일을 Hosting에서 빼고 Storage로 옮겼어요. 커밋 메시지는 블로그 미디어를 Storage로 옮겼다는 뜻이에요. 11시 5분 30초 커밋 b43ea6e(Redirect legacy media links to Storage)에서는 예전 주소를 새 주소로 보내는 리디렉션 설정 12줄을 추가했어요. 메시지는 옛 미디어 링크를 Storage로 돌린다는 뜻이에요. 리디렉션은 예전 주소로 온 방문자를 새 주소로 보내는 규칙이에요. 설정 파일 firebase.json에 남은 핵심 한 줄은 이거예요.

"source": "/assets/covers/:file*"

이미지의 옛 주소에서 파일 이름 부분을 받아들이는 패턴이에요. 같은 설정에는 음성 주소 /assets/audio/:file*도 들어 있었어요. 설정에 type: 301을 적으면 영구 이동 응답을 의도한다는 뜻이지만, 실제 주소가 어디로 이동했고 끝의 이미지가 열렸는지는 별도 요청으로 봐야 해요. 공개 주소로 요청해 본 결과는 이 커밋 기록에 남아 있지 않아요.

11시 7분 32초 커밋 b940e98은 방금 넣은 설정 12줄을 지우고, 옮겼던 이미지 6개와 음성 3개를 Hosting에 다시 놓았어요. 같은 커밋의 문서에는 옛 직링크를 ‘공유 미리보기 호환을 위해’ 작은 파일만 남겨 둔다고 적혀 있어요. 직링크는 글 페이지를 거치지 않고 파일로 바로 가는 주소예요. 커밋 메시지는 다음과 같아요.

Keep legacy media links working

‘기존 미디어 링크가 계속 작동하게 한다’는 뜻이에요. 리디렉션 설정을 넣은 지 2분 2초 만에 지우고 파일을 되돌린 이유는 기록에 따로 없어요. 설정이 제대로 작동하지 않아서였는지, 파일을 직접 남기는 편이 낫다고 판단해서였는지 단정할 수 없어요. 확인되는 것은 방식이 설정에서 파일 복원으로 바뀌었다는 사실이에요.

당시 제가 물은 말은 이랬어요.

공유된 글은 아직 없는데, 지금 이 작업을 왜 해?

옛 링크가 이미 누군가에게 전달됐는지를 묻는 말이에요. 기록된 대화에 따르면 글은 아직 공유되지 않았어요. 링크 보존에 쓴 작업의 필요성이 먼저 확인되지는 않았던 셈이에요.

11시 10분 25초 커밋 aafc5fc에서 Hosting에 복원한 옛 파일을 제거했어요. 복원한 지 2분 53초 뒤예요. 커밋 메시지는 다음과 같아요.

Remove unused legacy media from Hosting

‘사용하지 않는 옛 미디어를 Hosting에서 제거한다’는 뜻이에요. 같은 커밋에서 문서 문구도 ‘아직 공유된 옛 미디어 직링크가 없으므로 호환 파일은 Hosting에 남기지 않는다’로 바뀌었어요. 처음에 지키려던 것이 기록상 공유되지 않은 주소였다는 결론이에요. 옛 주소별 응답이나 방문자 영향은 별도 접속 기록이 없어 확인하지 못했어요.

남은 흔적을 한 줄로 놓으면 이래요. 9개 파일 이동 → 옛 주소 리디렉션 추가 → 설정 제거·9개 파일 복원 → 복원 파일 제거예요. 네 커밋 사이의 17분 17초는 이 왕복에 든 시간이고, 9개는 이미지 6개와 음성 3개를 합친 파일 수예요. 네 번은 방문자가 겪은 오류 횟수가 아니라 저장소에 남은 변경 단계의 수예요. 숫자만 보면 큰 사고처럼 보일 수 있지만, 실제 사용자에게 어떤 응답이 갔는지는 그 숫자로 알 수 없어요.

시각을 정확히 적어 두면 왜 이 왕복을 한 작업으로 보는지도 드러나요. 첫 이동에서 리디렉션 추가까지 12분 22초, 그 설정에서 파일 복원까지 2분 2초, 복원에서 마지막 제거까지 2분 53초였어요. 즉 17분 내내 서버가 고장 났다는 뜻이 아니라, 서로 다른 보존 방법을 짧은 간격으로 기록했다는 뜻이에요. 어느 설정이 실제 배포되어 방문자에게 보였는지, 그 사이 누가 옛 주소를 요청했는지까지 이 커밋 시각이 알려 주지는 않아요. 그래서 이 시간을 장애 지속 시간으로 부르지 않아요.

설정이 있다는 것과 주소가 열린다는 것은 달라요

이 사건에서 확인된 것은 네 번의 파일·설정 변경이에요. 리디렉션 설정을 작성한 것과 방문자가 예전 주소로 파일을 받는 것은 다른 단계예요. 설정 파일은 서버에 어떤 동작을 시키려는지 적은 기록이고, 주소의 응답은 그 동작이 밖에서 어떻게 보이는지 보여 줘요. 변경을 커밋했다고 해서 공개 서버가 그 설정을 적용했다는 뜻도 아니에요. 공개 주소의 응답을 보지 않았다면 ‘링크가 살아 있다’고 말할 근거도 아직 없어요.

파일을 지우기 전에 그 주소가 공유됐는지 확인하고, 보존해야 한다면 실제 요청으로 응답과 이동할 주소를 확인해야 해요. 예전 링크를 받은 사람이 없다는 근거와, 예전 링크가 기술적으로 열리지 않는다는 사실도 서로 달라요. 앞의 판단은 호환 파일을 남길 필요가 있는지를 가르고, 뒤의 검사는 주소를 남기기로 했을 때 그 약속이 작동하는지를 가려요.

그래서 하지 말아야 할 것

첫째, 쓰이는지 확인하지 않은 주소를 지키려고 원본부터 지우지 않아요. 글 안의 참조와 실제 공유 여부를 먼저 살펴보면 무엇을 보존할지 결정할 수 있어요.

둘째, 설정을 추가했다는 기록만 보고 링크가 살아 있다고 판단하지 않아요. 설정은 의도이고 방문자가 받는 응답은 결과예요. 보존을 약속했다면 두 가지를 함께 기록해야 해요.

셋째, 정리가 불확실하면 원본과 새 파일 수를 대조한 뒤 되돌릴 수 있는 단계로 나눠요. 이번에는 9개 파일을 옮기고 복원하고 제거했으므로, 단계마다 이미지 6개와 음성 3개의 위치를 대조했어야 해요.

넷째, 리디렉션을 쓴다면 옛 주소의 응답뿐 아니라 이동 뒤 파일도 확인해요. 첫 응답이 이동을 가리키더라도 마지막 주소에서 파일을 받지 못하면 링크 보존은 끝나지 않은 거예요. 예전 주소를 확인할 때는 다음처럼 응답 헤더를 볼 수 있어요.

curl -I https://nanoplaydays.com/assets/covers/확인할-파일.webp

이 명령은 해당 주소의 응답 상태와 이동할 주소를 보여 줘요. 확인할-파일.webp는 실제로 사용했던 파일 이름으로 바꿔야 해요. 이동했다면 마지막 파일이 실제로 열리는지도 이어서 확인해야 해요. 이 글을 위해 과거 공개 주소의 응답을 재현했다고 주장하는 명령은 아니에요.

마치며

9개 파일을 옮겼다가 되돌리고 다시 지운 17분 17초는 주소의 사용 여부와 실제 응답을 각각 확인해야 한다는 기록으로 남았어요. 여러분이라면 파일을 옮기기 전에 어떤 주소부터 열어 보시겠어요?