๋ชจ์œผ์žก-SSR์„ ์ด์šฉํ•œ ์ธ์ฆ,์ธ๊ฐ€ ๋„์ž…

@choi2021 ยท December 17, 2022 ยท 25 min read

๐Ÿ”“ SSR์„ ์ด์šฉํ•œ ์ธ์ฆ/์ธ๊ฐ€ ๋„์ž…

์ด๋ฒˆ์— ํ”„๋กœ์ ํŠธ๋ฅผ ๊ณ ๋ฏผํ•˜๋ฉด์„œ ํ•ญ์ƒ ๋‹ต๋‹ตํ–ˆ๋˜ ๋ถ€๋ถ„์ด์—ˆ๋˜ ํŽ˜์ด์ง€ redirection๊ณผ ์ธ์ฆ ๊ณผ์ •์— ๋Œ€ํ•ด ๋” ๊นŠ์ด ๊ณต๋ถ€ํ–ˆ๋‹ค. CSR๋กœ ์ฒ˜๋ฆฌํ•˜๋˜ ์ธ์ฆ ๋ฐฉ์‹์„ SSR๋กœ ์ˆ˜์ •ํ•˜๊ธฐ๊นŒ์ง€ ๊ณผ์ •์„ ์ •๋ฆฌํ•ด ๋ณด๋ ค ํ•œ๋‹ค.

๐Ÿ˜… Firebase์˜ API๋ฅผ ์ด์šฉํ•œ User ์ƒํƒœ ๊ด€๋ฆฌ

๊ธฐ์กด Authentication์€ firebase Auth๋ฅผ ์ด์šฉํ•ด ๋ฐ›์•„์˜จ user์ •๋ณด์˜ token์„ localstorage์— ์ €์žฅํ•ด์„œ ํ™•์ธํ–ˆ๋‹ค. localstorage์— token์„ ์ €์žฅํ•˜๋Š” ๋ฐฉ์‹์€ ๋ณด์•ˆ์— ์ทจ์•ฝํ•˜๋‹ค๋Š” ์–˜๊ธฐ๋ฅผ ๋งŽ์ด ๋“ค์–ด, ์ƒˆ๋กœ์šด ๋ฐฉ๋ฒ•์„ ๊ณ ๋ฏผํ•˜๊ณ  ์žˆ์—ˆ๋‹ค. ๊ณ ๋ฏผ๊ณผ์ •์—์„œ firebase์˜ API๋กœ onAuthStateChanged๊ฐ€ ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ์•Œ๊ฒŒ ๋˜์—ˆ๊ณ , API๋ฅผ ์ด์šฉํ•œ๋‹ค๋ฉด ๋ณ„๋„์˜ ํ† ํฐ๊ณผ ์ฟ ํ‚ค๋ฅผ ์ง์ ‘ ์ €์žฅํ•˜์ง€ ์•Š์•„๋„ ๋  ๊ฒƒ์ด๋ž€ ์˜ˆ์ƒ์ด ๋˜์–ด ์ ์šฉํ–ˆ๋‹ค.

//AuthService.ts
export class AuthServiceImpl implements AuthService {
  googleProvider: GoogleAuthProvider;
  githubProvider: GithubAuthProvider;
  auth: Auth;

  constructor(private app: FirebaseApp) {
    this.googleProvider = new GoogleAuthProvider();
    this.githubProvider = new GithubAuthProvider();
    this.auth = getAuth(this.app);
  }
    ...
  onUserStateChanged(callback: Dispatch<SetStateAction<User | null>>) {
    return onAuthStateChanged(this.auth, callback);
  }
}

//AuthContext.tsx

type InitialValue = {
  authService: AuthService;
  user: User | null;
};

const AuthContext = createContext<InitialValue | null>(null);
export const AuthProvider = ({ children, authService }: AuthProviderProps) => {
  const [user, setUser] = useState<User | null>(null);
  useEffect(() => {
    authService.onUserStateChanged((user) => {
      setUser(user);
    });
  }, []);

  return (
    <AuthContext.Provider value={{ user, authService }}>
      {children}
    </AuthContext.Provider>
  );
};

export const useAuthService = () => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('Not under AuthProvider');
  }
  return context;
};

์ด๋ ‡๊ฒŒ ์ˆ˜์ •ํ•˜๊ณ  API๋ฅผ ์ด์šฉํ•ด login์ƒํƒœ๋ฅผ ํ™•์ธํ•ด์„œ ํŽ˜์ด์ง€ ์ด๋™์„ ํ•˜๋‹ค ๋ณด๋‹ˆ ๋‹ค๋ฅธ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ฒผ๋‹ค. ๋žœ๋”๋ง์„ ํ•˜๊ณ  API๋กœ user์ƒํƒœ๋ฅผ ๋ฐ”๊พธ๊ธฐ ์ „, ์ดˆ๊ธฐ ๊ฐ’์ด null๋กœ ๋˜์–ด์žˆ์–ด API ํ˜ธ์ถœ์‹œ user๊ฐ€ ์—†๋Š” ์ƒํƒœ๋กœ ํ˜ธ์ถœ๋˜์–ด ์—๋Ÿฌ๊ฐ€ ๋‚˜ํƒ€๋‚ฌ๋‹ค. ์šฐ์„  useJobs ํ›… ๋‚ด๋ถ€์— useEffect๋กœ user์ƒํƒœ๊ฐ€ ๋‹ฌ๋ผ์ง€๋ฉด refetchํ•ด ๋ฐ›์•„์˜ฌ ์ˆ˜ ์žˆ๊ฒŒ ํ•ด๊ฒฐํ–ˆ๋‹ค.

export const useSpecificJobs = () => {
  const { user } = useAuthService();
  const { query } = useRouter();
  const { id } = query;
  const dbService = useDBService();

  const getFilteredJobs = useQuery(
    [JOBS_KEY],
    () => {
      if (!user) {
        return {};
      }
      return dbService.getJobs(user);
    },
    {
      select: (data: ModifiedJobsType) => {
        if (!data) {
          return [];
        }
        return Object.values(data).filter((item) => item.id !== id);
      },
      onError: (error) => {
        console.log(error);
      },
    }
  );
  useEffect(() => {
    getFilteredJobs.refetch();
  }, [user]);
	...
};

์ฑ„์šฉ๊ณต๊ณ ๋“ค์„ ๋ฐ›์•„์˜ค๋Š” ๊ณผ์ •์ด ๋Š˜์–ด๋‚˜๊ฒŒ ๋˜์–ด ๊ฐ™์€ API๋ฅผ ๋‘๋ฒˆ ํ˜ธ์ถœํ•˜๋Š” ๊ณผ์ •์—์„œ ์‹œ๊ฐ„์ด ๋” ์˜ค๋ž˜ ๊ฑธ๋ฆฌ๊ฒŒ ๋˜์—ˆ๋‹ค.

