![[Web][Issue] 이중 인코딩으로 인한 사진 누락](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FbRnkE0%2FbtsPnm7grFr%2FAAAAAAAAAAAAAAAAAAAAAMwXdmdzUbiaRpFi2ZynTnBV7KOWQ401KGO7N3Pl2Okj%2Fimg.png%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1753973999%26allow_ip%3D%26allow_referer%3D%26signature%3D30NTwt6zd%252F4sJvzXldve%252FrygesQ%253D)
개요
프론트에서 사진이 제대로 보이지 않는 문제가 발생했다. ▼
S3에는 분명히 이미지가 업로드 되어 있고, Postman으로 요청을 날려봐도 응답이 200으로 잘 오는데, 실제 화면에서는 이미지가 보이질 않았다. 처음엔 당연히 백엔드 문제라고 생각했고, 이게 뭐지 싶어 로깅도 켜고 요청 흐름도 추적해봤지만 서버에는 이미지가 잘 올라가 있었다.
클라이언트도 요청 값에 누락하는 일 없이 보내고 있었고, URL도 뭔가 이상 없어 보였다. 그럼 도대체 어디서 잘못된 걸까?
문제 상황
아래는 Postman에서 확인한 이미지 URL이다. ▼
https://aws_bucket/20240413%25EF%25BC%25BF165238.jpg
하지만 url에 직접 접속해서 보려고 하면 이런 오류가 나왔다. '백엔드에서 접근을 막아서 그런게 아닌가?' 라고 할 수 있지만, 해당 사진의 경우 다른 플랫폼들에서도 접근을 해야했기에 별도의 접근 제한 설정은 걸려있지 않았다. ▼
처음엔 대수롭지 않게 넘겼던 이 URL. 그런데 중간의 `%25EF%25BC%25BF` 부분이 눈에 띄었다.
혹시나 해서 디코딩을 해보니, 정체불명의 값이 튀어나왔다. 알고보니 두 번 인코딩된 문자였던 것.
이를 두번 디코딩하면 전각 언더스코어가 나오게 된다. ▼
%25EF%25BC%25BF → 디코딩 → %EF%BC%BF → 디코딩 → _ (전각 언더스코어)
즉, 전각 언더스코어 `_`가 `%EF%BC%BF`로 한 번 인코딩되고, 그것이 다시 인코딩되면서 `%`가 `%25`로 한 번 더 변환되면서 최종 URL에는 `%25EF%25BC%25BF`처럼 이중 인코딩된 문자열이 들어가게 된 것이었다.
원인 분석
이 문제를 추적하다보니 url을 만드는 코드가 문제라는 것을 깨달았다. ▼
const urlObject = new URL(url);
const pathname = urlObject.pathname;
이 `pathname`은 브라우저의 URL 규격에 따라 자동으로 인코딩이 된다. 예를 들어,
`Frame 30.jpg` → `Frame%2030.jpg`,
혹은
`_` → `%EF%BC%BF`
이런 식으로 말이다.
그리고 우리는 이 `pathname`을 그대로 파일명으로 사용해서 서버에 업로드하고 있었고, 이때 S3에서 또 한 번 인코딩을 적용하면서 URL이 `Frame%252030.jpg`처럼 변하게 된 것이다.
% → %25로 변환되기 때문에, 이미 인코딩된 %EF%BC%BF 또한 %25EF%25BC%25BF로 바뀌어버린다.
결과적으로,
- 로컬에서 볼 때는 `Frame 30.jpg`
- 브라우저에서 `pathname`으로 보면 `Frame%2030.jpg`
- S3에서 올라가면 `Frame%252030.jpg`
이런 인코딩 지옥이 펼쳐진 것이었다...
설계 시 고려할 점
- new URL().pathname은 인코딩된 값을 준다. 이것을 파일명으로 사용하는 건 매우 조심해야 한다.
- 업로드를 위해 URL을 쪼갤 때는 decodeURIComponent()를 사용해 원래의 파일명으로 되돌려 놓아야 한다.
- 파일명이 인코딩된 채로 서버에 업로드되면, 이후 S3 URL에서 자동 인코딩과 겹쳐서 정상적인 접근이 불가능해진다.
마치며
이슈 해결 과정은 마치 방탈출 같았다. 파일명 하나에 이런 함정이 있을 줄은 정말 몰랐고, `%25` 하나가 이미지 로딩 전체를 막고 있다는 걸 발견했을 때는 약간 허무했다. 단순한 문자열 같지만, URL 인코딩은 브라우저와 서버, 그리고 클라우드 플랫폼 간의 다층적인 규칙이 얽혀있는 민감한 영역이었다.
- URL path를 파일명으로 저장할 때는 반드시 디코딩을 거쳐야 한다.
- pathname은 브라우저가 자동으로 인코딩하는 값이라는 점을 항상 인식하고 사용하자.
- S3 URL은 자동 인코딩되므로, 이미 인코딩된 파일명을 올리면 이중 인코딩 문제가 발생한다.
'Develop > Web' 카테고리의 다른 글
[Next.js][Develop] Next.js 클린 아키텍처 적용기 (0) | 2025.07.10 |
---|---|
[React] Tailwind-css를 써보면서 느낀점 (0) | 2025.02.20 |
[React][Error] tailwind css 설치 오류 (1) | 2025.02.08 |
[JS] CommonJS와 ES모듈 (0) | 2024.12.13 |
[React][개발기] CI/CD 도입 (0) | 2024.08.08 |