๋ชจ์œผ์žก-firebase๋ฅผ ์ด์šฉํ•œ ํšŒ์›๊ฐ€์ž… ๋กœ์ง ๊ตฌํ˜„

@choi2021 ยท November 25, 2022 ยท 8 min read

๐Ÿ”‘ ํšŒ์›๊ฐ€์ž…/๋กœ๊ทธ์ธ ํŽ˜์ด์ง€

ํšŒ์›๊ฐ€์ž…๊ณผ ๋กœ๊ทธ์ธ์„ ํ†ตํ•ด์„œ ๊ฐœ์ธ ๋ณ„๋กœ ์ฑ„์šฉ๊ณต๊ณ ๋ฅผ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด์„œ ํšŒ์›๊ฐ€์ž…/๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“ค์—ˆ๋‹ค. ํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“ค๋ฉด์„œ ๊ณ ๋ฏผํ–ˆ๋˜ ๊ณผ์ •์„ ์ •๋ฆฌํ•ด๋ณด์•˜๋‹ค.

๐ŸŽจ ๋””์ž์ธ

๋””์ž์ธ์€ ์›ํ‹ฐ๋“œ์˜ ๋กœ๊ทธ์ธ/ํšŒ์›๊ฐ€์ž… ํŽ˜์ด์ง€๋ฅผ ์ฐธ๊ณ ํ–ˆ๋Š”๋ฐ ์ปจํ…์ธ ๋ฅผ ๋ชจ๋ฐ”์ผ ์‚ฌ์ด์ฆˆ์ธ 400px๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๋งŒ๋“ค์–ด ๋ชจ๋ฐ”์ผ๊ณผ ๋ฐ์Šคํฌ ํƒ‘์—์„œ ๋™์ผํ•˜๊ฒŒ ๋ณด์ผ ์ˆ˜ ์žˆ๊ฒŒ ํ–ˆ๋‹ค. ์ด๊ฒƒ์„ ์ฐธ๊ณ ํ•ด ๋˜‘๊ฐ™์ด 400px๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๋””์ž์ธํ–ˆ๊ณ , OAuth๋ฅผ ์—ฐ๊ฒฐํ•ด ๊ฐ„ํŽธํ•˜๊ฒŒ ์ด์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด๋‹น ํ”Œ๋žซํผ๋“ค์„ ์•„๋ž˜์— ์ถ”๊ฐ€ํ–ˆ๋‹ค.

[์›ํ‹ฐ๋“œ์˜ ํšŒ์›๊ฐ€์ž…/๋กœ๊ทธ์ธ ํŽ˜์ด์ง€]

์›ํ‹ฐ๋“œ์˜ ํšŒ์›๊ฐ€์ž…/๋กœ๊ทธ์ธ ํŽ˜์ด์ง€
์›ํ‹ฐ๋“œ์˜ ํšŒ์›๊ฐ€์ž…/๋กœ๊ทธ์ธ ํŽ˜์ด์ง€

[๋ชจ์œผ์žก์˜ ํšŒ์›๊ฐ€์ž…/๋กœ๊ทธ์ธ ํŽ˜์ด์ง€]

๋ชจ์œผ์žก ํšŒ์›๊ฐ€์ž…/๋กœ๊ทธ์ธ ํŽ˜์ด์ง€
๋ชจ์œผ์žก ํšŒ์›๊ฐ€์ž…/๋กœ๊ทธ์ธ ํŽ˜์ด์ง€

๐Ÿ’ŠFirebase์˜ Auth ๊ธฐ๋Šฅ

๋กœ๊ทธ์ธ/ํšŒ์›๊ฐ€์ž…์„ ์•„์ง ์ง์ ‘ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” node js์— ๋Œ€ํ•œ ๊ณต๋ถ€๊ฐ€ ๋” ํ•„์š”ํ•ด, ์šฐ์„ ์€ firebase๋กœ ๊ตฌํ˜„ํ–ˆ๋‹ค. firebase ๋ฌธ์„œ๋ฅผ ์ฐธ๊ณ ํ•ด์„œ email/password, google, github ์„ธ ๊ฐ€์ง€ ๋ฐฉ์‹์„ ์ด์šฉํ•ด ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ–ˆ๋‹ค.