[ํ˜ธ์ถœ ๊ณผ์ •]

  1. ๋ฉ”์ธ ํŽ˜์ด์ง€ ๋ Œ๋”๋ง
  2. APIํ˜ธ์ถœ
  3. User์ƒํƒœ ์—…๋ฐ์ดํŠธ
  4. API ์žฌํ˜ธ์ถœ

ํ•ด๊ฒฐ๋ฐฉ๋ฒ•์œผ๋กœ user๋ฅผ ๋จผ์ € ๋ฐ›๊ณ  API์ฝœ์„ ํ•˜๋Š” ๋ฐฉ๋ฒ•๊ณผ, auth๋ฅผ server-side๋กœ ๋จผ์ € ๋ฐ›์•„์˜ค๋Š” ๋ฐฉ๋ฒ• ๋‘๊ฐ€์ง€ ๋ฐฉ๋ฒ•์ด ๋– ์˜ฌ๋ž๋‹ค. ๊ทธ์ค‘ ๋จผ์ € ํ˜„์žฌ ์ง„ํ–‰ํ•˜๊ณ  ์žˆ๋Š” CSR์—์„œ ํ•ด๊ฒฐ๋ฐฉ๋ฒ•์„ ์ฐพ์•„๋ณด์•˜๋‹ค.

๐Ÿ’ป CSR์—์„œ์˜ ํ•ด๊ฒฐ๋ฒ•: Protected Route ๋„์ž…

CSR์—์„œ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” fetch๋กœ user๋ฅผ ๋ฐ›์•„์˜จ ํ›„์— api๋ฅผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์•ผ ํ–ˆ๋‹ค. ๊ฒ€์ƒ‰ํ•ด์„œ ์ƒˆ๋กญ๊ฒŒ ์•Œ๊ฒŒ ๋œ ๊ฒƒ์€ Protected Route๋ผ๋Š” HOC๋ฅผ ์ด์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์ด์—ˆ๋‹ค.

1) ๋กœ๊ทธ์ธ flow ์ˆ˜์ •ํ•˜๊ธฐ

๋จผ์ € ๊ฐ€์žฅ ์‹œ๊ธ‰ํ•œ ๋ฌธ์ œ๋Š” ๋กœ๊ทธ์ธ ํ›„์— ๋ฉ”์ธ ํŽ˜์ด์ง€ ์ด๋™์‹œ์— User๊ฐ€ ์—†๋Š” ์ƒํƒœ๋กœ API๊ฐ€ ํ˜ธ์ถœ๋œ๋‹ค๋Š” ์ ์ด์—ˆ๋‹ค. ์ด๊ฒƒ์„ ๋ง‰๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋จผ์ € user์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•ด ์ค„ ์ˆ˜ ์žˆ๋Š” component๊ฐ€ ํ•„์š”ํ–ˆ๊ณ , AuthStateChanged๋ผ๋Š” ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค์–ด ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋งˆ์šดํŠธ๋˜๋ฉด ๋จผ์ € User๋ฅผ ๋ฐ›์•„์˜ค๊ณ , ์ดํ›„์— ๋ฉ”์ธ ํŽ˜์ด์ง€๊ฐ€ ๋ Œ๋”๋ง ๋  ์ˆ˜ ์žˆ๊ฒŒ ํ–ˆ๋‹ค.

// AuthStateChanged.tsx

import React, { useEffect, useState } from 'react';
import { useAuthService } from '../context/AuthContext';

export default function AuthStateChanged({
  children,
}: {
  children: React.ReactNode;
}) {
  const { authService, setUser } = useAuthService();

  const [loading, setLoading] = useState(true);
  useEffect(() => {
    authService.onUserStateChanged((user) => {
      setUser(user);
      setLoading(false);
    });
  }, []);

  if (loading) {
    return <h1>๋กœ๋”ฉ์ค‘</>;
  }

  return <>{children}</>;
}

// _app.tsx

function MyApp({ Component, pageProps }: AppProps) {
    ...
  return (
    <>
      <QueryClientProvider client={queryClient}>
        <DBProvider dbService={dbService}>
          <AuthProvider authService={authService}>
            <AuthStateChanged>
              <ThemeProvider theme={theme}>
                <GlobalStyle />
                <Component {...pageProps} />
              </ThemeProvider>
            </AuthStateChanged>
          </AuthProvider>
        </DBProvider>
      </QueryClientProvider>
    </>
  );
}
export default MyApp;

User๊ฐ€ ์žˆ์–ด์•ผ๋งŒ ๋‹ค์Œ ์ปดํฌ๋„ŒํŠธ๋“ค๋กœ ๋„˜์–ด๊ฐ€ ๋™์ž‘ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ด์ „ ๊ฐ€์žฅ ํฐ ๋ฌธ์ œ์˜€๋˜ API๋ฅผ ์ •์ƒ์ ์œผ๋กœ ํ•œ๋ฒˆ๋งŒ ํ˜ธ์ถœํ•˜๊ฒŒ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

ํ•˜์ง€๋งŒ ์—ฌ์ „ํžˆ ์‚ด์ง ๋ฌธ์ œ๊ฐ€ ๋‚จ์•„์žˆ์—ˆ๋˜ ๊ฒƒ์€ AuthStateChanged์—์„œ User๋ฅผ ๋ฐ›์•„์˜ค๋Š” ๋™์•ˆ ํ™”๋ฉด์— ๋กœ๋”ฉ์„ ๋ณด์—ฌ์ค˜์•ผ ํ•œ๋‹ค๋Š” ์ ์ด์—ˆ๋‹ค. ๋กœ๋”ฉ์„ ์•ˆ ๋ณด์—ฌ์ฃผ๊ธฐ ์œ„ํ•ด loading์ด true์ผ ๋•Œ <></> ๋ฆฌ์•กํŠธ fragment๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค ํ•ด๋„ ์—ฌ์ „ํžˆ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๋™์•ˆ์˜ ๋นˆ ํŽ˜์ด์ง€๊ฐ€ ๋ณด์˜€๋‹ค.

[AuthStateChanged ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ถ”๊ฐ€ํ•œ ํ›„ ์ƒˆ๋กœ๊ณ ์นจํ•œ ๋ชจ์Šต]

์ƒˆ๋กœ๊ณ ์นจ

2) Protected Route

๋‘ ๋ฒˆ์งธ๋กœ ๋กœ๊ทธ์ธํ•˜์ง€ ์•Š๊ณ  ๋ฉ”์ธ ํŽ˜์ด์ง€์— ์ ‘์†ํ•˜๋Š” ๊ฒฝ์šฐ์™€ ๋กœ๊ทธ์ธ ํ›„์—๋„ ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€์— ์ ‘์†ํ•˜๋ ค๋Š” ๊ฒฝ์šฐ๋ฅผ ๋ง‰๊ธฐ ์œ„ํ•œ ๋ฆฌ๋‹ค์ด๋ ‰์…˜ ๋กœ์ง์ด ํ•„์š”ํ–ˆ๋‹ค. ์ด๊ฒƒ์„ ์œ„ํ•ด์„œ AuthStateChanged ์ปดํฌ๋„ŒํŠธ์™€ ์œ ์‚ฌํ•˜๊ฒŒ ์กฐ๊ฑด์— ๋”ฐ๋ผ ๋ Œ๋”๋ง์„ ํ•ด์ค„ ์ˆ˜ ์žˆ๋Š” ProtectedRoutes ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ถ”๊ฐ€ํ–ˆ๋‹ค. Protected Route๋Š” ํ•„์š”ํ•œ ํŽ˜์ด์ง€์— ๋งž๊ฒŒ ์‚ฌ์šฉ๋˜์–ด์•ผ ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— type์„ ์ •ํ•  ๋•Œ Generic์„ ์ด์šฉํ•ด ์ •ํ•ด ์ค„ ์ˆ˜ ์žˆ์—ˆ๋‹ค.

