상품 이미지가 관리자 화면에서는 보이는데 상품 페이지에서는 깨졌다. 같은 파일을 가리키는데 한쪽만 404다.
Table of contents
Open Table of contents
증상 — 만드는 주소가 달랐다
관리자 화면의 이미지 주소를 복사해 보니 이랬다.
/upload/product/%ED%83%80%EC%9D%B4%EC%96%B4_215_60_16.jpg
상품 페이지 쪽은 이랬다.
/upload/product/타이어_215_60_16.jpg
앞은 URL 인코딩된 이름이고 뒤는 원문 그대로다. 브라우저가 알아서 맞춰 줄 것 같지만 그렇지 않았다.
읽는 코드도 통일돼 있지 않았다.
// 관리자
echo '<img src="/upload/product/' . rawurlencode($row['img']) . '">';
// 상품 페이지
echo '<img src="/upload/product/' . $row['img'] . '">';
한쪽은 rawurlencode 를 걸고 한쪽은 안 건다. 자료에 든 값은 하나인데 만들어지는 주소가 둘이다.
규격이 두 개면 어느 쪽이 맞는지가 파일마다 달라진다. 그래서 어떤 상품은 관리자에서만 보이고 어떤 상품은 상품 페이지에서만 보였다.
저장 시점에 이름이 변형됐다
업로드 처리 부분을 봤다.
$name = $_FILES['img']['name'];
move_uploaded_file($_FILES['img']['tmp_name'], $dir . $name);
브라우저가 보낸 이름을 그대로 쓴다. 문제는 브라우저마다 운영체제마다 보내는 형태가 다르다는 것이다.
디렉터리를 열어 보니 눈으로는 같아 보이는 이름이 여러 벌 있었다.
타이어_215_60_16.jpg
%ED%83%80%EC%9D%B4%EC%96%B4_215_60_16.jpg
타이어_215_60_16.jpg
셋째는 자모 분리형이다. 유니코드에는 같은 글자를 적는 방식이 둘 있는데 「상」을 한 글자로 담는 NFC 와 초성·중성·종성을 따로 담는 NFD 다.
바이트로 보면 전혀 다르다. rawurlencode 를 걸어 보면 NFC 는 %EC%83%81 이고 NFD 는 %E1%84%89%E1%85%A1%E1%86%BC 다. 화면에는 둘 다 「상」으로 보인다.
리눅스 파일시스템은 정규화를 하지 않고 바이트를 그대로 쓰며 비교도 바이트로 한다. 그러니 NFC 로 만든 경로로는 NFD 로 저장된 파일이 없는 것이 된다.
Normalizer::normalize($name, Normalizer::FORM_C) 로 받는 쪽에서 통일하는 것도 해 봤다. 그래도 이미 쌓인 것이 제각각이라 불러오는 쪽만으로는 닫히지 않았다.
이름을 만들어 저장했다
원본 이름을 그대로 쓰는 것을 그만뒀다.
$ext = strtolower(pathinfo($_FILES['img']['name'], PATHINFO_EXTENSION));
$name = date('Ymd') . '_' . uniqid() . '.' . $ext;
move_uploaded_file($_FILES['img']['tmp_name'], $dir . $name);
uniqid 로 만든 이름이라 디스크에는 영문과 숫자만 들어간다. 어떤 환경에서 올려도 정규화 형태를 따질 일이 없어지고 이름이 겹치는 문제도 사라졌다.
원본 이름은 버리지 않고 별도 컬럼에 넣었다. 내려받을 때 원래 이름으로 보여 줄 데가 있어서다.
ALTER TABLE product_image
ADD COLUMN origin_name VARCHAR(255) NULL AFTER file_name;
origin_name 은 표시 전용이고 디스크 경로는 file_name 만 쓴다.
확장자와 이미 쌓인 것
이름을 새로 만들면서 확장자를 그대로 옮겨 붙였는데 여기에도 구멍이 있었다. 확장자는 올리는 쪽이 정하는 문자열이라 .jpg 라고 적혀 있어도 내용이 그림이 아닐 수 있다.
$info = getimagesize($_FILES['img']['tmp_name']);
if ($info === false) {
return '이미지 파일이 아닙니다';
}
getimagesize 로 내용을 읽어서 그림인지 본다. finfo_open(FILEINFO_MIME_TYPE) 으로 형식을 확인해 확장자를 정하면 이름과 내용이 어긋나지 않는다.
새로 올리는 것은 정리됐는데 기존 파일이 남아 있었다. 디렉터리를 훑어서 자료의 값과 대조했고 이때 양쪽을 Normalizer::FORM_C 로 맞춰 놓고 비교했다. 그러지 않으면 같은 파일이 서로 없는 것으로 잡힌다.
세 가지가 나왔다.
자료에 있고 파일도 있다 새 규격으로 이름을 바꾸고 값을 갱신
자료에 있는데 파일이 없다 목록으로 남겨서 다시 올리게 함
파일은 있는데 자료에 없다 상품이 지워진 잔여물
두 번째가 40건쯤 됐다. 인코딩이 어긋난 채로 저장돼서 어느 쪽 이름으로도 안 열리는 것들이었다.
정리
$_FILES의 이름을 그대로 쓰면 환경마다 다른 형태가 섞인다- 한글은 NFC 와 NFD 로 적을 수 있고 바이트가 다르다
rawurlencode를 걸면%EC%83%81과%E1%84%89%E1%85%A1%E1%86%BC로 갈린다- 리눅스 파일시스템은 정규화 없이 바이트로 비교한다
- 저장과 조회의 규격이 다르면 같은 파일을 못 찾는다
- 불러오는 쪽만
Normalizer로 맞춰도 닫히지 않는다 - 디스크에는
uniqid로 만든 ASCII 이름을 쓰고 원본 이름은 컬럼에 따로 둔다 - 확장자는 올리는 쪽이 적은 문자열이다. 내용을 읽어서 확인한다
- 이미 쌓인 것은
FORM_C로 맞춘 뒤 대조해 세 부류로 나눈다