nextjs에 대한 이해도가 낮아 최적화도 안되었고
아무것도 제대로 안된 상태였다
사용자 이미지가 새로고침을 할 때 이전의 이미지가 보였다가 다시 되돌아 가는 현상 해결
이미지를 변경하고 새로고침을 했을 때 이전 이미지로 됐다가 다시 되돌아 가는 현상을 발견했다.이 현상을 해결하기 위해 찾아본 결과이미지 캐싱에 관한 문제라는 것을 알게 되었고그 일이
lim-2.tistory.com
이미지 새로고침 시 깜빡거리는 문제 해결
프로필 이미지가 선명하게 나오지 않는 에러 해결 그리고 하이드레이션 에러 해결
"use client";import { CurrentUserType } from "@/types/authTypes";import Image from "next/image";import { useState } from "react";export const AVATAR_SIZE = { sm: { class: "w-6 h-6", size: 24 }, md: { class: "w-8 h-8", size: 32 }, lg: { class: "w-12 h-12",
lim-2.tistory.com
최적화와 하이드레이션 에러 해결
---------------------------
import type { Metadata } from "next";
import "./globals.css";
import Header from "@/components/ui/Header/Header";
import { Roboto } from "next/font/google";
import getCurrentUserServer from "@/utils/supabase/getCurrentUserServer";
import { SWRConfig } from "swr";
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
const roboto = Roboto({
weight: ["400", "500", "700"], // 필요한 폰트 굵기 선택
subsets: ["latin"], // 사용할 문자 세트
display: "swap", // 폰트 로딩 전략
variable: "--font-roboto",
});
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const user = await getCurrentUserServer(["image", "username"]);
return (
<html lang="kr" className={roboto.variable}>
<body>
<SWRConfig value={{ fallback: { "/api/user": user } }}>
<Header />
{children}
</SWRConfig>
</body>
</html>
);
}
import HeaderClient from "./HeaderClient";
const Header = async () => {
return (
<header>
<HeaderClient />
</header>
);
};
export default Header;
"use client";
import Link from "next/link";
import { NavLinks } from "./NavLinks";
import useSWR from "swr";
import { getCurrentUserClient } from "@/utils/supabase/getCurrentUserClient";
export default function HeaderClient() {
const { data: user } = useSWR("/api/user", () =>
getCurrentUserClient(["image", "username"])
);
return (
<>
<section className="hidden md:block sticky top-0 bg-white dark:bg-gray-700 shadow-sm z-10">
<nav className="container mx-auto px-4">
<div className="flex items-center justify-between h-16">
<Link className="text-brand-primary text-xl font-bold" href="/">
conduit
</Link>
<ul className="flex items-center space-x-6">
<NavLinks user={user!} />
</ul>
</div>
</nav>
</section>
<section
id="mobile-header"
className="md:hidden fixed bottom-0 left-0 right-0 z-50 transition-transform duration-200 ease-in-out"
>
<div className="absolute inset-0 bg-white dark:bg-gray-800 shadow-[0_-1px_3px_rgba(0,0,0,0.1)] dark:shadow-[0_-1px_3px_rgba(0,0,0,0.3)]" />
<nav className="relative container mx-auto px-4">
<ul className="flex items-center justify-around h-16">
<NavLinks user={user!} isMobile={true} />
</ul>
</nav>
</section>
</>
);
}
"use client";
import NavLink from "@/components/NavLink";
import {
EditIcon,
HomeIcon,
LoginIcon,
RegisterIcon,
SettingsIcon,
} from "./icons";
import Avatar from "@/components/Avatar";
import { User } from "@/types/authTypes";
interface NavLinksProps {
user: Pick<User, "image" | "username"> | null;
isMobile?: boolean;
}
export const NavLinks = ({ isMobile, user }: NavLinksProps) => {
const isLoggedIn = !!user;
const timestamp = user?.image ? Date.now().toString() : "";
return (
<>
<li className="md:translate-y-[1px]">{/* <DarkModeToggle /> */}</li>
<li>
<NavLink href="/" isMobile={isMobile} end>
{isMobile && <HomeIcon />}
<span>Home</span>
</NavLink>
</li>
{isLoggedIn ? (
<>
<li>
<NavLink href="/editor" isMobile={isMobile}>
{isMobile && <EditIcon />}
<span>New Article</span>
</NavLink>
</li>
<li>
<NavLink href="/settings" isMobile={isMobile}>
{isMobile && <SettingsIcon />}
<span>Settings</span>
</NavLink>
</li>
<li>
<NavLink
href={`/profile/${user?.username}`}
isMobile={isMobile}
classes="md:flex md:gap-1 md:translate-y-[1px]"
>
<Avatar
username={user?.username || ""}
image={user?.image}
size={isMobile ? "sm" : "md"}
className={isMobile ? "" : "mr-1"}
timestamp={timestamp}
/>
<span className="translate-y-[1px] lg:translate-y-[2px]">
{user?.username}
</span>
</NavLink>
</li>
</>
) : (
<>
<li>
<NavLink href="/login" isMobile={isMobile}>
{isMobile && <LoginIcon />}
<span>Sign in</span>
</NavLink>
</li>
<li>
<NavLink href="/register" isMobile={isMobile}>
{isMobile && <RegisterIcon />}
<span>Sign up</span>
</NavLink>
</li>
</>
)}
</>
);
};
"use server";
import getCurrentUserServer from "@/utils/supabase/getCurrentUserServer";
import { createClient } from "@/utils/supabase/server";
export async function updateAvatar(file: File) {
const supabase = await createClient();
const user = await getCurrentUserServer(["id"]);
try {
// 1. Storage에 이미지 업로드
const { data: uploadData, error: uploadError } = await supabase.storage
.from("realworldAvtImage")
.upload(`${user?.id}/avatar.jpg`, file, {
upsert: true,
});
if (uploadError) throw uploadError;
// 2. 이미지 URL 가져오기
const {
data: { publicUrl },
} = supabase.storage
.from("realworldAvtImage")
.getPublicUrl(uploadData.path);
// 3. 프로필 업데이트
const { error: updateError } = await supabase
.from("users")
.update({ image: publicUrl })
.eq("id", user?.id);
if (updateError) throw updateError;
return publicUrl;
} catch (error) {
console.error("Error updating avatar:", error);
throw error;
}
}
"use client";
import { updateAvatar } from "@/actions/storage";
import Crop from "@/components/crop/Crop";
import { Button } from "@/components/ui/Button/Button";
import Modal from "@/components/ui/Modal";
import { useAvatar } from "@/context/avatar/AvatarContext";
import { User } from "@/types/authTypes";
import { useRouter } from "next/navigation";
import { mutate } from "swr";
const fileToBase64 = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result as string);
reader.onerror = (error) => reject(error);
});
};
export default function AvatarModalPage() {
const router = useRouter();
const { imageData, setCroppedImage, croppedImage } = useAvatar();
const handleSave = async () => {
try {
if (croppedImage) {
const previousData = await mutate("/api/user");
const base64Image = await fileToBase64(croppedImage);
console.log("base64Image", base64Image);
await mutate(
"/api/user",
async (currentData: Pick<User, "image" | "username"> | undefined) => {
try {
const publicUrl = await updateAvatar(croppedImage);
if (!currentData?.username) {
throw new Error("Username is required");
}
return { ...currentData, image: publicUrl };
} catch (error) {
console.error("Error updating avatar:", error);
return previousData;
}
},
{
optimisticData: (currentData) => ({
...currentData,
image: base64Image,
}),
revalidate: true,
}
);
}
router.back();
} catch (error) {
console.error("Error updating avatar:", error);
}
};
return (
<Modal>
<Modal.Header>Update Profile Picture</Modal.Header>
<Modal.Content className="w-full relative">
{imageData && (
<>
<Crop imageSrc={imageData} setCroppedImage={setCroppedImage} />
</>
)}
</Modal.Content>
<Modal.Footer>
<button onClick={() => router.back()} className=" text-gray-600">
Cancel
</button>
<Button onClick={handleSave}>Save</Button>
</Modal.Footer>
</Modal>
);
}
/setting page.tsx에서 사용자의 이미지를 업데이트 하는 기능을 추가하는 아주 간단하다고 생각하는 작업을 하는데
계속해서 벽에 부딪혔었다
원하는 동작은 사용자 이미지 수정을 하면 header에 있는 정보도 수정이 되야 하는데 안되서 엄청 고생했다.
안됐던 이유 1.
header를 root layout에 넣었었는데 layout의 캐싱은 페이지와 다르게 동작했었다
이유는 nextjs는 캐싱을 활발히 활용하는데 layout은 페이지 전환시에도 영향을 안받는 patial lendering이란걸 한다고 한다
그래서 이곳의 header를 넣었더니 안바뀌었던 것이다. 그래서 사용자의 정보에 따라 rendering에 영향을 받는 컴포넌트에 'use client'선언을 하여 해결하였다
안됐던 이유 2.
Image from 'next/image' 의 최적화
storage를 supabase를 사용하는데 경로가
storage에 사용자 id에 해당하는 폴더를 만들어서 이용을 하는데
그 때문에 새로운 이미지를 업로드해도 항상 경로가 같아
Image에 전달되는 src가 항상 값이 같아 같은 이미지만 보여주는 것이었다.
그래서 방법이
1. unoptimized 옵션을 넣어 최적화를 포기함과 동시에 캐싱을 없애 버리는 것
2. url에 쿼리 스트링을 넣어 변경을 알리는 것
"use client";
import Image from "next/image";
import { useEffect, useState } from "react";
interface AvatarProps {
username: string;
image?: string | null;
size?: "sm" | "md" | "lg";
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",
};
const handleError = () => {
setImgError(true);
};
const defaultImage = `https://ui-avatars.com/api/?name=${encodeURIComponent(
username
)}`;
const imageUrl =
imgError || !image
? defaultImage
: image.startsWith("data:image")
? image // base64는 그대로 사용
: `${image}?timestamp=${timestamp}`; // URL은 timestamp 추가
console.log("imageUrl", imageUrl);
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;
등등 있었는데
Image 최적화를 포기하고 싶지 않아 쿼리스크링을 넣는 방법으로 선택하였다
안됐던 이유 3.
realativePath를 쓰고 싶지 않았다
안됐던 이유라기 보다는 고생했던 이유이긴 한데
강의에서 배운 건
웬만하면 서버에서 가져온 값으로 렌더링 하는 게 좋다, (맞긴 하다)
서버에서 가져온걸 초기화 해서 새로운 값으로 띄우기 위해서는 relativePath를 사용하면 된다 였는데
아무리 생각해도 개인 user의 정보를 변경했다고 nextjs 안에 있는 캐싱을 모두 지워버리는게 너무 손해라고 생각했기에 다른 방법을 찾아 헤메었다
그래서 공식 홈페이지와 유튜브 강의 여러 글들을 보며 개념을 다졌는데
ssr의 동적 렌더링을 사용하면서 client에서 fetch를 담당하는 것은 swr을 이용하자였다
swr을 사용한 이유는 다른 글에 남겼다
그래서 결과적으로
초기 렌더링은 root rayout 설정한 SWR config의 초기값으로(server value) 렌더링 하고
이후의 변화는 client에서 값을 가져오고 또 사용자의 경험을 만족 시키기 위해
mutate를 활용하여 낙관적 업데이트를 구현하였다.
또 다른 작은 문제
SWR도 캐싱을 하여 사용자가 새로고침을 눌렀을 때
이미지가 캐싱됐던 이미지로 바뀌었다가 다시 현재의 이미지로 돌아왔는데
해당 문제는 사용자의 경험에 큰 영향을 끼치지 않아 그냥 두었다
배운점
1. swr 사용 방법
2. supabase에서 왜 createClient를 server와 client를 해서 줬는지
3. layout 캐싱 특징
4. ssr의 동적 렌더링이 어떻게 작동되는지
- 서버에서 모든 걸 생성하지 않고 특정 부분은 client 측에서 동적으로 변경할 수 있도록 하는 방식으로 캐싱 시스템이 이 부분은 작동하지 않아 realativePath를 사용하지 않고 싶어하는 나에게 딱 맞았다
5.Image의 최적화
6. SWR의 캐싱
7. client 컴포넌트와 server 컴포넌트 차이
8. server action 사용법
더 수정해야 할 점
에러 처리
'개발 > NEXTJS' 카테고리의 다른 글
error.tsx 사용 (0) | 2025.02.10 |
---|---|
nextjs 에러 관리 (0) | 2025.02.05 |
좋은 블로그 글 모음 (1) | 2025.01.27 |
nextjs에서 swr과 react query 비교 (0) | 2025.01.27 |
병렬라우팅과 일반 컴포넌트 방식 비교 (1) | 2025.01.22 |