//ProtectRoute.tsx

import { useRouter } from "next/router"
import React, { ComponentType } from "react"
import { useAuthService } from "../context/AuthContext"
export function withPublic<T>(Component: ComponentType<T>) {
  return function WithPublic(props: T) {
    const auth = useAuthService()
    const router = useRouter()
    if (auth.user) {
      router.replace("/")
      return <></>
    }
    return <Component auth={auth} {...props} />
  }
}

export function withProtected<T>(Component: React.ComponentType<T>) {
  return function WithProtected(props: T) {
    const auth = useAuthService()
    const router = useRouter()
    if (!auth.user) {
      router.replace("/login")
      return <></>
    }
    return <Component auth={auth} {...props} />
  }
}

// pages/login.tsx
function Login() {
  // ...
}

export default withPublic(Login)

// Pages/register.tsx
function Register() {
  // ...
}

export default withPublic(Register)

// pages/index.tsx
function Home() {
  // ...
}

export default withProtected(Home)

// pages/job/[id].tsx

function Index() {
  // ...
}

export default withProtected(Index)

Protected Route๋ฅผ ์ด์šฉํ•ด ๋กœ๊ทธ์ธ์„ ํ•œ ํ›„์— home์—์„œ login์œผ๋กœ ์ด๋™ํ•˜๊ฑฐ๋‚˜ ๋กœ๊ทธ์ธ์„ ํ•˜์ง€ ์•Š๊ณ  login์—์„œ home์œผ๋กœ์˜ ์ด๋™์„ ๋ง‰์„ ์ˆ˜ ์žˆ์—ˆ๋‹ค. ํ•˜์ง€๋งŒ ์—ฌ์ „ํžˆ ์•ž์„œ ๋ฌธ์ œ๊ฐ€ ๋˜์—ˆ๋˜ ํŽ˜์ด์ง€ ์ด๋™์‹œ๊ฐ„๋™์•ˆ ๋กœ๋”ฉ์„ ๋ณด์—ฌ์ค˜์•ผ ํ•˜๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์—ˆ๋‹ค.

Protected Routes๋ฅผ ์•Œ๊ธฐ ์ „์— ํ•ด๊ฒฐ๋ฐฉ๋ฒ•์œผ๋กœ ๋– ์˜ฌ๋ ธ๋˜ ์„œ๋ฒ„์‚ฌ์ด๋“œ์—์„œ auth๋ฅผ ๋ฏธ๋ฆฌ ๋ฐ›์•„์™€ ๋ Œ๋”๋ง ์ „์— ์ฒดํฌํ•œ ํ›„์—, ํ•ด๋‹น ํŽ˜์ด์ง€์—์„œ auth๋ฅผ ๋ฐ”๋กœ ๋ฐ›์•„๋ณผ ์ˆ˜ ์žˆ๋‹ค๋ฉด ์ข€ ๋” ๋กœ์ง๋„ ๊ฐ„๋‹จํ•ด์ง€๊ณ , next๋ฅผ ์ž˜ ํ™œ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ๋˜์ง€ ์•Š์„๊นŒ๋ผ๋Š” ์ƒ๊ฐ์ด ๋“ค์–ด ๋‹ค์‹œ ์ฐพ์•„๋ณด๊ธฐ ์‹œ์ž‘ํ–ˆ๋‹ค.

๐Ÿ’พ Next-auth๋ฅผ ์ด์šฉํ•œ SSR๋กœ ์ „ํ™˜

SSR์„ ์ด์šฉํ•˜๊ธฐ ์œ„ํ•œ ๋ฐฉ๋ฒ•์„ ์ฐพ๊ธฐ์œ„ํ•ด Next JS ๊ณต์‹ ์‚ฌ์ดํŠธ๋ฅผ ์ฐพ์•„๋ณด๋‹ˆ, ์ œ์‹œ๋œ ๋ฐฉ๋ฒ•์œผ๋กœ with-iron-session๊ณผ next-auth๋ผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ด์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์ด์—ˆ๋‹ค. ๊ทธ์ค‘์—์„œ ๊ธฐ์กด ์ง€์›ํ–ˆ๋˜ OAuth๋ฅผ ์ด์šฉํ•œ ๋กœ๊ทธ์ธ ๋ฐฉ์‹์„ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” next-auth๋ฅผ ์„ ํƒํ–ˆ๋‹ค.

[next auth]

next auth
next auth

Next-auth๋Š” Next js์—์„œ ์‰ฝ๊ฒŒ ์ธ์ฆ๊ณผ์ •์„ ๊ตฌํ˜„ํ•ด ์ค„ ์ˆ˜ ์žˆ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ, API Routes๋ฅผ ์ด์šฉํ•ด ์„œ๋ฒ„๋‹จ์—์„œ session๊ณผ ํ† ํฐ์„ ๋งŒ๋“ค๊ณ  ํ™•์ธํ•ด client์—์„œ ํ•„์š”ํ–ˆ๋˜ ํ™•์ธ๋กœ์ง์„ ์ค„์—ฌ์ค„ ์ˆ˜ ์žˆ๋‹ค. Next Auth๋ฅผ ๊ณต๋ถ€ํ•˜๋ฉด์„œ ๊ต‰์žฅํžˆ ๋‹ค์–‘ํ•œ Provider๋“ค์„ OAuth๋กœ ์ œ๊ณตํ•ด์ค€๋‹ค๋Š” ์ ์ด ๋†€๋ผ์› ๋‹ค. ํŠนํžˆ, ์นด์นด์˜คํ†ก๊ณผ ๋„ค์ด๋ฒ„๋„ ์ง€์›ํ•ด์ค€๋‹ค๋Š” ์ ์ด ์‹ ๊ธฐํ–ˆ๊ณ , ๋‹ค์–‘ํ•œ DB์™€๋„ ์—ฐ๋™ํ•ด์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด ์ข‹์•˜๋‹ค.

