개발/NEXTJS
사용자 이미지가 새로고침을 할 때 이전의 이미지가 보였다가 다시 되돌아 가는 현상 해결
LeeHyoGeun
2025. 3. 2. 03:41
이미지를 변경하고 새로고침을 했을 때 이전 이미지로 됐다가 다시 되돌아 가는 현상을 발견했다.
이 현상을 해결하기 위해 찾아본 결과
이미지 캐싱에 관한 문제라는 것을 알게 되었고
그 일이 일어나는 이유는 이미지의 주소가 항상 같기 때문인 것을 알았다.
export async function updateAvatar(
file: File,
userId: string
): Promise<updateAvatarState> {
try {
const supabase = await createClient();
const cookieStore = await cookies();
const token = cookieStore.get("token")?.value;
if (!token) {
throw new Error("인증되지 않은 접근입니다.");
}
const extension = file.type.split("/")[1];
const fileName = `avatar.${extension}`; // 또는 원본 파일명 사용
// 1. Storage에 이미지 업로드
const { data: uploadData, error: uploadError } = await supabase.storage
.from(`${process.env.NEXT_PUBLIC_STORAGE_BUCKET}`)
.upload(`${userId}/${fileName}`, file, {
upsert: true,
contentType: file.type,
});
if (uploadError) {
console.error(uploadError);
return {
success: false,
error: new Error("프로필 이미지 업데이트에 실패했습니다."),
};
}
// 2. 이미지 URL 가져오기
const {
data: { publicUrl },
} = supabase.storage
.from(`${process.env.NEXT_PUBLIC_STORAGE_BUCKET}`)
.getPublicUrl(uploadData.path);
console.log(publicUrl);
if (!publicUrl) {
console.error("이미지 URL 가져오기에 실패했습니다.");
return {
success: false,
error: new Error("프로필 이미지 업데이트에 실패했습니다."),
};
}
// 3. 프로필 업데이트
const response = await fetch(`${API_URL}/user/image`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
user: {
image: publicUrl,
},
}),
});
const responseData = await response.json();
if (!response.ok) {
console.error(responseData.error);
const message =
translateError(responseData.error) ||
"프로필 이미지 업데이트에 실패했습니다.";
throw new Error(message);
}
await setAuthToken(responseData.user.token);
return {
success: true,
value: { publicUrl: publicUrl },
};
} catch (error) {
console.error(error);
throw new Error("프로필 이미지 업데이트에 실패했습니다.");
}
}
이 현상을 해결하기 위해
"use client";
import { useState, useEffect } from "react";
import { CurrentUserType } from "@/types/authTypes";
import Avatar, { TimestampAvatarSize } from "./Avatar";
interface TimestampAvatarProps {
user: Pick<CurrentUserType, "image" | "username"> | null | undefined;
size?: TimestampAvatarSize;
className?: string;
}
export function TimestampAvatar({
user,
size = "md",
className = "",
}: TimestampAvatarProps) {
const [timestamp, setTimestamp] = useState("");
useEffect(() => {
setTimestamp(Date.now().toString());
}, [user?.image]);
return (
<Avatar
username={user?.username || ""}
image={user?.image}
size={size}
className={className}
timestamp={timestamp}
/>
);
}
"use client";
import Image from "next/image";
import { useEffect, useState } from "react";
export const TIMESTAMP_AVATAR_SIZE = {
sm: "w-6 h-6",
md: "w-8 h-8",
lg: "w-12 h-12",
xxxxl: "w-32 h-32",
} as const;
export type TimestampAvatarSize = keyof typeof TIMESTAMP_AVATAR_SIZE;
interface AvatarProps {
username: string;
image?: string | null;
size?: TimestampAvatarSize;
className?: string;
timestamp?: string;
}
const Avatar = ({
username,
image,
size = "md",
className = "",
timestamp,
}: AvatarProps) => {
const [imgError, setImgError] = useState(false);
useEffect(() => {
setImgError(false);
}, [image, timestamp]);
const sizeClasses = {
sm: "w-6 h-6",
md: "w-8 h-8",
lg: "w-12 h-12",
xl: "w-16 h-16",
xxl: "w-20 h-20",
xxxl: "w-24 h-24",
xxxxl: "w-32 h-32",
};
const handleError = () => {
setImgError(true);
};
const defaultImage = `https://ui-avatars.com/api/?name=${username}&format=png`;
const imageUrl =
imgError || !image
? defaultImage
: image.startsWith("data:image")
? image
: `${image}?timestamp=${timestamp}`;
return (
<Image
src={imageUrl}
alt={`${username}'s avatar`}
width={24}
height={24}
className={`rounded-full object-cover ${sizeClasses[size]} ${className}`}
onError={handleError}
quality={75}
/>
);
};
export default Avatar;
이렇게 time stamp를 넣는 방식으로 개선을 꾀하였고
실패하고 말았다
또한 nextjs image의 캐싱을 꺼버리는 방법도 있었지만 그건 의미없다고 판단하여 안하였다.
그래서 해결 방법을 생각한 결과 그냥 이미지의 url을
이미지를 바꿀 때마다 변하게 하면 된다는 것이었다.
const fileExt = file.name.split(".").pop();
const hash = uuidv4();
const fileName = `${hash}.${fileExt}`;
// 1. Storage에 이미지 업로드
const { data: uploadData, error: uploadError } = await supabase.storage
.from(`${process.env.NEXT_PUBLIC_STORAGE_BUCKET}`)
.upload(`${userId}/${fileName}`, file, {
upsert: true,
contentType: file.type,
});
이렇게 수정 완료
"use client";
import { CurrentUserType } from "@/types/authTypes";
import Image from "next/image";
import { useState } from "react";
export const AVATAR_SIZE = {
sm: "w-6 h-6",
md: "w-8 h-8",
lg: "w-12 h-12",
xxxxl: "w-32 h-32",
} as const;
export type AvatarSize = keyof typeof AVATAR_SIZE;
interface AvatarProps {
user: Pick<CurrentUserType, "image" | "username"> | null | undefined;
size?: AvatarSize;
className?: string;
}
const Avatar = ({ user, size = "md", className = "" }: AvatarProps) => {
const [imgError, setImgError] = useState(false);
const sizeClasses = {
sm: "w-6 h-6",
md: "w-8 h-8",
lg: "w-12 h-12",
xl: "w-16 h-16",
xxl: "w-20 h-20",
xxxl: "w-24 h-24",
xxxxl: "w-32 h-32",
};
const handleError = () => {
setImgError(true);
};
const { username, image } = user || {};
const defaultImage = `https://ui-avatars.com/api/?name=${username}&format=png`;
const imageUrl = imgError || !image ? defaultImage : image;
return (
<Image
src={imageUrl}
alt={`${username}'s avatar`}
width={24}
height={24}
className={`rounded-full object-cover ${sizeClasses[size]} ${className}`}
onError={handleError}
quality={75}
/>
);
};
export default Avatar;
복잡했던 컴포넌트도 이렇게 간소화 할 수 있었다.