next.config.js에 다음과 같이 설정을 추가하여 res.cloudinary.com에서 이미지를 가져올 수 있게 허용한다.
이제 이미지 URL을 이용해서 다음과 같이 이미지를 가져와볼 수 있다.
이를 Vercel에서도 쓸 수 있게 위의 환경변수들을 Vercel에서도 추가한다. 현재 환경 변수들은 다음과 같은 것들이 있다.
3. 이미지 서빙 시스템 설계
기존에는 모든 이미지를 다 웹사이트 빌드 시에 같이 넣어 주었다. 그런데 이제는 cloudinary를 사용할 것이다. 하지만 그럼 기존에 쓰던 이미지 저장 방식은 아예 버려야 할까?
그럴 수도 있겠지만 이미지 저장 방식을 바꿀 때 새로운 방식밖에 쓸 수 없도록 바꾸는 게 좋지는 않다고 생각한다. cloudinary를 무제한으로 쓸 수 있는 것도 아니기에 언젠가 기존의 저장 방식으로 돌아와야 할 수도 있다. 그리고 또다른 클라우드 저장소를 사용하게 될 수도 있다.
이런 걱정을 하는 이유는 물론 돈이다. 나는 프리티어나 아주 저렴한 요금제밖에 쓸 수 없는데 cloudinary의 유료정책은 꽤 비싸니까...
따라서 blog-config.ts의 blogConfig에 이미지를 어디에 저장할지도 택할 수 있게 하자. 기본값은 local이다.
blogConfig.imageStorage 값이 local이면 public/images에 저장하고, cloudinary면 cloudinary에 저장하도록 하고 이미지 URL은 2가지로 저장하여 사용자가 설정하는 blogConfig.imageStorage에 따라서 불러오도록 하자.
4. 메인 페이지 이미지 최적화
메인 페이지에 있는 이미지는 내 프로필의 것을 뺀다면 고작 4개뿐이다. 프로젝트들의 이미지들이다. 그리고 이는 동적으로 생성되는 게 아니기 때문에 바꾸기도 쉽다. Cloudinary에 업로드한 후 각 이미지를 쓰는 태그의 src를 바꿔주면 된다.
먼저 blog-project.ts에서 프로젝트의 저장 시 image URL 타입을 local, cloudinary 두 URL 모두가 담길 수 있도록 변경하자.
그리고 cloudinary media library에서 /blog 폴더를 생성한다.
이렇게 생성한 폴더에 프로젝트 사진들(/public/project에 있던 그 이미지들)을 업로드한다. 그러면 URL이 생기는데 이를 projectList의 프로젝트 이미지에 넣어주자.
전체 URL을 넣어 놓으면 cloudinary cloud name이 노출되어서 문제가 있지 않을까 생각했는데 cloudinary 공식 사이트의 글을 보니 cloud name과 API key는 노출되어도 상관없다고 한다.
API secret만 노출되지 않도록 잘 숨기면 된다고 한다. 따라서 blog-project.ts에서는 cloudinary URL을 다음과 같이 저장하자.
프로젝트를 보여주는 ProjectCard 컴포넌트에서는 blogConfig.imageStorage에 따라서 다른 이미지 URL을 사용하도록 하자.
5. 글 썸네일 이미지 최적화
현재 글 썸네일 같은 경우 src/plugins/make-thumbnail.mjs에서 생성하여 변환 파일의 data._raw.thumbnail에 파일 경로를 넣어주고 있다. 따라서 기존의 파일 경로를 thumbnail.local로 바꾸고 thumbnail.cloudinary를 추가하자.
그러기 위해서는 썸네일 생성과 함께 이미지 업로드가 먼저 되어야 한다. make-thumbnail.mjs의 기존 코드에서 이미지 생성까지는 이미 잘 하고 있으므로 cloudinary에 업로드하는 코드만 추가하면 된다. makeThumbnail함수를 수정하자.
thumbnail.local에 저장되어 있는 이미지를 cloudinary에 업로드하고 thumbnail.cloudinary에 URL을 저장하자. upload API 문서의 응답을 보면 응답의 secure_url에 이미지 URL이 담겨있다는 걸 알 수 있다. 이걸 썸네일 URL로 지정하자. 그냥 url도 있지만 그건 http 주소라서 그걸 쓰면 보안 경고가 뜰 것이다.
그리고 이렇게 받아온 thumbnail 중 blog-config.ts에서 지정하고 있는 imageStorage를 사용하도록 하기 위해 Card컴포넌트를 수정한다. CardProps 함수도 수정하고 비슷한 타입을 쓰는 모든 부분을 수정한다.
이렇게 thumbnail의 주소를 blogConfig.imageStorage에 따라서 다르게 가져오도록 수정해야 했던 코드의 다른 부분들은 당시의 커밋 내역에서 확인할 수 있다.
6. 이미지 중복 제거, 최적화
그런데 문제가 있다. run dev를 할 때마다 혹은 빌드할 때마다 makeThumbnail이 계속 실행되어서 이미지가 계속 올라간다는 것이다.
이는 업로드 시 public ID를 주고, overwrite(같은 ID가 있을 때 덮어쓸지)를 false로 지정하면 된다.
makeThumbnail의 upload API 호출 부분을 다음과 같이 수정하자.
위를 보면 이미지를 300px로 줄여서 가져오고, 자동으로 파일 양식을 최적화도 하도록 하는 것을 볼 수 있다. URL의 c_scale,w_300,f_auto이 부분이 그 역할을 수행한다.
같은 작업을 프로젝트 이미지에도 해준다. 다음과 같은 형식으로 blog-project.ts의 배열을 수정하자.
7. blur 이미지 제공
사진을 보내오는 서버가 아무리 빨라도 사실 용량이 작은 사진을 쓰는 것만큼 빨라질 수는 없다. 따라서 이미지 로딩 시점에 사용할 사진을 준비하자.
cloudinary URL을 받아서 해당 이미지의 blur 이미지를 만들어주는 함수를 만들자. 이 함수는 src/utils/generateBlurPlaceholder.ts에 추가하자.
그전에 imagemin이라는 라이브러리를 설치하자. 이 라이브러리는 이미지를 최적화해주는 라이브러리다. imagemin-jpegtran도. 그리고 여기에 필요한 타입 라이브러리도 설치한다.
cloudinary URL의 이미지를 16px짜리 jpg로 받아서 imagemin 라이브러리를 이용해 축소한 후 base64로 인코딩하여 반환한다.
그리고 makeThumbnail 함수에서 위 함수를 이용해 썸네일의 blurURL을 생성해준다.
썸네일을 보여주는 Card 컴포넌트에서도 이 blur placeholder를 사용하도록 해준다.
이미지를 불러올 때 블러 처리된 이미지가 잠시 보이는 것을 확인할 수 있을 것이다.
그리고 Image 컴포넌트에 style={{ transform: 'translate3d(0, 0, 0)' }} 속성이 추가된 것을 볼 수 있다. 이건 요소를 옮기는 CSS인데 (0,0,0) 벡터만큼 평행이동한다는 뜻이므로 사실 아무 위치 변화가 없다.
이런 의미없어 보이는 CSS를 쓴 이유는 이렇게 하면 일부 기기에서 해당 요소 렌더링 시 GPU를 사용하도록 해주기 때문이다. 특히 사파리에서 GPU 렌더링은 유용하다.