SSR์„ Next-auth๋กœ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์‚ฌ์‹ค์„ ์•Œ๊ฒŒ ๋œ ํ›„์—, ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ž˜ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด ์ธ์ฆ, ์ธ๊ฐ€ ๊ณผ์ •๊ณผ ์•ž์„œ ์‚ฌ์šฉํ–ˆ๋˜ ๋ฐฉ์‹์ธ local storage์— ๋‘๋Š” ๋ฐฉ์‹์ด ์™œ ์•ˆ ์ข‹์€์ง€์— ๋Œ€ํ•ด ์ดํ•ดํ•˜๊ณ , Next-auth๊ฐ€ ํ•ด๊ฒฐ์ฑ…์ด ๋  ์ˆ˜ ์žˆ๋Š”์ง€ ๊ณ ๋ฏผํ–ˆ๋‹ค.

๐Ÿ˜– ํ† ํฐ์„ localStorage์— ๋‘๋ฉด ์•ˆ๋˜๋Š” ์ด์œ 

local storage๋Š” ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๋‹ซํ˜€๋„ ์œ ์ง€๋˜๊ธฐ ๋•Œ๋ฌธ์— ๋กœ๊ทธ์ธ์„ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ„ํŽธํ•œ ๋ฐฉ๋ฒ•์œผ๋กœ ์‚ฌ์šฉํ•ด์™”๋‹ค. ํ•˜์ง€๋งŒ ์ด๋Ÿฌํ•œ ๋ฐฉ์‹์€ XSS (Cross-Site Scripting) ๊ณต๊ฒฉ์— ์ทจ์•ฝํ•œ ๋‹จ์ ์„ ๊ฐ€์ง„๋‹ค.

XSS (Cross-site scripting)

XSS ๊ณต๊ฒฉ์€ ์›น์‚ฌ์ดํŠธ์— ์•…์„ฑ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ฃผ์ž…ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์ฃผ๋กœ ํŽ˜์ด์ง€์˜ input์ด๋‚˜ form์„ ์ด์šฉํ•ด ๊ณต๊ฒฉํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค. ์ฃผ์ž…ํ•ด๋†“์€ ์‚ฌ์ดํŠธ์— ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธํ•˜๊ฒŒ ๋  ๊ฒฝ์šฐ์— ์‚ฌ์šฉ์ž์˜ ํ† ํฐ, ์ฟ ํ‚ค ๋“ฑ์˜ ์ •๋ณด๋ฅผ ๋นผ๋‚ผ ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค. ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋กœ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋Š” localstorage๋‚˜ ๋ณ„๋„์˜ ์˜ต์…˜์ด ์—†๋Š” ์ฟ ํ‚ค์— ํ† ํฐ์„ ์ €์žฅํ•˜๊ฒŒ ๋  ๊ฒฝ์šฐ์—๋Š” XSS ๊ณต๊ฒฉ์— ์ทจ์•ฝํ•˜๊ฒŒ ๋œ๋‹ค.

์ด๊ฒƒ์„ ๋ง‰๊ธฐ ์œ„ํ•ด์„œ๋Š” in-memory์— ์ €์žฅํ•˜๋Š” ๋ฐฉ์‹๊ณผ http only ์ฟ ํ‚ค๋ฅผ ์ด์šฉํ•˜๋Š” ๋ฐฉ์‹์ด ์žˆ๋‹ค. in-memory์— ์ €์žฅํ•  ๊ฒฝ์šฐ์—๋Š” ๊ด€๋ฆฌ๋Š” ์‰ฝ์ง€๋งŒ ๋กœ๊ทธ์ธ์„ ์œ ์ง€ํ•  ์ˆ˜๊ฐ€ ์—†๋Š” ๋‹จ์ ์ด ์žˆ์–ด, ๋ณดํ†ต http only ์ฟ ํ‚ค๋กœ ์„œ๋ฒ„์—์„œ ํ† ํฐ์„ ๋ณด๋‚ด์ฃผ๋ฉด ๋ธŒ๋ผ์šฐ์ €์— ์ €์žฅํ•˜๊ณ  ์ž๋™์œผ๋กœ ์š”์ฒญ์‹œ ๋‹ด์•„์„œ ๋ณด๋‚ด๋Š” ๋ฐฉ์‹์„ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ํ•˜์ง€๋งŒ http only ์ฟ ํ‚ค๋กœ ํ† ํฐ์„ ์ €์žฅํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ CSRF ๊ณต๊ฒฉ์— ์•ฝํ•œ ๋‹จ์ ์ด ์กด์žฌํ•œ๋‹ค.

CSRF(Cross-site Request Forgery)

CSRF๊ณต๊ฒฉ์€ ์‚ฌ์šฉ์ž๊ฐ€ ์›ํ•˜์ง€ ์•Š์€ action์„ ํ•˜๊ฒŒํ•˜๋Š” ํ•ดํ‚น ๋ฐฉ๋ฒ•์œผ๋กœ, ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž์˜ ์ •๋ณด๋ฅผ ์ด์šฉํ•ด ์‚ฌ์šฉ์ž ๋ชฐ๋ž˜ ๋ธŒ๋ผ์šฐ์ €์— ์ €์žฅ๋˜์–ด์žˆ๋Š” ์ฟ ํ‚ค๋ฅผ ์ด์šฉํ•ด ์š”์ฒญ์„ ๋ณด๋‚ด๋Š” ๋ฐฉ์‹์ด๋‹ค. http only๋กœ ๋œ ์ฟ ํ‚ค์ด๊ธฐ ๋•Œ๋ฌธ์— ํ•ดํ‚น๋ฒ”์ด ์ฟ ํ‚ค๋ฅผ ์ง์ ‘ ๋นผ์˜ฌ ์ˆ˜๋Š” ์—†์ง€๋งŒ, ๋Œ€์‹  ์ž์‹ ์ด ์›ํ•˜๋Š” ์š”์ฒญ์— ์‚ฌ์šฉ์ž์˜ ํ† ํฐ์„ ์ด์šฉํ•ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๋ ‡๊ธฐ http only ์ฟ ํ‚ค๋งŒ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ์ถ”๊ฐ€์ ์ธ ์ธ์ฆ๋ฐฉ์‹์ด ํ•„์š”ํ•œ๋ฐ, ์ด๋•Œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์ด CSRF ํ† ํฐ์ด๋‹ค.

CSRF ํ† ํฐ์€ ์„œ๋ฒ„์—์„œ ๋ฐœ๊ธ‰ํ•˜๋Š” ์ธ์ฆ์šฉ ํ† ํฐ์œผ๋กœ, ํด๋ผ์ด์–ธํŠธ๋Š” ๋ฐ›์€ ํ† ํฐ์„ in-memory์— ์ €์žฅํ•ด ๋‘๊ณ  ์ดํ›„ ์š”์ฒญ์— ํ† ํฐ์„ ํ•จ๊ป˜ ๋ณด๋‚ธ๋‹ค. ๋งŒ์•ฝ์— ํ•ดํ‚น๋ฒ”์ด CSRF๊ณต๊ฒฉ์„ ํ•œ ํ›„์— ์‚ฌ์šฉ์ž์˜ ์ฟ ํ‚ค๋ฅผ ์ด์šฉํ•ด ์š”์ฒญ์„ ๋ณด๋‚ด๋”๋ผ๋„, ์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘๋ณด๋‚ด๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ๋ฉด ํ† ํฐ์ด ์—†๊ธฐ ๋•Œ๋ฌธ์— ์„œ๋ฒ„๊ฐ€ ํ™•์ธํ•  ์ˆ˜ ์žˆ๊ฒŒํ•ด ๊ณต๊ฒฉ์„ ๋ง‰์„ ์ˆ˜ ์žˆ๋‹ค.