AuthService ๋ชจ๋“ˆ ์ œ์ž‘

๋จผ์ € firebase๋ฅผ ์ดํ›„์— database์—๋„ ์ด์šฉํ•  ์˜ˆ์ •์ด๊ธฐ ๋•Œ๋ฌธ์—, ๊ณตํ†ต์œผ๋กœ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ๊ฒŒ _app.tsx์—์„œ firebaseApp์„ ๋งŒ๋“ค๊ณ  ๊ฐ๊ฐ์˜ ๋ชจ๋“ˆ์— ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ–ˆ๋‹ค.

const config: ConfigType = {
  apiKey: process.env.NEXT_PUBLIC_API_KEY || "",
  authDomain: process.env.NEXT_PUBLIC_AUTH_DOMAIN || "",
  projectId: process.env.NEXT_PUBLIC_PROJECT_ID || "",
  storageBucket: process.env.NEXT_PUBLIC_STORAGE_BUCKET || "",
  appId: process.env.NEXT_PUBLIC_APP_ID || "",
  measurementId: process.env.NEXT_PUBLIC_MEASUREMENT_ID || "",
}

function MyApp({ Component, pageProps }: AppProps) {
  const app = initializeApp(config)
  const authService = new AuthServiceImpl(app)
  const { push } = useRouter()

  return (
    <>
      <AuthProvider authService={authService}>
        <Component {...pageProps} />
      </AuthProvider>
    </>
  )
}
export default MyApp

ํšŒ์›๊ฐ€์ž…/๋กœ๊ทธ์ธ ๋กœ์ง์„ ๋‹ด๋Š” ๋ชจ๋“ˆ์ธ Authservice๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด interface์™€ interface๋ฅผ ์‹คํ–‰ํ•˜๋Š” AuthserviceImpl class๋ฅผ ๋งŒ๋“ค์—ˆ๋‹ค.

/Authtypes.ts
export interface AuthService {
  signIn: (email: string, password: string) => Promise<UserCredential>;
  signUp: (email: string, password: string) => Promise<UserCredential>;
  OAuthSignIn: (platfrom: OAuthType) => Promise<UserCredential>;
  signOut: () => Promise<void>;
}

//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);
  }
  signIn(email: string, password: string) {
    return signInWithEmailAndPassword(this.auth, email, password);
  }

  signUp(email: string, password: string) {
    return createUserWithEmailAndPassword(this.auth, email, password);
  }

  OAuthSignIn(platform: OAuthType): Promise<UserCredential> {
    const provider = this[`${platform}Provider`];
    return signInWithPopup(this.auth, provider);
  }

  signOut() {
    return signOut(this.auth);
  }
}

SignIn๊ณผ SignUp

ํšŒ์›๊ฐ€์ž…/๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ ๋‚ด AuthService๊ฐ€ ์‚ฌ์šฉ๋  ๊ณณ์€ AuthForm ์ปดํฌ๋„ŒํŠธ๋กœ ํšŒ์›๊ฐ€์ž…๊ณผ ๋กœ๊ทธ์ธ ๋ชจ๋‘์—์„œ ๋™์ผํ•œ UI๋ฅผ ์‚ฌ์šฉ๋˜๊ฒŒ ํ–ˆ๋‹ค. ํ•œ๊ณณ์—์„œ ํšŒ์›๊ฐ€์ž…/๋กœ๊ทธ์ธ ๋‘ ๊ฐ€์ง€ ์กฐ๊ธˆ ๋‹ค๋ฅธ ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•ด์•ผ ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€๊ฐ€ ๋ณต์žกํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๋‹ค.

ํšŒ์›๊ฐ€์ž… ํŽ˜์ด์ง€์—์„œ ์„ฑ๊ณต ์‹œ์—๋Š” loginํŽ˜์ด์ง€๋กœ ์ด๋™ํ•˜๋ฉด์„œ ์ด์ „์— ์žˆ๋˜ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋Š” ์ง€์›Œ์ค˜์•ผ ํ–ˆ๊ณ , ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋Š” ์„ฑ๊ณต์‹œ์— ๋ฐ›์€ userData๋ฅผ ์ด์šฉํ•ด accessToken์„ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด ์šฐ์„  localStorage์— ์ €์žฅํ•˜๊ณ  ๋ฉ”์ธ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•ด์•ผ ํ–ˆ๋‹ค.

