Skip to content

Commit

Permalink
improve: image optimization (#99)
Browse files Browse the repository at this point in the history
  • Loading branch information
ojj1123 authored Mar 27, 2024
1 parent b110868 commit 97c7ebf
Show file tree
Hide file tree
Showing 20 changed files with 140 additions and 41 deletions.
17 changes: 6 additions & 11 deletions config/base.next.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const IS_PROD = process.env.NODE_ENV === "production";
const isProd = process.env.NODE_ENV === "production";
const cloudName = process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME || "";

// The folders containing files importing twin.macro
const path = require("path");
Expand Down Expand Up @@ -62,16 +63,10 @@ module.exports = () => ({
return config;
},
images: {
unoptimized: true,
remotePatterns: [
{
protocol: "https",
hostname: "**",
},
],
unoptimized: !isProd || !cloudName,
// Link: https://fe-developers.kakaoent.com/2022/220714-next-image/
imageSizes: [64, 256],
deviceSizes: [512],
deviceSizes: [440],
imageSizes: [100, 200],
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
},
sentry: {
Expand All @@ -81,6 +76,6 @@ module.exports = () => ({
// https://webpack.js.org/configuration/devtool/ and
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#use-hidden-source-map
// for more information.
hideSourceMaps: IS_PROD,
hideSourceMaps: isProd,
},
});
26 changes: 26 additions & 0 deletions config/cloudinary-loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"use client";

import type { ImageLoaderProps } from "next/image";

const cloudName = process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME || "";

export function cloudinaryLoader({ src, width, quality }: ImageLoaderProps) {
const rawTransformations = ["f_auto", "c_limit", `w_${width}`, `q_${quality || "auto"}`];
let isAbsolute: boolean;
let href: string;

if (src.startsWith("/")) {
href = src;
isAbsolute = false;
} else {
const hrefParsed = new URL(src);
href = hrefParsed.toString();
isAbsolute = true;
}

const cldUrl = `https://res.cloudinary.com/${cloudName}/image/fetch/${rawTransformations.join(
",",
)}/${href}`;

return isAbsolute ? cldUrl : src;
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { cloudinaryLoader } from "config/cloudinary-loader";
import Link from "next/link";

import { Icon } from "@/common/components/Icon";
Expand All @@ -17,7 +18,13 @@ export const LoginSideBarContent = (props: LoginSideBarContentProps) => {
<>
<div className="w-full rounded-24 bg-gray-100 px-16 pt-16 pb-24">
<Link className="flex items-center gap-12" href="/mypage">
<Photo className="h-50 w-50 rounded-20" src={user?.imageUrl} />
<Photo
alt={`${user?.name || ""}의 프로필 이미지`}
className="h-50 w-50 rounded-20"
loader={cloudinaryLoader}
sizes="50px"
src={user?.imageUrl || ""}
/>
<span className="grow text-left text-18-bold-140 text-gray-900">{user?.name}</span>
<Icon name="setting" />
</Link>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@ export const LogoutSideBarContent = (props: LogoutSideBarContentProps) => {
<>
<button className="w-full rounded-24 bg-gray-100 px-16 pt-16 pb-24" onClick={validate()}>
<div className="flex items-center gap-12">
<Photo className="h-50 w-50 rounded-20" src={defaultAvatarUrl} />
<Photo
unoptimized
alt="기본 프로필 이미지"
className="h-50 w-50 rounded-20"
sizes="50px"
src={defaultAvatarUrl}
/>
<span className="grow text-left text-18-bold-140 text-gray-900">{defaultName}</span>
<Icon name="setting" />
</div>
Expand Down
6 changes: 3 additions & 3 deletions src/common/components/Photo/Photo.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ export const Default = () => (
<>
<h1 className="text-20-bold-140">Resizeable Photo</h1>
<h2>width: 100, height: 100</h2>
<Photo className="h-100 w-100" src={IMAGE_SRC} />
<Photo unoptimized alt="예시 이미지" className="h-100 w-100" src={IMAGE_SRC} />
<hr />
<h2>width: 100, height: 200</h2>
<Photo className="h-200 w-100" src={IMAGE_SRC} />
<Photo unoptimized alt="예시 이미지" className="h-200 w-100" src={IMAGE_SRC} />
<hr />
<h2>Render fallback image(Wrong image src)</h2>
<Photo className="h-200 w-100" src="" />
<Photo unoptimized alt="예시 이미지" className="h-200 w-100" src="" />
</>
);
14 changes: 7 additions & 7 deletions src/common/components/Photo/Photo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,25 @@ import { useEffect, useState } from "react";
/**
* NOTE
* ComponentProps<typeof Image> 으로 타입 선언하면
* storybook jsdoc parse 오류 발생
* - interface 내부에 @deprecated 어노테이션이 있으면 문제 생기는 듯 보임
* storybook jsdoc parse 오류 발생. interface 내부에 @deprecated 어노테이션이 있으면 문제 생기는 듯 보임
* storybook v7으로 업그레이드 시 문제 발생하는지 확인해야 함
*/
interface Props extends Omit<ComponentProps<"img">, "placeholder"> {
interface Props extends ComponentProps<typeof Image> {
fallbackSrc?: string;
priority?: boolean;
unoptimized?: boolean;
}
const base64Blur =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mO8/Z8BAzAOZUEAQ+ESj6kXXm0AAAAASUVORK5CYII=";

const fallback = "/img/fallbackImage.png";

export const Photo = ({
src = "",
alt = "thumbnail",
src,
className = "",
width,
height,
fallbackSrc = fallback,
alt,
...props
}: Props) => {
/**
* FIX
Expand Down Expand Up @@ -55,6 +54,7 @@ export const Photo = ({
src={isFailLoading ? fallbackSrc : src}
style={{ objectFit: "cover" }}
onError={setIsFailLoading}
{...props}
/>
</div>
);
Expand Down
12 changes: 10 additions & 2 deletions src/common/components/RandomImge/RandomImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { ComponentProps } from "react";

import { Photo } from "../Photo";

interface Props extends ComponentProps<typeof Photo> {
interface Props extends Omit<ComponentProps<typeof Photo>, "alt" | "src" | "sizes"> {
images?: { name: string; src: string }[];
}
const randomImages = [
Expand Down Expand Up @@ -33,5 +33,13 @@ const randomImages = [
];
export const RandomImage = ({ images = randomImages, className = "" }: Props) => {
const randomImage = images[Math.floor(Math.random() * images.length)];
return <Photo alt={randomImage.name} className={className} sizes="32px" src={randomImage.src} />;
return (
<Photo
unoptimized
alt={randomImage.name}
className={className}
sizes="32px"
src={randomImage.src}
/>
);
};
2 changes: 1 addition & 1 deletion src/features/common/components/InfiniteMemeList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { MemeItem } from "@/features/common";
import type { Meme } from "@/types";

interface InfiniteMemeListProps {
memeList: Meme[];
memeList: (Meme & { priority?: boolean })[];
onRequestAppend: () => void;
}

Expand Down
6 changes: 4 additions & 2 deletions src/features/common/components/MemeItem.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { cloudinaryLoader } from "config/cloudinary-loader";
import { memo } from "react";

import { Icon } from "@/common/components/Icon";
Expand All @@ -8,7 +9,7 @@ import { MemeActionSheet, useMoveMemeDetail } from "@/features/common";
import type { Meme } from "@/types";

interface Props {
meme: Meme;
meme: Meme & { priority?: boolean };
onClick?: (id: number) => void;
}

Expand All @@ -32,7 +33,8 @@ export const MemeItem = memo(({ meme, onClick }: Props) => {
className="rounded-16"
draggable={false}
height={image.images[0]?.imageHeight}
sizes="100px"
loader={cloudinaryLoader}
sizes="200px"
src={image.images[0]?.imageUrl}
unoptimized={isEncodingError(image.images[0]?.imageUrl)}
width={image.images[0]?.imageWidth}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,13 @@ export const CategoryContent = () => {
gtmTrigger[category.name]
} flex w-full items-center justify-between gap-8 rounded-full px-4 py-12 text-16-semibold-140 [&>span>#chevronDown]:data-[state=open]:rotate-180`}
>
<Photo className="h-24 w-24 p-2" loading="eager" src={category.icon} />
<Photo
unoptimized
alt={category.name}
className="h-24 w-24 p-2"
loading="eager"
src={category.icon}
/>
<span className="flex-grow text-left text-16-semibold-140">
{category.mainTags.length ? (
<SlotCategory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,13 @@ export const FavoriteCategory = () => {
<Item value={FAVORITE_ID}>
<Header className="py-4">
<Trigger className="flex w-full items-center justify-between gap-8 rounded-full px-4 py-12 text-16-semibold-140 [&>span>#chevronDown]:data-[state=open]:rotate-180">
<Photo className="h-24 w-24 p-2" loading="eager" src={FAVORITE_ICON} />
<Photo
unoptimized
alt="북마크"
className="h-24 w-24 p-2"
loading="eager"
src={FAVORITE_ICON}
/>
<span className="flex-grow text-left text-16-semibold-140">{FAVORITE_ID}</span>
<span className="flex h-40 w-40 items-center justify-center rounded-full hover:bg-gray-100">
<Icon
Expand Down
19 changes: 16 additions & 3 deletions src/features/explore/tags/components/MemesByTagsContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { cloudinaryLoader } from "config/cloudinary-loader";
import { useRouter } from "next/router";
import * as React from "react";

Expand All @@ -18,7 +19,11 @@ export const MemesByTagsContainer = ({ tag }: Props) => {
}
return (
<>
<Thumbnail image={memeList?.[0].image.images[0].imageUrl} totalCount={totalCount as number} />
<Thumbnail
image={memeList?.[0].image.images[0].imageUrl}
tag={tag}
totalCount={totalCount as number}
/>
<InfiniteMemeList
memeList={memeList}
onRequestAppend={() => fetchNextPage({ cancelRefetch: false })}
Expand All @@ -29,9 +34,10 @@ export const MemesByTagsContainer = ({ tag }: Props) => {

interface ThumbnailProps {
image: string;
tag: string;
totalCount: number;
}
const Thumbnail = React.memo(function Thumbnail({ image, totalCount }: ThumbnailProps) {
const Thumbnail = React.memo(function Thumbnail({ image, tag, totalCount }: ThumbnailProps) {
const router = useRouter();
const clipboard = useClipboard();
const toast = useToast();
Expand All @@ -40,7 +46,14 @@ const Thumbnail = React.memo(function Thumbnail({ image, totalCount }: Thumbnail

return (
<div className="flex gap-16 px-22 pt-16 pb-24">
<Photo className="h-80 w-80 rounded-full" src={image} />
<Photo
priority
alt={`${tag} 밈 썸네일`}
className="h-80 w-80 rounded-full"
loader={cloudinaryLoader}
sizes="80px"
src={image}
/>
<div className="flex flex-1 flex-col items-center justify-center gap-2">
<span className="text-14-semibold-140 text-gray-900">{totalCount}개 밈</span>
<button
Expand Down
3 changes: 2 additions & 1 deletion src/features/memes/components/MemeDetail/MemeDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@ export const MemeDetail = ({ id }: Props) => {
>
<Photo
priority
unoptimized
alt={name}
className="max-h-[70vh] min-h-[25vh] w-full rounded-15"
height={imageHeight}
sizes="200px"
src={imageUrl}
width={imageWidth}
/>
Expand Down
10 changes: 9 additions & 1 deletion src/features/memes/components/MemeShareModal/MemeShareModal.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { cloudinaryLoader } from "config/cloudinary-loader";

import { usePostMemeToSharedCollection } from "@/api/collection";
import { useGetMemeDetailById } from "@/api/meme";
import { Modal } from "@/common/components/Modal";
Expand Down Expand Up @@ -39,7 +41,13 @@ export const MemeShareModal = ({ id, isOpen, onClose }: Props) => {
return (
<Modal open={isOpen} onClose={onClose}>
<Modal.Header />
<Photo className="my-24 h-300 w-300 rounded-15" src={src} />
<Photo
alt={name}
className="my-24 h-300 w-300 rounded-15"
loader={cloudinaryLoader}
sizes="300px"
src={src}
/>
<ul className="mx-auto mb-32 flex h-77 w-fit gap-16 whitespace-nowrap text-gray-600">
<li className="relative flex flex-col items-center gap-8">
<KakaoShareButton
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { cloudinaryLoader } from "config/cloudinary-loader";
import type { HTMLAttributes } from "react";

import { Photo } from "@/common/components/Photo";
Expand All @@ -18,6 +19,7 @@ export const SearchPopularItem = ({ name, imageSrc, ...rest }: Props) => {
alt={name}
// NOTE: Photo의 기본 className과 충돌나서 css props로 작성
css={{ position: "absolute", inset: 0, filter: "brightness(.5)" }}
loader={cloudinaryLoader}
loading="eager"
sizes="100px"
src={imageSrc}
Expand Down
2 changes: 1 addition & 1 deletion src/features/upload/components/UploadMeme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const UploadMeme = ({ src, isFocus }: Props) => {
setHeight(h);
}}
>
<Photo className="mx-16 rounded-16" src={src} />
<Photo unoptimized alt="업로드 이미지" className="mx-16 rounded-16" src={src} />
<UploadMemeData
className="max-h-[100rem] overflow-hidden transition-[max-height] duration-500 ease-in-out group-[:not(:focus-within)]:max-h-0"
css={{ maxHeight: height / 10 + "rem" }}
Expand Down
11 changes: 8 additions & 3 deletions src/features/upload/components/UploadMemeData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ export const UploadMemeData = ({ src, className }: Props) => {
return (
<div className={`flex flex-col gap-24 pt-24 pb-6 ${className}`}>
<div className="flex items-center gap-8 py-8 px-16">
<Photo className="h-24 w-24 rounded-12 bg-gray-300" src={src} />
<Photo
unoptimized
alt="썸네일"
className="h-24 w-24 rounded-12 bg-gray-300"
src={src || ""}
/>
<span className="text-14-semibold-140 text-gray-900">분노하는 ISTJ</span>
</div>
<div className="relative w-full px-16 text-18-semibold-140 leading-[160%]">
Expand All @@ -27,7 +32,7 @@ export const UploadMemeData = ({ src, className }: Props) => {
<Item value="밈 출처">
<Header className="border-b border-gray-100">
<Trigger className="flex w-full items-center justify-between gap-8 rounded-full px-24 py-16 text-16-semibold-140 [&>span>#chevronDown]:data-[state=open]:rotate-180">
<Photo className="h-24 w-24 rounded-12" />
<Photo unoptimized alt="밈 출처" className="h-24 w-24 rounded-12" src="" />
<span className="flex-grow text-left text-16-semibold-140">밈 출처</span>
<span className="flex h-24 w-24 items-center justify-center rounded-full hover:bg-gray-100">
<Icon
Expand Down Expand Up @@ -58,7 +63,7 @@ export const UploadMemeData = ({ src, className }: Props) => {
<Item value="밈 사용상황">
<Header className="border-b border-gray-100">
<Trigger className="flex w-full items-center justify-between gap-8 rounded-full px-24 py-16 text-16-semibold-140 [&>span>#chevronDown]:data-[state=open]:rotate-180">
<Photo className="h-24 w-24 rounded-12" />
<Photo unoptimized alt="밈 사용상황" className="h-24 w-24 rounded-12" src="" />
<span className="flex-grow text-left text-16-semibold-140">밈 사용상황</span>
<span className="flex h-24 w-24 items-center justify-center rounded-full hover:bg-gray-100">
<Icon
Expand Down
9 changes: 8 additions & 1 deletion src/pages/mypage/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { cloudinaryLoader } from "config/cloudinary-loader";
import Link from "next/link";
import { css } from "twin.macro";

Expand All @@ -19,7 +20,13 @@ const MyPage = () => {
<>
<MyPageNavigation />
<div className="flex flex-col items-center justify-center py-40 font-suit">
<Photo className="h-100 w-100 rounded-full" src={user.imageUrl} />
<Photo
alt={`${user.name || ""}의 프로필 이미지`}
className="h-100 w-100 rounded-full"
loader={cloudinaryLoader}
sizes="100px"
src={user.imageUrl}
/>
<span className="mt-4 text-22-bold-140">
{isLoading ? <Skeleton animation="wave" width={70} /> : user.name}
</span>
Expand Down
Loading

0 comments on commit 97c7ebf

Please sign in to comment.