๊ทธ๋Ÿฌ๋ฉด Next-auth๋ฅผ ์ด์šฉํ•œ ์ธ์ฆ๋ฐฉ์‹์€ ์–ด๋–ป๊ฒŒ ์ด๋ฃจ์–ด์ง€๊ณ  ์žˆ์„๊นŒ?

Next-auth์˜ ๋ณด์•ˆ ๋ฐฉ์‹๊ณผ ์ ์šฉ

Next-auth๋Š” OAuth์™€ JWT๋ฅผ ์ง€์›ํ•˜๊ณ , ๋ฌด์—‡๋ณด๋‹ค ๊ณ ๋ฏผํ–ˆ๋˜ ๋ณด์•ˆ ๋ฌธ์ œ๋ฅผ ํ•œ๋ฒˆ์— ํ•ด๊ฒฐํ•ด ์ค„ ์ˆ˜ ์žˆ์—ˆ๋‹ค. CSRFํ† ํฐ์„ POST ์š”์ฒญ์— ์ ์šฉํ•˜๊ณ , ์ธ์ฆ์„ ์œ„ํ•œ ํ† ํฐ๊ณผ ์„ธ์…˜๋“ค์„ server-readable-only cookie๋กœ ์ž๋™์œผ๋กœ ๊ด€๋ฆฌํ•ด์ค€๋‹ค. ๋‘๊ฐ€์ง€ ๋•๋ถ„์— ์•ž์„œ ์•Œ์•„๋ณด์•˜๋˜ XSS๊ณต๊ฒฉ๊ณผ CSRF ๊ณต๊ฒฉ์„ ๋ฐฉ์–ดํ•˜๋ฉด์„œ ์ธ์ฆ์„ ํŽธํ•˜๊ฒŒ ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ํ™•์ธํ–ˆ๋‹ค.

next-auth
next-auth

Next auth์— ๋Œ€ํ•ด ๊ณต๋ถ€ํ•˜๋ฉด์„œ ๊ธฐ๋ณธ์ ์œผ๋กœ password๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๋ฐฉํ–ฅ์„ ์ง€ํ–ฅํ•œ๋‹ค๋Š” ๊ฒƒ์„ ์•Œ๊ฒŒ ๋˜์—ˆ๋‹ค. ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ๋ณดํ†ต ํ•˜๋‚˜์˜ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์—ฌ๋Ÿฌ ๊ณณ์— ์‚ฌ์šฉ๋˜๊ธฐ ๋•Œ๋ฌธ์— ๋ณด์•ˆ์— ๋ฏผ๊ฐํ•œ ์‚ฌํ•ญ์ด๋ผ๊ณ  ์ƒ๊ฐํ•ด, ๊ธฐ์กด ํšŒ์›๊ฐ€์ž…-๋กœ๊ทธ์ธ ๋กœ์ง ๋Œ€์‹ ์— "์ด๋ฉ”์ผ ์ธ์ฆ"์œผ๋กœ ๊ต์ฒดํ•˜๊ณ , OAuth๋กœ "๊ตฌ๊ธ€"๊ณผ "Github"๋ฅผ ์ด์šฉํ•œ ์ด ์„ธ๊ฐ€์ง€ ๋ฐฉ์‹์˜ ์ธ์ฆ๊ณผ์ •์„ ์ง„ํ–‰ํ•˜๊ธฐ๋กœ ํ–ˆ๋‹ค.

OAuth๋ฅผ ์ด์šฉํ•  ๋•Œ๋„ ๊ณ ๋ คํ•ด์•ผํ•  ์ ์€, ๋‹ค๋ฅธ ํ”Œ๋žซํผ์ด์ง€๋งŒ ๊ฐ™์€ email์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ์ด๋‹ค. ๊ณ ๋ฏผ์„ ํ•ด๋ณด์•˜์„ ๋•Œ ๋‹ค๋ฅธ ํ”Œ๋žซํผ์ด์ง€๋งŒ ๊ฐ™์€ email๋กœ ์ ‘์†ํ–ˆ์„ ๋•Œ ๊ฐ™์€ ์ฑ„์šฉ๊ณต๊ณ ๋“ค์„ ๋ณด์—ฌ์ค˜๋„ ๊ดœ์ฐฎ์„ ๊ฒƒ ๊ฐ™์•„ ์ด๋ถ€๋ถ„์€ provider ์˜ต์…˜์— allowDangerousEmailAccountLinking์„ ์ถ”๊ฐ€ํ•ด์„œ ์ฒ˜๋ฆฌํ–ˆ๋‹ค.

์‚ฌ์šฉ ๋ฐฉ๋ฒ•

Next-auth๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ์šฐ์„  pages/api/auth/[...nextauth].ts๋ฅผ ๋งŒ๋“ค์–ด์•ผํ•œ๋‹ค. ๋‚ด๊ฐ€ ์‚ฌ์šฉํ•œ [...nextauth].tsํŒŒ์ผ ๋‚ด์šฉ์€ ์•„๋ž˜์™€ ๊ฐ™๊ณ , ์ฝ”๋“œ์˜ ์ดํ•ด๋ฅผ ๋•๊ธฐ ์œ„ํ•ด์„œ ๊ณตํ†ต ๋ถ€๋ถ„์„ ์„ค๋ช…ํ•˜๊ณ  ๊ฐ๊ฐ์˜ provider๋“ค์˜ ์—ฐ๊ฒฐ์„ ์ •๋ฆฌํ•ด๋ณด๋ ค ํ•œ๋‹ค.

import GoogleProvider from "next-auth/providers/google"
import GithubProvider from "next-auth/providers/github"
import EmailProvider from "next-auth/providers/email"
import NextAuth from "next-auth"
import { PrismaAdapter } from "@next-auth/prisma-adapter"
import prisma from "../../../prisma/prisma"
import nodemailer from "nodemailer"
import { html, text } from "../../../src/utils/emailFormat"

export default NextAuth({
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_ID || "",
      clientSecret: process.env.GOOGLE_SECRET || "",
      allowDangerousEmailAccountLinking: true,
    }),
    GithubProvider({
      clientId: process.env.GITHUB_ID || "",
      clientSecret: process.env.GITHUB_SECRET || "",
      allowDangerousEmailAccountLinking: true,
    }),
    EmailProvider({
      server: process.env.EMAIL_SERVER,
      from: process.env.EMAIL_FROM,
      async sendVerificationRequest({
        identifier: email,
        url,
        provider: { server, from },
      }) {
        const { host } = new URL(url)
        const transport = nodemailer.createTransport(server)
        await transport.sendMail({
          to: email,
          from,
          subject: `Sign in to ${host}`,
          text: text({ url, host }),
          html: html({ url, host, email }),
        })
      },
    }),
  ],

  session: {
    strategy: "jwt",
  },
  adapter: PrismaAdapter(prisma),
  pages: {
    signIn: "/login",
  },
  secret: process.env.JWT_SECRET,
  debug: true,
  callbacks: {
    async session({ session, token, user }) {
      session.user.id = user.id
      return session
    },
  },
})