//AuthForm.tsx
...
export default function AuthForm({ isLogin }: AuthFormProps) {
	...
  const authService = useAuth();
  const { push } = useRouter();

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const { email, password } = userInfo;
    if (isLogin) {
      authService
        .signIn(email, password)
        .then((userData: UserCredential) => {
          return userData.user.getIdToken();
        })
        .then((token) => {
          localStorage.setItem(AccessToken, token);
          push('/');
        })
        .catch((error) => setMessage(error.message));
    } else {
      authService
        .signUp(email, password)
        .then(() => {
          push('/login');
          setMessage('');
        })
        .catch((error) => setMessage(error.message));
    }
  };

  return (
    <Layout action="submit" onSubmit={handleSubmit} isActive={!isInActive}>
      <AuthInput
        name={EMAIL_INPUT.name}
        text={userInfo.email}
        title={'์ด๋ฉ”์ผ'}
        placeholder={EMAIL_INPUT.placeholder}
        dispatch={dispatch}
      />
      <AuthInput
        name={PASSWORD_INPUT.name}
        text={userInfo.password}
        title={'๋น„๋ฐ€๋ฒˆํ˜ธ'}
        placeholder={PASSWORD_INPUT.placeholder}
        dispatch={dispatch}
      />
      {message && <ErrorMessage message={message} />}
      <button type="submit" disabled={isInActive}>
        {name}
      </button>
    </Layout>
  );
}

๋กœ์ง์ด promise chaining์œผ๋กœ ์ด์–ด์ง€๋‹ค ๋ณด๋‹ˆ ํ•˜๋Š” ์ผ์— ๋น„ํ•ด ๋กœ์ง์ด ์ฐจ์ง€ํ•˜๋Š” ์ฝ”๋“œ๊ฐ€ ๋งŽ์ด ๋ณด์—ฌ, ๋”ฐ๋กœ ๋ถ„๋ฆฌํ•˜๊ณ ์ž ํ–ˆ๋‹ค. ์ •๋ฆฌํ•  ๋•Œ custom Hook์„ ์ด์šฉํ•˜๋ ค ํ–ˆ์ง€๋งŒ custom Hook์€ handleSubmit ๋‚ด๋ถ€์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์— "ํ•จ์ˆ˜"๋กœ ๋ถ„๋ฆฌํ•ด ์ •๋ฆฌํ–ˆ๋‹ค.

//logic.ts
export const login = async (
  userInfo: UserInfoType,
  authService: AuthService,
  router: NextRouter,
  setMessage: React.Dispatch<React.SetStateAction<string>>
) => {
  const { push } = router;
  const { email, password } = userInfo;
  try {
    const userData = await authService.signIn(email, password);
    const token = await userData.user.getIdToken();
    localStorage.setItem(AccessToken, token);
    push('/');
  } catch (error) {
    const loginError = error as { message: string };
    setMessage(loginError?.message);
  }
};

export const register = async (
  userInfo: UserInfoType,
  authService: AuthService,
  router: NextRouter,
  setMessage: React.Dispatch<React.SetStateAction<string>>
) => {
  const { push } = router;
  const { email, password } = userInfo;
  try {
    await authService.signUp(email, password);
    push('/login');
  } catch (error) {
    const registerError = error as { message: string };
    setMessage(registerError?.message);
  }
};

//AuthForm.tsx
export default function AuthForm({ isLogin }: AuthFormProps) {
  const [message, setMessage] = useState('');
  const [userInfo, dispatch] = useReducer(authReducer, initialState);
  const isInActive = !userInfo.emailValid || !userInfo.passwordValid;
  const authService = useAuthService();
  const router = useRouter();
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    if (isLogin) {
      login(userInfo, authService, router, setMessage);
    } else {
      register(userInfo, authService, router, setMessage);
    }
  };
  const name = isLogin ? '๋กœ๊ทธ์ธ' : 'ํšŒ์›๊ฐ€์ž…';

  return (
 	...
  );
}

