
팀 프로젝트를 진행 중 위 화면을 구현 하던 중 Hydration failed because the initial UI does not match what was rendered on the server 이라는 에러가 발생했다고 한다. 말 그대로 SSR로 생성된 HTML의 UI와 클라이언트에서 생성된 HTML의 UI가 서로 차이가 생겨 발생하는 문제였다.
페이지를 작업하는 팀원분이 suppressHydrationWarning을 써서 당장 에러메시지는 제거할 수 있었지만, 근본적으로 문제를 해결했다고 보기 보다는 그냥 문제를 덮어 버리는 식의 방법이었고 다른 해결법이 필요하다고 말씀해주셨다.
<Image
src={imageUrl}
alt={"대표 이미지"}
fill={true}
className="object-cover"
sizes="(max-width: 768px) 100vw, (min-width: 768px) and (max-width: 1024px) 50vw, 280px"
suppressHydrationWarning={true}
/>;
우선 서버와 클라언트 간의 차이가 발생할 수 있는 부분을 분석해보자 맨위에 있는 사진을 보면 마감일 그리고 모임정보 관련 시간에서 데이터간의 차이가 발생할 수 있어 보인다.
{
teamId: '11',
id: 1466,
type: 'MINDFULNESS',
name: 'Heal with photo',
dateTime: '2024-10-18T01:00:00.000Z',
registrationEnd: '2024-10-18T01:00:00.000Z',
location: '을지로3가',
participantCount: 1,
capacity: 6,
image: '<https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/together-dallaem/1729210768960_000231480003.jpg>',
createdBy: 821,
canceledAt: null
},
위 데이터를 기반으로 렌더링 되고 있는 컴포넌트인데 이 컴포넌트들의 UTC 기반의 시간을 클라이언트에서 +9 시간을 더해 로컬시간으로 변환한다. 서버의 시간은 01:00 분인데 클라이언트의 시간은 +9 시인 10시로 렌더링이 되므로 이 부분에서 시간을 기반으로 하는 컴포넌트들의 차이가 발생한다.
그렇다면 서버에 데이터를 넘겨줄 때 즉 prefetch를 진행할 때 데이터를 변환해주면, 시간 차이가 해소되지 않을까 하는 생각이 들었고 아래 작성한 코드를 팀원 분께 변경을 요청드렸다.
export async function prefetchGatherings({
queryClient,
type,
date,
...
}: {
queryClient: QueryClient;
type?: GatheringType;
date?: Date;
...
}) {
await queryClient.prefetchInfiniteQuery<IGatherings[]>({
queryKey: ["gatherings", type, location, date, sortBy, sortOrder],
queryFn: ({ pageParam = 0 }) =>
getGatheringList({
pageParam: pageParam as number,
type,
date,
...
}),
});
}
위에서 작성된 코드에 우선 시간을 변환하는 함수들을 작성한다.
// 시간을 9시간 변환하는 코드
function convertTime(date: string | Date) {
return new Date(
new Date(date).toLocaleString("en-US", { timeZone: "Asia/Seoul" })
);
}
// 마감일 관련 시간 보정 코드
function getDeadline(registrationTime: Date, now: Date) {
if (registrationTime < now) return "마감된 모임";
const hoursDiff = differenceInHours(registrationTime, now);
if (hoursDiff < 24 && isToday(registrationTime))
return `오늘 ${registrationTime.getHours()}시 마감`;
if (hoursDiff < 24 && !isToday(registrationTime))
return `내일 ${registrationTime.getHours()}시 마감`;
const daysDiff = differenceInDays(registrationTime, now);
return `${daysDiff}일 후 마감`;
}
그리고 데이터 변환 함수를 prefetch를 할 때 적용해준다.
export async function prefetchGatherings({
queryClient,
type,
date,
...
}: {
queryClient: QueryClient;
type?: GatheringType;
date?: Date;
...
}) {
await queryClient.prefetchInfiniteQuery<IGatherings[]>({
queryKey: ["gatherings", type, location, date, sortBy, sortOrder],
queryFn: async ({ pageParam = 0 }) => {
const gatherings = await getGatheringList({
type,
date,
...
});
const transformedData = gatherings.map((gathering: IGatherings) => {
const currentTime = convertTime(new Date());
const registrationEndTime = convertTime(gathering.registrationEnd);
return {
...gathering,
dateTime: format(
convertTime(gathering.dateTime),
"yyyy-MM-dd HH:mm:ss",
{ locale: ko }
),
registrationEnd: format(registrationEndTime, "yyyy-MM-dd HH:mm:ss", {
locale: ko,
}),
isClosed: registrationEndTime < currentTime,
deadlineText: getDeadline(registrationEndTime, currentTime),
};
});
return transformedData;
},
});
}
이렇게 하는 과정을 통해서 데이터 간의 불일치를 해결할 수 있었다. 쉬운설명을 위해 코드에는 생략된 부분이 많아 온전하지 못하지만 핵심은 이렇다. prefetch 과정에서 데이터를 보정해 불일치를 해소한다.