๊ณตํ†ต ๋ถ€๋ถ„

  • secret: ๋ฐฐํฌ ๋•Œ, JWT๋ฅผ ๋งŒ๋“œ๋Š”๋ฐ ์‚ฌ์šฉ๋  secret์œผ๋กœ ํ„ฐ๋ฏธ๋„์— openssl rand -base64 32 ์„ ์ž…๋ ฅํ•ด ๋งŒ๋“ค์–ด์ง„ ํ‚ค๋ฅผ env์— ์ถ”๊ฐ€ํ•ด ์—ฐ๊ฒฐํ–ˆ๋‹ค.
  • debug: ์—ฐ๊ฒฐ๋œ provider์˜ ํ† ํฐ ๋‚ด์šฉ์ด๋‚˜ ์—๋Ÿฌ๋“ค์„ ๋ณผ ์ˆ˜ ์žˆ๋Š” ์˜ต์…˜์œผ๋กœ ํ„ฐ๋ฏธ๋„๋กœ ๋‚ด์šฉ๋“ค์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.
  • callbacks: ์ธ์ฆ๊ณผ์ •์—์„œ ํ•„์š”ํ•œ ๋‚ด์šฉ๋“ค์„ ์ปค์Šคํ…€ํ•˜๋Š” ๋ถ€๋ถ„์œผ๋กœ API์— ํ•„์š”ํ•œ id๋ฅผ ๊ฐ’์„ ๊ธฐ๋ณธ์ ์œผ๋กœ session/user์— ๋‹ด๊ฒจ์žˆ์ง€ ์•Š์•„ ์ถ”๊ฐ€ํ•ด์ฃผ์—ˆ๋‹ค.
  • pages: custom ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋ฅผ ์—ฐ๊ฒฐํ•  url์„ ์ „๋‹ฌํ•ด ์ค„ ์ˆ˜ ์žˆ๋‹ค.
  • session: database์™€ ์—ฐ๊ฒฐํ•˜๊ฒŒ ๋˜๋ฉด ์‚ฌ์šฉ์ž ์ •๋ณด๊ฐ€ ์„ธ์…˜์œผ๋กœ ๊ด€๋ฆฌ๋˜๊ธฐ ๋•Œ๋ฌธ์— jwt๋กœ ๋ณ€๊ฒฝํ–ˆ๋‹ค. database๋ฅผ ์—ฐ๊ฒฐํ•˜์ง€ ์•Š์œผ๋ฉด ๊ธฐ๋ณธ์ ์œผ๋กœ jwt๊ฐ€ ๋œ๋‹ค.

callbacks์—์„œ ์ถ”๊ฐ€ํ•  ๋ถ€๋ถ„์€ ํƒ€์ž…๋„ ์ถ”๊ฐ€ํ•ด์ค˜์•ผํ•˜๊ธฐ ๋•Œ๋ฌธ์— next-auth.d.ts์—์„œ ์ •์˜ํ•ด์ฃผ์—ˆ๋‹ค.

// types/next-auth.d.ts

import { DefaultUser } from "next-auth"
declare module "next-auth" {
  interface Session {
    user: DefaultUser & {
      id: string
    }
  }
}

Adapter

์‚ฌ์šฉ์ž์ •๋ณด๋“ค์„ ์ €์žฅํ•  DB๋ฅผ ์—ฐ๊ฒฐํ•˜๋Š” ๋ถ€๋ถ„์œผ๋กœ, next-auth๋Š” ๋‹ค์–‘ํ•œ DB๋ฅผ ์ง€์›ํ•œ๋‹ค. ๊ทธ์ค‘์—์„œ ๊ธฐ์กด์— ์‚ฌ์šฉํ•˜๋˜ firebase ๋Œ€์‹ ์— prisma๋ฅผ ์ด์šฉํ•ด ์—ฐ๊ฒฐํ–ˆ๋‹ค. Firebase๋ฅผ ์‚ฌ์šฉํ•˜๋ คํ–ˆ์ง€๋งŒ ํ˜„์žฌ next-auth๊ฐ€ version 4๋กœ ์—…๋ฐ์ดํŠธ ํ•˜๋ฉด์„œ ์ง€์›ํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์„ ํ™•์ธํ–ˆ๋‹ค.

๊ทธ๋ž˜์„œ ์ƒˆ๋กœ์šด Database๋ฅผ ์ฐพ๋‹ค๊ฐ€ prisma๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค. Prisma๋Š” ORM์œผ๋กœ SQL DB์™€ ํ•จ๊ป˜ ์‚ฌ์šฉ๋˜์ง€๋งŒ ํ˜„์žฌ MongoDB๊นŒ์ง€ ์ง€์›ํ•ด์ค˜, ๋ฐ์ดํ„ฐ์˜ schema๋ฅผ ๊ธฐ์ž…ํ•ด์„œ ์•ˆ์ „ํ•˜๊ฒŒ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๊ณ , ๋‹ค์–‘ํ•œ database๋ฅผ ์ง€์›ํ•˜๋Š” ์žฅ์ ๋“ค๋กœ ์„ ํƒํ–ˆ๋‹ค.

Prisma๋ฅผ ์ด์šฉํ•œ DB ์—ฐ๊ฒฐ

prisma๋ฅผ ์ด์šฉํ•ด MongoDB๋ฅผ ์—ฐ๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ next-auth์˜ ๊ณต์‹ ํ™ˆํŽ˜์ด์ง€๋ฅผ ์ฐธ๊ณ ํ•˜๋ฉด ๋˜์ง€๋งŒ ์ฃผ์˜ํ•  ๋ถ€๋ถ„์ด ์žˆ์—ˆ๋‹ค.

๊ณต์‹ ํ™ˆํŽ˜์ด์ง€์˜ schema์˜ VerificationToken ๋ถ€๋ถ„์˜ id๊ฐ€ ์—†๊ธฐ ๋•Œ๋ฌธ์— ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š”๋ฐ, ์ด๊ฒƒ์„ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ถ”๊ฐ€ํ•ด ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

// ...

model VerificationToken {
  id         String   @id @default(auto()) @map("_id") @db.ObjectId
  identifier String
  token      String   @unique
  expires    DateTime @map("expiresAt")

  @@unique([identifier, token])
  @@map("verification_tokens")
}

OAuth

