본문 바로가기

TIL

TIL - 서버 사이드 렌더링으로 화면 깜빡임 제거

문제 설명

React 애플리케이션에서 서버 사이드 렌더링(SSR)을 사용하지 않을 때, 즉 next.js를 사용하기 전에는 초기 페이지 로드 시 데이터가 없어서 화면이 깜빡이는 현상이 발생할 수 있습니다. 예를 들어, 로그인된 사용자의 정보를 표시할 때, 초기 렌더링에서 데이터가 없으면 "없습니다"라고 표시되었다가, 데이터가 로드되면 "사용자 이메일"로 변경됩니다. 이러한 화면 깜빡임은 사용자 경험을 저하시킬 수 있습니다.

 

해결 방법

이 문제를 해결하기 위해, 서버 사이드에서 초기 데이터를 가져와 클라이언트로 전달하고, 이를 통해 초기 렌더링 시에 데이터를 사용하여 화면 깜빡임을 방지할 수 있습니다. 이를 위해 Next.js의 서버 사이드 렌더링 기능과 컨텍스트 API를 사용합니다.

 

 

구현 단계

1. AuthContext 설정

먼저, 사용자 인증 정보를 관리할 AuthContext를 설정합니다. 여기서는 useAuth 훅과 AuthProvider 컴포넌트를 정의합니다.

"use client";

import { User } from "@supabase/supabase-js";
import {
  createContext,
  PropsWithChildren,
  useContext,
  useEffect,
  useState,
} from "react";

type AuthContextValue = {
  isInitialized: boolean;
  isLoggedIn: boolean;
  me: User | null;
  logIn: (email: string, password: string) => void;
  logOut: () => void;
  signUp: (email: string, password: string) => void;
};

const initialValue: AuthContextValue = {
  isInitialized: false,
  isLoggedIn: false,
  me: null,
  logIn: () => {},
  logOut: () => {},
  signUp: () => {},
};

const AuthContext = createContext<AuthContextValue>(initialValue);

export const useAuth = () => useContext(AuthContext);

interface AuthProviderProps {
  initialIsInitialized: boolean;
  initialMe: User | null;
}

export function AuthProvider({
  initialIsInitialized,
  initialMe,
  children,
}: PropsWithChildren<AuthProviderProps>) {
  const [isInitialized, setIsInitialized] =
    useState<AuthContextValue["isInitialized"]>(initialIsInitialized);
  const [me, setMe] = useState<AuthContextValue["me"]>(initialMe);
  const isLoggedIn = !!me;

  const logIn: AuthContextValue["logIn"] = async (email, password) => {
    if (me) return alert("이미 로그인 되어 있어요");
    if (!email || !password) return alert("이메일, 비밀번호 모두 채워 주세요.");

    const data = { email, password };
    const response = await fetch("http://localhost:3000/api/auth/log-in", {
      method: "POST",
      body: JSON.stringify(data),
    });
    const user = await response.json();

    setMe(user);
  };

  const logOut = async () => {
    if (!me) return alert("로그인하고 눌러주세요");

    await fetch("http://localhost:3000/api/auth/log-out", { method: "DELETE" });

    setMe(null);
  };

  const signUp: AuthContextValue["signUp"] = async (email, password) => {
    if (me) return alert("이미 로그인 되어 있어요");
    if (!email || !password) return alert("이메일, 비밀번호 모두 채워 주세요.");

    const data = { email, password };
    const response = await fetch("http://localhost:3000/api/auth/sign-up", {
      method: "POST",
      body: JSON.stringify(data),
    });
    const user = await response.json();

    setMe(user);
  };

  useEffect(() => {
    fetch("http://localhost:3000/api/auth/me").then(async (response) => {
      if (response.status === 200) {
        const {
          data: { user },
        } = await response.json();
        setMe(user);
      }

      setIsInitialized(true);
    });
  }, []);

  const value: AuthContextValue = {
    isInitialized,
    isLoggedIn,
    me,
    logIn,
    logOut,
    signUp,
  };

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

 

2. 서버 사이드에서 데이터 가져오기

ProviderLayout 컴포넌트를 통해 서버 사이드에서 초기 데이터를 가져옵니다.

import { AuthProvider } from "@/contexts/auth.context/auth.context";
import { createClient } from "@/supabase/server";
import React from "react";

async function ProvidersLayout({ children }: { children: React.ReactNode }) {
  const supabase = createClient();
  const {
    data: { user },
  } = await supabase.auth.getUser();

  const isInitialized = true;
  const me = user || null;

  return (
    <AuthProvider initialIsInitialized={isInitialized} initialMe={me}>
      {children}
    </AuthProvider>
  );
}

export default ProvidersLayout;

 

3. 홈 페이지 컴포넌트

홈 페이지 컴포넌트에서 useAuth 훅을 사용하여 인증 상태를 관리합니다.

"use client";

import { useAuth } from "@/contexts/auth.context/auth.context";
import { useState } from "react";

export default function Home() {
  const { me, logIn, logOut, signUp } = useAuth();
  const [email, setEmail] = useState<string>("");
  const [password, setPassword] = useState<string>("");

  const handleClickLogIn = async () => logIn(email, password);
  const handleClickLogOut = async () => logOut();
  const handleClickSignUp = async () => signUp(email, password);

  return (
    <main className="h-screen flex flex-col items-center justify-center">
      <h1>현재 로그인한 유저는</h1>
      {me ? me.email + "입니다" : "없습니다"}

      <hr className="my-10 w-full border-black" />
      <input
        type="text"
        className="input"
        placeholder="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <input
        type="text"
        className="input"
        placeholder="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />

      <button className="button" onClick={handleClickLogIn}>
        로그인하기
      </button>
      <hr className="my-10 w-full border-black" />
      <button className="button" onClick={handleClickLogOut}>
        로그아웃하기
      </button>

      <hr className="my-10 w-full border-black" />

      <input
        type="text"
        className="input"
        placeholder="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <input
        type="text"
        className="input"
        placeholder="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />

      <button className="button mt-3" onClick={handleClickSignUp}>
        회원가입하기
      </button>
    </main>
  );
}

 

결론

이렇게 서버 사이드 렌더링을 통해 초기 데이터를 미리 가져와서 화면 깜빡임 현상을 제거할 수 있습니다. 이를 통해 사용자 경험을 개선할 수 있습니다.