OAuthSignIn

firebase์˜ OAuth ์„œ๋น„์Šค๋ฅผ ์ด์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ ํ•ด๋‹น ํ”Œ๋žซํผ์˜ provider์„ ์—ฐ๊ฒฐํ•˜๊ณ  signInWithPopup ๋ฉ”์†Œ๋“œ๋ฅผ ์ด์šฉํ•˜๋ฉด ๋˜๋Š”๋ฐ, provider์˜ ์ข…๋ฅ˜์— ๊ด€๊ณ„์—†์ด ํ•˜๋‚˜์˜ ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด ์ „๋‹ฌ๋ฐ›์€ platform์˜ provider์™€ ์—ฐ๊ฒฐ๋  ์ˆ˜ ์žˆ๊ฒŒ ๋กœ์ง์„ ๊ตฌ์„ฑํ–ˆ๋‹ค. AuthService์™€ ์—ฐ๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ buttonํƒœ๊ทธ์˜ name์œผ๋กœ ์ธ์ž๋ฅผ ์ „๋‹ฌ๋˜๊ฒŒ ํ–ˆ๋‹ค.

//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)
  }

  OAuthSignIn(platform: OAuthType): Promise<UserCredential> {
    const provider = this[`${platform}Provider`]
    return signInWithPopup(this.auth, provider)
  }
}

//PlatformBtns.tsx

export const OAuthLogin = async (
  name: OAuthType,
  authService: AuthService,
  router: NextRouter
) => {
  const { push } = router
  try {
    const userData = await authService.OAuthSignIn(name)
    const token = await userData.user.getIdToken()
    localStorage.setItem(AccessToken, token)
    push("/")
  } catch (error) {
    console.log(error)
  }
}

export default function PlatformBtns() {
  const authService = useAuthService()
  const router = useRouter()
  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    const { name } = e.currentTarget
    if (name === PLATFORM.GOOGLE || name === PLATFORM.GITHUB) {
      OAuthLogin(name, authService, router)
    }
  }
  return (
    <Wrapper>
      <button name={PLATFORM.GOOGLE} onClick={handleClick}>
        ...
      </button>
      <button name={PLATFORM.GITHUB} onClick={handleClick}>
        ...
      </button>
    </Wrapper>
  )
}

Logout

Navbar์˜ ๋กœ๊ทธ์•„์›ƒ ๋ฒ„ํŠผ์„ ์ถ”๊ฐ€ํ•ด ๊ฐ„๋‹จํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

export default function Navbar() {
  const { push } = useRouter()
  const authService = useAuthService()
  const onSignOut = () => {
    authService
      .signOut()
      .then(() => {
        localStorage.removeItem(AccessToken)
        push("/login")
      })
      .catch(error => console.log(error))
  }
  return (
    <Wrapper>
      <Layout>
        <Link href="/">๋ชจ์œผ์žก</Link>
        <Btns>
          <button onClick={onSignOut}>๋กœ๊ทธ์•„์›ƒ</button>
        </Btns>
      </Layout>
    </Wrapper>
  )
}

์•„์ง ์˜ˆ์™ธ ์ฒ˜๋ฆฌ, token๊ด€๋ฆฌ, redirection์„ ํ•ด์ฃผ์–ด์•ผ ํ•˜์ง€๋งŒ ์šฐ์„  ์ „๋ฐ˜์ ์œผ๋กœ ์™„์„ฑํ•œ ํ›„์— ๋‹ค์‹œ ๋Œ์•„์™€์„œ ์ˆ˜์ •ํ•˜๊ณ ์ž ํ•œ๋‹ค. ๋‚จ์€ ํฐ ๊ธฐ๋Šฅ๋“ค์€ Database, ์ž๊ฒฉ์กฐ๊ฑด/์šฐ๋Œ€์‚ฌํ•ญ ์ฒดํฌ ๊ธฐ๋Šฅ, ํฌ๋กค๋Ÿฌ ํ”„๋กœ๊ทธ๋žจ์„ ์ˆ˜ํ–‰ํ•ด ์ค„ ์„œ๋ฒ„ ๊ธฐ๋Šฅ์ด ์žˆ๋‹ค.

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