OAuth๋กœ ์‚ฌ์šฉํ•œ Google๊ณผ Github์— ์—ฐ๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๊ฐ๊ฐ ID์™€ Secret์„ ์ „๋‹ฌํ•ด์ฃผ์–ด์•ผํ•œ๋‹ค. ์ด๋•Œ ๋งŽ์ด ์—๋Ÿฌ๊ฐ€ ๋‚ฌ๋˜ ๋ถ€๋ถ„์€ ์ธ์ฆ์ด ๋๋‚˜๊ณ  ๋Œ์•„๊ฐˆ callback์„ ์„ค์ •ํ•ด์ฃผ๋Š” ๋ถ€๋ถ„์ด์—ˆ๋‹ค. ๋ฐฐํฌ๋ฅผ ํ•˜๋ฉด์„œ ์—๋Ÿฌ๊ฐ€ ๋งŽ์ด ๋‚œ ๋ถ€๋ถ„์ด๋ผ ๋ฏธ๋ฆฌ ๋กœ์ปฌ๊ณผ ๋ฐฐํฌ์šฉ ๋‘๊ฐœ์˜ OAuth ์ธ์ฆ ์ •๋ณด๋ฅผ ๋งŒ๋“ค๊ณ , ๊ฐ๊ฐ ๊ตฌ๋ถ„๋œ key์™€ secret์œผ๋กœ .env.local๊ณผ .env.production์œผ๋กœ ํ•ด๋‘์—ˆ๋‹ค๋ฉด ํ—ท๊ฐˆ๋ฆฌ์ง€ ์•Š์•˜์„ํ…๋ฐ๋ผ๋Š” ์•„์‰ฌ์›€์ด ๋“ ๋‹ค.

Google

Google์˜ ๊ฒฝ์šฐ๋Š” GCP(Google Cloud Platform)์— ๋“ค์–ด๊ฐ€ ์‚ฌ์šฉ์ž ์ธ์ฆ์ •๋ณด- OAuth Client ID๋ฅผ ๋งŒ๋“ค๋ฉด ๋˜๋Š”๋ฐ ์ด๋•Œ ์ฃผ์˜ํ•  ์ ์€ url์ฃผ์†Œ๋ฅผ ์ž˜ ์ ์–ด์ฃผ์–ด์•ผํ–ˆ๋‹ค.

  • ๊ฐœ๋ฐœ: url=http://localhost:3000, ์Šน์ธ๋œ ๋ฆฌ๋””๋ ‰์…˜ URI= http://localhost:3000/api/auth/callback/google
  • ๋ฐฐํฌ: url= http://moejob.shop, ์Šน์ธ๋œ ๋ฆฌ๋””๋ ‰์…˜ URI= http://moejob.shop/api/auth/callback/google

Github

Github์€ Github ๋ณธ์ธ๊ณ„์ •์˜ settings-์™ผ์ชฝ ๋ฉ”๋‰ด ๊ฐ€์žฅํ•˜๋‹จ์˜ developer settings-OAuth Apps๋ฅผ ์ด์šฉํ•ด ์ถ”๊ฐ€ํ•˜๋ฉด ๋œ๋‹ค. ์ด๋•Œ๋„ url์ฃผ์†Œ๋ฅผ ์ฃผ์˜ํ•ด์„œ ์ ์–ด์•ผํ–ˆ๋‹ค. ํŠนํžˆ Github์€ ํ•œ๋ฒˆ ์ธ์ฆ๋œ ๊ณ„์ •์˜ url์„ ๋ฐ”๊พธ์–ด๋„ ์ž˜ ์ ์šฉ์ด ์•ˆ๋˜ ์ƒˆ๋กญ๊ฒŒ ๋ฐฐํฌ์šฉ ๊ณ„์ •์„ ๋งŒ๋“ค์–ด์„œ ์ธ์ฆํ–ˆ๋‹ค.

  • ๊ฐœ๋ฐœ: Homepage URL=http://localhost:3000, Authorization callback URL= http://localhost:3000/api/auth/callback/github
  • ๋ฐฐํฌ: Homepage URL=http://moejob.shop, Authorization callback URL= http://moejob.shop/api/auth/callback/github

Email

์ด๋ฉ”์ผ์„ ์—ฐ๊ฒฐํ•˜๋Š” ๋ถ€๋ถ„์ด ๊ฐ€์žฅ ํž˜๋“ค์—ˆ๋˜ ๋ถ€๋ถ„์ค‘ ํ•˜๋‚˜๋‹ค. Next-auth์™€ ๊ฒ€์ƒ‰์˜ ๋Œ€๋ถ€๋ถ„์€ sendGrid์˜ SMTP๋ฅผ ์ด์šฉํ•ด ์ด๋ฉ”์ผ์„ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๊ฒŒ ์†Œ๊ฐœํ–ˆ์ง€๋งŒ, ๊ฐ€์ž…์„ ํ•ด๋„ ๊ณ„์ •์ด pending์ƒํƒœ๋กœ ๋‚จ์•„์žˆ์–ด์„œ Gmail์„ ์ด์šฉํ•œ ๋ฐฉ๋ฒ•์œผ๋กœ ๋ฐ”๊พธ์—ˆ๋‹ค.

์šฐ์„  next-auth์ž์ฒด์— nodemailer๊ฐ€ ์—†๊ธฐ ๋•Œ๋ฌธ์— nodemailer๋ฅผ ์„ค์น˜ํ•œ ํ›„์— ์—ฐ๊ฒฐํ•ด์ฃผ์—ˆ๋‹ค. GmailํŽ˜์ด์ง€์—์„œ ์„ค์ •์— ๋“ค์–ด๊ฐ€ IMAP์„ ์‚ฌ์šฉ์œผ๋กœ ์ˆ˜์ •ํ•˜๊ณ , google๊ณ„์ •์˜ ์•ฑ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ถ”๊ฐ€ํ•ด ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ์ „๋‹ฌํ–ˆ๋‹ค.

  • EMAIL_SERVER =smtp://<email>:<app password>@smtp.gmail.com:587
  • EMAIL_FROM=moejob@gmail.com

์ด๋ ‡๊ฒŒ ์„ค์ •๋งŒ ํ•˜๋ฉด ๊ธฐ๋ณธ ์ด๋ฉ”์ผ ํฌ๋งท์œผ๋กœ ๋‚˜์˜ค๊ธฐ ๋•Œ๋ฌธ์— ์ถ”๊ฐ€์ ์ธ ํฌ๋งท์„ ์œ„ํ•ด EmailProvider option์œผ๋กœ sendVerificationRequest๋กœ ํ•„์š”ํ•œ ๋‚ด์šฉ๋“ค์„ ์ „๋‹ฌ ํ•  ์ˆ˜ ์žˆ๋‹ค.

_app.tsx

๊ฐ๊ฐ์˜ Provider๋ฅผ ์—ฐ๊ฒฐํ•œ ํ›„์— ์ปดํฌ๋„ŒํŠธ์— ์ ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” next-auth๊ฐ€ ์ œ๊ณตํ•˜๋Š” SessionProvider์„ ๊ฐ์‹ธ์ฃผ๋ฉด ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์—์„œ useSession hook์„ ์ด์šฉํ•ด session์ •๋ณด๋ฅผ ์ „์—ญ ์ƒํƒœ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ๋•๋ถ„์— ๊ธฐ์กด์— CSR์—์„œ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋˜ AuthService๋ฅผ ์ด์šฉํ•ด user๋ฅผ ๋”ฐ๋กœ ๊ด€๋ฆฌํ•˜์ง€ ์•Š์•„๋„ ๋˜์—ˆ๋‹ค.

function MyApp({ Component, pageProps }: AppProps) {
  const dbService = new DBServiceImpl(firebaseApp)
  return (
    <>
      <QueryClientProvider client={queryClient}>
        <DBProvider dbService={dbService}>
          <SessionProvider basePath={process.env.NEXTAUTH_URL}>
            ...
          </SessionProvider>
        </DBProvider>
      </QueryClientProvider>
    </>
  )
}

Proteceted Route

getServerSideProps๋กœ session์„ ๋ฐ›์•„์™€ ๋กœ๊ทธ์ธ ๋œ ์‚ฌ์šฉ์ž์ธ์ง€ ๋จผ์ € ์ฒดํฌํ•ด ์ฃผ๋Š” ๋ฐฉ์‹์„ ํ†ตํ•ด ํ˜„์žฌ ๋กœ๊ทธ์ธ๋œ ์œ ์ €์ธ์ง€ ๋จผ์ € ํ™•์ธํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค. ๋จผ์ € ์„œ๋ฒ„์—์„œ ์‚ฌ์šฉ์ž์ •๋ณด๋ฅผ ์ด์šฉํ•ด ๋ฆฌ๋‹ค์ด๋ ‰์…˜ ์‹œํ‚ค๊ธฐ ๋•Œ๋ฌธ์— ํŽ˜์ด์ง€๋ฅผ ๋žœ๋”๋ง์„ ํ•˜์ง€ ์•Š๊ณ , ๋ฆฌ๋‹ค์ด๋ ‰์…˜์ด ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.

export default function User({
  session,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
  return (
    <MainLayout>
      <JobSection session={session} />
    </MainLayout>
  )
}

export const getServerSideProps = async (context: NextPageContext) => {
  const session = await getSession(context)
  if (!session) {
    return {
      redirect: {
        destination: "/login",
      },
    }
  }
  return {
    props: { session },
  }
}

Custom Login

Next auth๋ฅผ ์ด์šฉํ•˜๊ฒŒ ๋˜๋ฉด ๊ธฐ๋ณธ์ ์œผ๋กœ ์ œ๊ณตํ•ด ์ฃผ๋Š” login ํŽ˜์ด์ง€๊ฐ€ ์žˆ์ง€๋งŒ ์ด๋ฏธ ๋งŒ๋“ค์–ด๋‘” ํŽ˜์ด์ง€๊ฐ€ ์žˆ์—ˆ๊ธฐ ๋•Œ๋ฌธ์— custom ํŽ˜์ด์ง€์™€ ์—ฐ๊ฒฐํ–ˆ๋‹ค. custom ํŽ˜์ด์ง€์—์„œ provider๋ฅผ ์ด์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ์„œ๋ฒ„๋กœ ํ•ด๋‹น provider์ •๋ณด๋ฅผ ์ „๋‹ฌํ•ด ์ฃผ์–ด์•ผ ํ•œ๋‹ค.

const Login = ({
  providers,
}: InferGetServerSidePropsType<typeof getServerSideProps>) => {
  return (
    <>
      <SEO title="๋กœ๊ทธ์ธ" />
      <AuthLayout providers={providers} />;
    </>
  )
}

export default Login

export const getServerSideProps = async ({ req }: NextPageContext) => {
  const session = await getSession({ req })
  if (session) {
    return {
      props: {},
      redirect: {
        destination: "/",
      },
    }
  }

  return {
    props: {
      providers: await getProviders(),
    },
  }
}

๐Ÿ‘“์„ฑ๋Šฅ ๋น„๊ต

ํฌ๋กฌ์˜ ๊ฐœ๋ฐœ์ž๋„๊ตฌ๋ฅผ ์ด์šฉํ•ด ์™„๋ฃŒ (ํŽ˜์ด์ง€๋ฅผ ๋ชจ๋‘ ๋ถˆ๋Ÿฌ์˜ค๋Š”๋ฐ ๊นŒ์ง€ ๊ฑธ๋ฆฌ๋Š” ์‹œ๊ฐ„)๋ฅผ ๊ธฐ์ค€์œผ๋กœ CSR์„ ์ด์šฉํ•ด ์ธ์ฆ์„ ํ–ˆ์„๋•Œ ์‹œ๊ฐ„๊ณผ SSR์„ ์ด์šฉํ•ด ์ธ์ฆ์„ ํ–ˆ์„ ๋•Œ ์‹œ๊ฐ„์„ ๋น„๊ตํ•ด๋ณด์•˜๋‹ค.

์„ฑ๋Šฅ๋น„๊ต
์„ฑ๋Šฅ๋น„๊ต

์œ„ ์‚ฌ์ง„๊ณผ ๊ฐ™์ด CSR์€ ์ด 1.52s, SSR์€ 955ms๋กœ SSR์ด CSR์— ๋น„ํ•ด 38%์ •๋„ ๋กœ๋”ฉ์†๋„๋ฅผ ๊ฐœ์„ ๋œ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ์—ˆ๋‹ค.

๋งˆ์น˜๋ฉฐ

์ด๋ ‡๊ฒŒ ์„œ๋ฒ„์‚ฌ์ด๋“œ ๋ Œ๋”๋ง์„ ์ด์šฉํ•ด์„œ ์ธ์ฆ์„ ์ ์šฉํ•˜๋ฉด์„œ ์ „์ฒด์ ์œผ๋กœ ์ปดํฌ๋„ŒํŠธ๋“ค์ด ๊ฐ„๋‹จํ•ด์กŒ๊ณ , ๋นˆํŽ˜์ด์ง€๋ฅผ ๋ณด์—ฌ์ฃผ์ง€ ์•Š๊ณ  ํŽ˜์ด์ง€ ์ด๋™์ด ์ด๋ฃจ์–ด์ ธ ์„ฑ๋Šฅ ๊ฐœ์„ ๋„ ์ด๋ฃจ์–ด์ง„ ์ ์ด ๋„ˆ๋ฌด ์ข‹์•˜๋‹ค. ์ดํ›„์—๋Š” ํŽ˜์ด์ง€๋งˆ๋‹ค ๊ถŒํ•œ์„ ์ •ํ•˜๊ณ  ํŽ˜์ด์ง€ ๊ฐœ์„ ์„ ํ•œ ํ›„์— SEO์™€ HTTPS๋ฅผ ์ ์šฉํ•˜๋Š” ์ž‘์—…์„ ํ•  ์˜ˆ์ •์ด๋‹ค.


[์ฐธ์กฐ]

@choi2021
๋งค์ผ์˜ ์‹œํ–‰์ฐฉ์˜ค๋ฅผ ๊ธฐ๋กํ•˜๋Š” ๊ฐœ๋ฐœ์ผ์ง€์ž…๋‹ˆ๋‹ค.