Skip to content

๐Ÿฝ ๋ง›์ง‘์„ ๊ณต์œ ํ•˜๊ณ  ์ž์‹ ์˜ ๋ง›์ง‘ ์ง€๋„๋ฅผ ์™„์„ฑ์‹œ์ผœ๊ฐ€๋Š” SNS ํ”Œ๋žซํผ

Notifications You must be signed in to change notification settings

NamJongtae/TasteMap

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

๐Ÿฝ TasteMap

thumbnail

๐ŸŽˆ ํ…Œ์ŠคํŠธ ๊ณ„์ •

ID PW
test@a.com asdzxc123!

๐ŸŒ ๋ฐฐํฌ URL : ๐Ÿด TasteMap


๐Ÿ“ƒ ๋ชฉ์ฐจ (ํด๋ฆญ ์‹œ ํ•ด๋‹น ๋ชฉ์ฐจ๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค.)


๐Ÿ™‹โ€โ™‚ ํ”„๋กœ์ ํŠธ ์†Œ๊ฐœ

TasteMap์€ ๋ง›์ง‘์„ ๊ณต์œ ํ•˜๊ณ  ์ž์‹ ์˜ ๋ง›์ง‘ ์ง€๋„๋ฅผ ์™„์„ฑํ•˜๋Š” SNS ํ”Œ๋žซํผ์ž…๋‹ˆ๋‹ค.

  • ๋‚˜๋งŒ ์•Œ๊ณ  ์žˆ๋Š” ์ˆจ๊ฒจ์ง„ ๋ง›์ง‘ ์ •๋ณด๋ฅผ ๊ณต์œ ํ•˜๊ณ , ์›ํ•˜๋Š” ๋ง›์ง‘์„ ๋‚˜์˜ ๋ง›์ง‘ ์ง€๋„์— ์ถ”๊ฐ€ํ•˜์—ฌ ๋‚˜๋งŒ์˜ ๋ง›์ง‘ ์ง€๋„๋ฅผ ์™„์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๋‚ด๊ฐ€ ๋งŒ๋“  ๋ง›์ง‘ ์ง€๋„๋ฅผ ๋ณ„๋„์˜ URL ๋งํฌ๋ฅผ ํ†ตํ•ด ๊ณต์œ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์ง€๋„์˜ ๋กœ๋“œ ๋ทฐ ๊ธฐ๋Šฅ์„ ํ†ตํ•ด ํ•ด๋‹น ๋ง›์ง‘ ์œ„์น˜๋ฅผ ์‰ฝ๊ฒŒ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๋Œ“๊ธ€๊ณผ ๋‹ต๊ธ€ ์ž‘์„ฑ์„ ํ†ตํ•ด ์—ฌ๋Ÿฌ ์‚ฌ์šฉ์ž๋“ค๊ณผ ๋ง›์ง‘์— ๋Œ€ํ•ด ์†Œํ†ตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ํŒ”๋กœ์šฐํ•œ ์‚ฌ์šฉ์ž์˜ ๊ฒŒ์‹œ๋ฌผ์„ ํ”ผ๋“œ ํŽ˜์ด์ง€์—์„œ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ฐœ๋ฐœ ์˜๋„

  • ๋‚˜๋งŒ ์•Œ๊ณ  ์žˆ๋Š” ์ˆจ์€ ๋ง›์ง‘์„ ๊ณต์œ ํ•˜๊ณ , ์‚ฌ์šฉ์ž๋“ค์ด ๋ง›์ง‘ ์ •๋ณด๋ฅผ ์•Œ์•„๊ฐ€๋ฉฐ ๋‚˜๋งŒ์˜ ๋ง›์ง‘ ์ง€๋„๋ฅผ ์™„์„ฑํ•ด๊ฐ€๋Š” SNS ํ”Œ๋žซํผ์„ ๊ตฌํ˜„ํ•˜๊ณ ์ž ๊ฐœ๋ฐœํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
  • ๊ฐ€๊ฒŒ ํ™๋ณด ๋ฐ ์ง€์—ญ ํŠน์ƒ‰ ๋จน๊ฑฐ๋ฆฌ๋“ค์„ ์•Œ๋ ค ์ง€์—ญ ๊ฒฝ์ œ ํ™œ์„ฑํ™”์— ๋„์›€์„ ์ฃผ๊ณ ์ž ๊ฐœ๋ฐœํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ“† ๊ฐœ๋ฐœ๊ธฐ๊ฐ„

๊ฐœ๋ฐœ ์‹œ์ž‘ : 2023. 09. 08

๊ฐœ๋ฐœ ์™„๋ฃŒ : 2023. 10. 08

Refactoring

  • react-query ๋„์ž… : 2023.11.19 ~ 2023.11.27
  • customhook ๋””์ž์ธ ํŒจํ„ด ์ ์šฉ: 2023.12.01 ~ 2023.12.03
  • clean code : 2023.12.04 ~ 2023.12.19
  • react-hook-form : 2023.12.12 ~ 2023.12.18

โœจ Refactoring

๐Ÿงช react-query ๋„์ž…

๋„์ž… ์ด์œ 

  • ๊ธฐ์กด์—๋Š” redux-toolkit thunk๋ฅผ ์ด์šฉํ•˜์—ฌ api ์ฒ˜๋ฆฌ ๋ฐ api ์ƒํƒœ๊ด€๋ฆฌ๊ฐ€ ์ฝ”๋“œ ์–‘์ด ๋งŽ์•„์ง€๊ณ , ๋ณต์žกํ•˜๋‹ค๋Š” ๋‹จ์ ์ด ์žˆ์–ด react-query๋ฅผ ๋„์ž…ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

๋„์ž… ๋ฐฉ์‹

  • ๊ธฐ์กด redux-toolkit์€ global state ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๊ณ , react-query๋Š” api ์ฒ˜๋ฆฌ ๋ฐ api ์ƒํƒœ๊ด€๋ฆฌ์— ์‚ฌ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค.

๋„์ž…์œผ๋กœ ์–ป์€ ์ด์ 

  • react-query ๋„์ž…์œผ๋กœ ์„œ๋ฒ„ api ์ฒ˜๋ฆฌ๊ฐ€ ๋งค์šฐ ๊ฐ„๊ฒฐํ•ด ์กŒ์œผ๋ฉด ์ƒํƒœ๊ด€๋ฆฌ ์ฝ”๋“œ๋ฅผ ์ง์ ‘ ๊ตฌ์„ฑํ•˜์ง€ ์•Š์•„๋„ react-query ์ž์ฒด ๋‚ด์žฅ๋œ ์ƒํƒœ๊ด€๋ฆฌ ์†์„ฑ์„ ํ†ตํ•ด ์ƒํƒœ๊ด€๋ฆฌ๋ฅผ ํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.
  • react-query๋Š” ์บ์‹ฑ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์†๋„ ํ–ฅ์ƒ์— ๋„์›€์„ ์ค„ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.
  • ๋™์ผํ•œ ๋ฐ์ดํ„ฐ ์š”์ฒญ์˜ ๊ฒฝ์šฐ ์ž๋™์œผ๋กœ ์ œ๊ฑฐํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ค‘๋ณต ์š”์ฒญ์„ ์‹ ๊ฒฝ์“ฐ์ง€ ์•Š์•„๋„ ๋˜์–ด ํŽธ๋ฆฌํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿงฉ customhook ๋””์ž์ธ ํŒจํ„ด ์ ์šฉ

์ ์šฉ ์ด์œ 

  • UI์™€ logic๋ฅผ ๊ตฌ๋ถ„ํ•  ์ˆ˜ ์žˆ์œผ๋ฉด UI์™€ ๊ธฐ๋Šฅ์—๋งŒ ์ดˆ์ ์„ ๋‘˜ ์ˆ˜ ์žˆ์–ด ๊ฐœ๋ฐœ ๋ฐ ์œ ์ง€ ๋ณด์ˆ˜๊ฐ€ ์šฉ์ดํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.
  • customhook ํŒจํ„ด์„ ์‚ฌ์šฉํ•˜๋ฉด ๋ฐ˜๋ณต๋˜๋Š” logic์˜ ์žฌ์‚ฌ์šฉ์„ฑ์„ ๋†’์ผ ์ˆ˜ ์žˆ๊ธฐ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

์ ์šฉ ๋ฐฉ์‹

  • customhook์œผ๋กœ ์ปดํฌ๋„ŒํŠธ์— ํ•„์š”ํ•œ ๋กœ์ง๋“ค์„ ๊ตฌํ˜„ํ•˜๊ณ  ๊ธฐ์กด ์ปดํฌ๋„ŒํŠธ์—๋Š” UI ์ฝ”๋“œ๋งŒ ๋‚จ๊ธฐ๋„๋ก ๋ฆฌํŒฉํ† ๋ง ํ•˜์˜€์œผ๋ฉฐ, ํ•„์š”ํ•œ ๋กœ์ง์€ ์ปค์Šคํ…€ ํ›…์œผ๋กœ ๋ถˆ๋Ÿฌ์™€ ์‚ฌ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค.

์ ์šฉ์œผ๋กœ ์–ป์€ ์ด์ 

  • ๊ธฐ์กด container, presenter ํŒจํ„ด์€ props๋กœ presenter์— ํ•„์š”ํ•œ ๊ฐ’๋“ค์„ ๋„˜๊ฒจ์ฃผ์–ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค. props๊ฐ€ ๋งŽ์•„ ์งˆ์ˆ˜๋ก ์ฝ”๋“œ๊ฐ€ ๋ณต์žกํ•ด์ง€๋ฉฐ, ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ์•ˆ์ข‹์•„์ง„๋‹ค๋Š” ๋‹จ์ ์ด ์กด์žฌํ•˜์˜€์Šต๋‹ˆ๋‹ค. customhook ํŒจํ„ด์„ ํ†ตํ•ด ์ด๋ฅผ ํ•ด๊ฒฐํ•˜์—ฌ ์ฝ”๋“œ๊ฐ€ ๋” ๊ฐ„๊ฒฐํ•ด์งˆ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.
  • customhook์œผ๋กœ ๊ตฌํ˜„ํ•˜์˜€๊ธฐ ๋•Œ๋ฌธ์— ์žฌ์‚ฌ์šฉ์„ฑ์ด ๋†’์•„์กŒ์Šต๋‹ˆ๋‹ค.
  • UI์™€ ๊ธฐ๋Šฅ์„ ๊ตฌ๋ถ„ํ•˜์˜€๊ธฐ ๋•จ๋ฌธ์— ๊ฐ๊ฐ์˜ ๊ธฐ๋Šฅ์— ์ง‘์ค‘ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ ์œ ์ง€๋ณด์ˆ˜์„ฑ ๋˜ํ•œ ํ–ฅ์ƒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

์ฝ”๋“œ ๋น„๊ต

์ฝ”๋“œ ๋ณด๊ธฐ

์ด์ „ ์ฝ”๋“œ

import React, { useEffect, useRef, useState } from "react";
import {
  LoginBtn,
  LoginForm,
import {
  InputWrapper,
  SocialLoginItem
} from "./login.styels";

import { useValidationInput } from "../../hook/useValidationInput";
import Loading from "../../component/commons/loading/Loading";
import ErrorMsg from "../../component/commons/errorMsg/ErrorMsg";
import UserInput from "../../component/commons/userInput/UserInput";
import { useLoginMutation } from "../../hook/query/auth/useLoginMutation";
import { useSocialLoginMutation } from "../../hook/query/auth/useSocialLoginMutation";
import { useSupportedWebp } from '../../hook/useSupportedWebp';

export default function Login() {
  const { isWebpSupported, resolveWebp } = useSupportedWebp();
  const [disabled, setDisabled] = useState(true);
  const emailRef = useRef<HTMLInputElement>(null);
  const [emailValue, emailValid, onChangeEmail, setEmailValue] =
    useValidationInput("", "email", false);
  const [passwordValue, passwordValid, onChangePassword, setPasswordValue] =
    useValidationInput("", "password", false);

  const { mutate: loginMutate, isPending: loginIsPending } = useLoginMutation();
  const { mutate: socialLoginMutate, isPending: socialLoginIsPending } =
    useSocialLoginMutation();

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    if (emailValid.valid && passwordValid.valid) {
      loginMutate({ email: emailValue, password: passwordValue });
      setEmailValue("");
      setPasswordValue("");
      setDisabled(true);
    }
  };

  const socialLoginHandler = (type: "google" | "github") => {
    socialLoginMutate(type);
  };

  useEffect(() => {
    emailRef.current && emailRef.current.focus();
  }, []);

  useEffect(() => {
    if (emailValid.valid && passwordValid.valid) {
      setDisabled(false);
    } else {
      setDisabled(true);
    }
  }, [emailValid, passwordValid]);

  return (
    <>
      <Title className='a11y-hidden'>๋กœ๊ทธ์ธ ํŽ˜์ด์ง€</Title>
      <Wrapper>
        <LoginForm onSubmit={handleSubmit}>
          <LoginFormTitle>
            <img src={resolveWebp("/assets/webp/icon-loginLogo.webp", "svg")} />
          </LoginFormTitle>
              <InputWrapper>
            <UserInput
              label_hidden={true}
              label={"์ด๋ฉ”์ผ"}
              id={"input-email"}
              placeholder={"Email"}
              type={"text"}
              value={emailValue}
              onChange={onChangeEmail}
              InputRef={emailRef}
            />
            {emailValid.errorMsg && <ErrorMsg message={emailValid.errorMsg} />}
          </InputWrapper>
          <InputWrapper>
            <UserInput
              label_hidden={true}
              label={"๋น„๋ฐ€๋ฒˆํ˜ธ"}
              id={"input-password"}
              placeholder={"Password"}
              type={"password"}
              onChange={onChangePassword}
              value={passwordValue}
            />
            {passwordValid.errorMsg && (
              <ErrorMsg message={passwordValid.errorMsg} />
            )}
          </InputWrapper>

          <FindAccountLink to={"/findAccount"}>
            ์ด๋ฉ”์ผ{" "}
            <span style={{ fontSize: "10px", verticalAlign: "top" }}>|</span>{" "}
            ๋น„๋ฐ€๋ฒˆํ˜ธ ์ฐพ๊ธฐ
          </FindAccountLink>
          <LoginBtn type='submit' disabled={disabled}>
            ๋กœ๊ทธ์ธ
          </LoginBtn>

          <SignupText>
            ์•„์ง ํšŒ์›์ด ์•„๋‹Œ๊ฐ€์š”?
            <SignupLink to={"/signup"}>ํšŒ์›๊ฐ€์ž…</SignupLink>
          </SignupText>
          <SocialLoginWrapper>
            <SocialLoginItem>
              <SocialLoginBtn
                className='google'
                type='button'
                onClick={() => socialLoginHandler("google")}
                $isWebpSupported={isWebpSupported}
              >
                ๊ตฌ๊ธ€ ๊ณ„์ •์œผ๋กœ ๋กœ๊ทธ์ธ
              </SocialLoginBtn>
            </SocialLoginItem>
            <SocialLoginItem>
              <SocialLoginBtn
                className='github'
                type='button'
                onClick={() => socialLoginHandler("github")}
                $isWebpSupported={isWebpSupported}
              >
                ๊นƒ ํ—ˆ๋ธŒ ๊ณ„์ •์œผ๋กœ ๋กœ๊ทธ์ธ
              </SocialLoginBtn>
            </SocialLoginItem>
          </SocialLoginWrapper>
        </LoginForm>
      <Wrapper />
     {(loginIsPending || socialLoginIsPending) && <Loading />}
    </>

customhook ํŒจํ„ด ์ ์šฉ ํ›„ ์ฝ”๋“œ

import React from "react";
import Loading from "../../component/commons/loading/Loading";
import ErrorMsg from "../../component/commons/errorMsg/ErrorMsg";
import UserInput from "../../component/commons/userInput/UserInput";
import { useSupportedWebp } from "../../hook/useSupportedWebp";
import {
  LoginBtn,
  LoginForm,
  LoginFormTitle,
  Title,
  Wrapper,
  SignupLink,
  FindAccountLink,
  SignupText,
  SocialLoginWrapper,
  SocialLoginBtn,
  InputWrapper,
  SocialLoginItem
} from "./login.styels";
import { useLogin } from "../../hook/logic/login/useLogin";

export default function Login() {
  const { isWebpSupported, resolveWebp } = useSupportedWebp();
  const {
    loginHandler,
    socialLoginHandler,
    disabled,
    onChangeEmail,
    onChangePassword,
    loginIsPending,
    socialLoginIsPending,
    emailValue,
    emailRef,
    emailValid,
    passwordValue,
    passwordValid
  } = useLogin();

  return (
    <>
      <Title className='a11y-hidden'>๋กœ๊ทธ์ธ ํŽ˜์ด์ง€</Title>
      <Wrapper>
        <LoginForm onSubmit={loginHandler}>
          <LoginFormTitle>
            <img src={resolveWebp("/assets/webp/icon-loginLogo.webp", "svg")} />
          </LoginFormTitle>
          <InputWrapper>
            <UserInput
              label_hidden={true}
              label={"์ด๋ฉ”์ผ"}
              id={"input-email"}
              placeholder={"Email"}
              type={"text"}
              value={emailValue}
              onChange={onChangeEmail}
              InputRef={emailRef}
            />
            {emailValid.errorMsg && <ErrorMsg message={emailValid.errorMsg} />}
          </InputWrapper>
          <InputWrapper>
            <UserInput
              label_hidden={true}
              label={"๋น„๋ฐ€๋ฒˆํ˜ธ"}
              id={"input-password"}
              placeholder={"Password"}
              type={"password"}
              onChange={onChangePassword}
              value={passwordValue}
            />
            {passwordValid.errorMsg && (
              <ErrorMsg message={passwordValid.errorMsg} />
            )}
          </InputWrapper>

          <FindAccountLink to={"/findAccount"}>
            ์ด๋ฉ”์ผ{" "}
            <span style={{ fontSize: "10px", verticalAlign: "top" }}>|</span>{" "}
            ๋น„๋ฐ€๋ฒˆํ˜ธ ์ฐพ๊ธฐ
          </FindAccountLink>
          <LoginBtn type='submit' disabled={disabled}>
            ๋กœ๊ทธ์ธ
          </LoginBtn>

          <SignupText>
            ์•„์ง ํšŒ์›์ด ์•„๋‹Œ๊ฐ€์š”?
            <SignupLink to={"/signup"}>ํšŒ์›๊ฐ€์ž…</SignupLink>
          </SignupText>
          <SocialLoginWrapper>
            <SocialLoginItem>
              <SocialLoginBtn
                className='google'
                type='button'
                onClick={() => socialLoginHandler("google")}
                $isWebpSupported={isWebpSupported}
              >
                ๊ตฌ๊ธ€ ๊ณ„์ •์œผ๋กœ ๋กœ๊ทธ์ธ
              </SocialLoginBtn>
            </SocialLoginItem>
            <SocialLoginItem>
              <SocialLoginBtn
                className='github'
                type='button'
                onClick={() => socialLoginHandler("github")}
                $isWebpSupported={isWebpSupported}
              >
                ๊นƒ ํ—ˆ๋ธŒ ๊ณ„์ •์œผ๋กœ ๋กœ๊ทธ์ธ
              </SocialLoginBtn>
            </SocialLoginItem>
          </SocialLoginWrapper>
        </LoginForm>
      </Wrapper>
      {(loginIsPending || socialLoginIsPending) && <Loading />}
    </>
  );
}

๐Ÿ—ƒ๏ธ clean code

ํด๋ฆฐ์ฝ”๋“œ๋ž€, ๋ฌด์กฐ๊ฑด ์งง์€ ์ฝ”๋“œ๊ฐ€ ์•„๋‹Œ ์ฝ๊ธฐ ์ข‹์€ ์ฝ”๋“œ, ํ๋ฆ„ ํŒŒ์•…์ด ์‰ฝ๊ณ , ์œ ์ง€ ๋ณด์ˆ˜๊ฐ€ ์šฉ์ดํ•œ ์ฝ”๋“œ๋ฅผ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

์•„๋ž˜ ์กฐ๊ฑด์„ ๋งŒ์กฑํ•˜๋Š” clean code๋กœ ๋ฆฌํŒฉํ† ๋งํ•˜์˜€์Šต๋‹ˆ๋‹ค.

  • ์‘์ง‘๋„ : ๊ฐ™์€ ๋ชฉ์ ์˜ ์ฝ”๋“œ๋Š” ๋ญ‰์ณ๋‘ก๋‹ˆ๋‹ค.
  • ๋‹จ์ผ์ฑ…์ž„ : ํ•˜๋‚˜์˜ ์ผ์„ ํ•˜๋Š” ๋šœ๋ ทํ•œ ์ด๋ฆ„์˜ ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ํ•˜๋‚˜์˜ ์ปดํฌ๋„ŒํŠธ์—์„œ ํ•˜๋‚˜์˜ ์ฑ…์ž„์„ ๊ฐ€์ง€๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.
  • ์ถ”์ƒํ™” : ํ•ต์‹ฌ ๊ฐœ๋…์„ ํ•„์š”ํ•œ ๋งŒํผ๋งŒ ๋…ธ์ถœ์‹œํ‚ต๋‹ˆ๋‹ค.

์ ์šฉ ์ด์œ 

  • ๊ธฐ์กด ์ฝ”๋“œ๋Š” ๋„ˆ๋ฌด ๊ธธ๊ณ  ๋ณต์žกํ•˜๋ฉฐ, ์œ ์ง€๋ณด์ˆ˜์‹œ ์ฝ”๋“œ์˜ ํŒŒ์•…์ด ์–ด๋ ค์› ์Šต๋‹ˆ๋‹ค.
  • ์ฝ”๋“œ๋ฅผ ๋ณด๋Š” ์‚ฌ๋žŒ์ด ์ดํ•ดํ•˜๊ธฐ ์‰ฝ๋„๋ก, ์œ ์ง€๋ณด์ˆ˜ ๋ฐ ๊ฐ€๋…์„ฑ ํ–ฅ์ƒ์„ ์œ„ํ•ด ์ ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค.

์ ์šฉ ๋ฐฉ์‹

  • ์‘์ง‘๋„, ๋‹จ์ผ์ฑ…์ž„, ์ถ”์ƒํ™” 3๊ฐ€์ง€ ์›์น™์„ ๋งŒ์กฑ์‹œํ‚ค๋Š” clean code๋กœ ์ฝ”๋“œ๋ฅผ ๋ณ€๊ฒฝํ•˜์˜€์Šต๋‹ˆ๋‹ค.

์ฝ”๋“œ ๋น„๊ต

์ฝ”๋“œ ๋ณด๊ธฐ

์ด์ „ ์ฝ”๋“œ

import React from "react";
import Loading from "../../component/commons/loading/Loading";
import ErrorMsg from "../../component/commons/errorMsg/ErrorMsg";
import UserInput from "../../component/commons/userInput/UserInput";
import {
  LoginBtn,
  LoginForm,
  LoginFormTitle,
  Title,
  Wrapper,
  SignupLink,
  FindAccountLink,
  SignupText,
  SocialLoginWrapper,
  SocialLoginBtn,
  InputWrapper,
  SocialLoginItem
} from "./login.styels";
import { useLogin } from "../../hook/logic/login/useLogin";
import { useSelector } from 'react-redux';
import { RootState } from '../../store/store';
import { resolveWebp } from '../../library/resolveWebp';

export default function Login() {
  const isWebpSupported = useSelector((state: RootState) => state.setting.isWebpSupported);
  const {
    loginHandler,
    socialLoginHandler,
    disabled,
    onChangeEmail,
    onChangePassword,
    loginIsPending,
    socialLoginIsPending,
    emailValue,
    emailRef,
    emailValid,
    passwordValue,
    passwordValid
  } = useLogin();

  return (
    <>
      <Title className='a11y-hidden'>๋กœ๊ทธ์ธ ํŽ˜์ด์ง€</Title>
      <Wrapper>
        <LoginForm onSubmit={loginHandler}>
          <LoginFormTitle>
            <img src={resolveWebp("/assets/webp/icon-loginLogo.webp", "svg")} />
          </LoginFormTitle>
          <InputWrapper>
            <UserInput
              label_hidden={true}
              label={"์ด๋ฉ”์ผ"}
              id={"input-email"}
              placeholder={"Email"}
              type={"text"}
              value={emailValue}
              onChange={onChangeEmail}
              InputRef={emailRef}
            />
            {emailValid.errorMsg && <ErrorMsg message={emailValid.errorMsg} />}
          </InputWrapper>
          <InputWrapper>
            <UserInput
              label_hidden={true}
              label={"๋น„๋ฐ€๋ฒˆํ˜ธ"}
              id={"input-password"}
              placeholder={"Password"}
              type={"password"}
              onChange={onChangePassword}
              value={passwordValue}
            />
            {passwordValid.errorMsg && (
              <ErrorMsg message={passwordValid.errorMsg} />
            )}
          </InputWrapper>

          <FindAccountLink to={"/findAccount"}>
            ์ด๋ฉ”์ผ{" "}
            <span style={{ fontSize: "10px", verticalAlign: "top" }}>|</span>{" "}
            ๋น„๋ฐ€๋ฒˆํ˜ธ ์ฐพ๊ธฐ
          </FindAccountLink>
          <LoginBtn type='submit' disabled={disabled}>
            ๋กœ๊ทธ์ธ
          </LoginBtn>

          <SignupText>
            ์•„์ง ํšŒ์›์ด ์•„๋‹Œ๊ฐ€์š”?
            <SignupLink to={"/signup"}>ํšŒ์›๊ฐ€์ž…</SignupLink>
          </SignupText>
          <SocialLoginWrapper>
            <SocialLoginItem>
              <SocialLoginBtn
                className='google'
                type='button'
                onClick={() => socialLoginHandler("google")}
                $isWebpSupported={isWebpSupported}
              >
                ๊ตฌ๊ธ€ ๊ณ„์ •์œผ๋กœ ๋กœ๊ทธ์ธ
              </SocialLoginBtn>
            </SocialLoginItem>
            <SocialLoginItem>
              <SocialLoginBtn
                className='github'
                type='button'
                onClick={() => socialLoginHandler("github")}
                $isWebpSupported={isWebpSupported}
              >
                ๊นƒ ํ—ˆ๋ธŒ ๊ณ„์ •์œผ๋กœ ๋กœ๊ทธ์ธ
              </SocialLoginBtn>
            </SocialLoginItem>
          </SocialLoginWrapper>
        </LoginForm>
      </Wrapper>
      {(loginIsPending || socialLoginIsPending) && <Loading />}
    </>
  );
}

claean code ๋ณ€๊ฒฝ ํ›„

์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ธฐ๋Šฅ๋ณ„๋กœ ๋ถ„๋ฆฌ => LoginForm ์ƒ์„ฑ

๊ธฐ์กด useLogin customhook๋ฅผ ๊ธฐ๋Šฅ๋ณ„๋กœ ๋ถ„๋ฆฌ

import React from "react";
import styled from "styled-components";
import LoginForm from "./LoginForm";
export const Title = styled.h1``;

const Wrapper = styled.main`
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: #f5f5f5;
  height: 100vh;
  overflow: auto;
`;

export default function Login() {
  return (
    <>
      <Title className='a11y-hidden'>๋กœ๊ทธ์ธ ํŽ˜์ด์ง€</Title>
      <Wrapper>
        <LoginForm />
      </Wrapper>
    </>
  );
}

LoginForm ์ปดํฌ๋„ŒํŠธ ๊ธฐ๋Šฅ๋ณ„ ์„ธ๋ถ„ํ™” => LoginFormTitle, InputField, FindAccountLink, LoginBtn, SignupLink, SocialLoginBtns

// LoginForm.tsx
import React from "react";
import { InputField } from "../../component/commons/UI/InputField";
import LoginFormTitle from "./LoginFormTitle";
import FindAccountLink from "./FindAccountLink";
import SignupLink from "./SignupLink";
import { SocialLoginBtns } from "./SocialLoginBtns";
import styled from "styled-components";
import { useLoginDataFetch } from "../../hook/logic/login/useLoginDataFetch";
import { useSocialLoginDataFetch } from "../../hook/logic/login/useSocialLoginDataFetch";
import { useLoginEmailInput } from "../../hook/logic/login/useLoginEmailInput";
import Loading from "../../component/commons/loading/Loading";
import { useLoginPasswordInput } from "../../hook/logic/login/useLoginPasswordInput";

const Form = styled.form`
  display: flex;
  flex-direction: column;
  height: 100vh;
  gap: 20px;
  max-width: 400px;
  width: calc(100% - 60px);
  padding: 100px 40px 0 40px;
  @media screen and (max-width: 431px) {
    width: calc(100% - 40px);
    padding: 30px 20px;
  }
`;
const InputWrapper = styled.div`
  & > p {
    margin-top: 10px;
  }
`;
export const LoginBtn = styled.button`
  width: 100%;
  background-color: ${(props) => (props.disabled ? "#cbcbcb" : "gold")};
  padding: 14px 0;
  border-radius: 4px;
  font-size: 18px;
  font-weight: 500;
  margin-top: 10px;
  transition: all 0.5s;
`;

export default function LoginForm() {
  const { emailValue, emailValid, onChangeEmail, emailRef } =
    useLoginEmailInput();

  const { passwordValue, passwordValid, onChangePassword } =
    useLoginPasswordInput();

  const { loginIsPending, loginHandler } = useLoginDataFetch();

  const { socialLoginHandler, socialLoginIsPending } =
    useSocialLoginDataFetch();

  if (loginIsPending || socialLoginIsPending) {
    return <Loading />;
  }

  return (
    <Form onSubmit={loginHandler}>
      <LoginFormTitle />
      <InputField
        label_hidden={true}
        label={"์ด๋ฉ”์ผ"}
        name={"email"}
        id={"input-email"}
        placeholder={"Email"}
        type={"email"}
        onChange={onChangeEmail}
        value={emailValue}
        InputRef={emailRef}
        errorMsg={emailValid.errorMsg}
      />
      <InputField
        label_hidden={true}
        label={"๋น„๋ฐ€๋ฒˆํ˜ธ"}
        name={"password"}
        id={"input-password"}
        placeholder={"Password"}
        type={"password"}
        onChange={onChangePassword}
        value={passwordValue}
        errorMsg={passwordValid.errorMsg}
      />

      <FindAccountLink />
      <LoginBtn
        type='submit'
        disabled={!(emailValid.valid && passwordValid.valid)}
      >
        ๋กœ๊ทธ์ธ
      </LoginBtn>
      <SignupLink />

      <SocialLoginBtns
        buttonTypeArr={["google", "github"]}
        textArr={["๊ตฌ๊ธ€ ๊ณ„์ •์œผ๋กœ ๋กœ๊ทธ์ธ", "๊นƒ ํ—ˆ๋ธŒ ๊ณ„์ •์œผ๋กœ ๋กœ๊ทธ์ธ"]}
        onClickArr={[
          () => socialLoginHandler("google"),
          () => socialLoginHandler("github")
        ]}
      />
    </Form>
  );
}
// LoginFormTitle
import React from "react";
import styled from "styled-components";
import { resolveWebp } from "../../library/resolveWebp";

export const Title = styled.h2`
  text-align: center;
  font-weight: 500;
`;

export default function LoginFormTitle() {
  return (
    <Title>
      <img src={resolveWebp("/assets/webp/icon-loginLogo.webp", "svg")} />
    </Title>
  );
}
// SignupLink.tsx
import React from "react";
import styled from "styled-components";
import { Link } from "react-router-dom";

const SignupLinkWrapper = styled.div`
  display: inline-block;
  font-size: 12px;
  color: #111;
  text-align: center;
`;

const StyledSignupLink = styled(Link)`
  font-size: 12px;
  margin-left: 5px;
  font-weight: 500;
`;

export default function SignupLink() {
  return (
    <SignupLinkWrapper>
      ์•„์ง ํšŒ์›์ด ์•„๋‹Œ๊ฐ€์š”?
      <StyledSignupLink to={"/signup"}>ํšŒ์›๊ฐ€์ž…</StyledSignupLink>
    </SignupLinkWrapper>
  );
}
// SocialLoginBtns.tsx
import styled from "styled-components";
import { useSupportedWebp } from "../../hook/useSupportedWebp";
import { isMobile } from "react-device-detect";

const SocialLoginWrapper = styled.ul`
  position: relative;
	@@ -114,3 +54,35 @@ export const SocialLoginBtn = styled.button`
    background-color: ${isMobile ? "" : "#ddd"};
  }
`;

interface IPrpos {
  buttonTypeArr: string[];
  textArr: string[];
  onClickArr: React.MouseEventHandler<HTMLButtonElement>[];
}

export const SocialLoginBtns = ({
  buttonTypeArr,
  textArr,
  onClickArr
}: IPrpos) => {
  const { isWebpSupported } = useSupportedWebp();
  return (
    <SocialLoginWrapper>
      {textArr.map((text: string, i: number) => {
        return (
          <SocialLoginItem key={text + i}>
            <SocialLoginBtn
              className={buttonTypeArr[i]}
              type='button'
              onClick={onClickArr[i]}
              $isWebpSupported={isWebpSupported}
            >
              {text}
            </SocialLoginBtn>
          </SocialLoginItem>
        );
      })}
    </SocialLoginWrapper>
  );
};

๐Ÿ—’๏ธ react-hook-form ์ ์šฉ

์ ์šฉ ์ด์œ 

  • ๊ธฐ์กด form์— ๋Œ€ํ•œ ๋กœ์ง๊ณผ ๊ด€๋ จ ์ฝ”๋“œ๋“ค์ด ๋ณต์žกํ•˜๊ธฐ ๋•Œ๋ฌธ์— react-hook-form๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ€๋…์„ฑ ๋ฐ ์œ ์ง€๋ณด์ˆ˜์„ฑ ํ–ฅ์ƒ์„ ์œ„ํ•ด ๋„์ž…ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • formProvider๋ฅผ ํ†ตํ•ด form ํ•˜์œ„ input๋“ค์˜ ๊ฐ’๋“ค์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด ์ œ์–ด ์ปดํฌ๋„ŒํŠธ์˜ ์˜์กด์„ฑ์„ ๋ถ„๋ฆฌ์‹œํ‚ฌ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋„์ž…ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

์‚ฌ์šฉ ๋ฐฉ์‹

  • formProvider๋ฅผ ์ ์šฉ์‹œํ‚จ MyForm customhook๋ฅผ ๋งŒ๋“ค์–ด์„œ ์‚ฌ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • ๊ธฐ์กด InputField ์ปดํฌ๋„ŒํŠธ์— react-hook-form ์†์„ฑ์„ ์ ์šฉ์‹œ์ผฐ์Šต๋‹ˆ๋‹ค.
MyForm ์ฝ”๋“œ
import React from "react";
import {
  useForm,
  FormProvider,
  SubmitHandler,
  UseFormProps,
  FieldValues
} from "react-hook-form";
import { DevTool } from "@hookform/devtools";
import { Form } from "./myForm.styles";

// ์ œ๋„ค๋ฆญ ํƒ€์ž…์„ ์‚ฌ์šฉํ•œ ํผ interface ์ •์˜
interface GenericFormInterface<TFormData extends FieldValues> {
  children: React.ReactNode;
  onSubmit: SubmitHandler<TFormData>;
  formOptions?: UseFormProps<TFormData>;
}

export const MyForm = <TFormData extends FieldValues>({
  children,
  onSubmit,
  formOptions
}: GenericFormInterface<TFormData>) => {
  const methods = useForm<TFormData>(formOptions);
  return (
    // form provider๋ฅผ ํ†ตํ•ด useForm์—์„œ ๊ฐ€์ ธ์˜จ methods๋ฅผ children (ํ•˜์œ„ ์ปดํฌ๋„ŒํŠธ)์— ์ „๋‹ฌ
    <>
      <FormProvider {...methods}>
        <Form onSubmit={methods.handleSubmit(onSubmit)} noValidate>
          {children}
        </Form>
        <DevTool control={methods.control} />
      </FormProvider>
    </>
  );
};

์ฝ”๋“œ ๋น„๊ต

์ฝ”๋“œ ๋ณด๊ธฐ

์ด์ „ ์ฝ”๋“œ

import React from "react";
import { InputField } from "../../../component/commons/UI/InputField";
import LoginFormTitle from "./LoginFormTitle/LoginFormTitle";
import FindAccountLink from "./FindAccountLink/FindAccountLink";
import SignupLink from "./SignupLink/SignupLink";
import { SocialLoginBtns } from "./socialLoginBtns/SocialLoginBtns";
import { useLoginDataFetch } from "../../../hook/logic/login/useLoginDataFetch";
import { useSocialLoginDataFetch } from "../../../hook/logic/login/useSocialLoginDataFetch";
import { useLoginEmailInput } from "../../../hook/logic/login/useLoginEmailInput";
import Loading from "../../../component/commons/loading/Loading";
import { useLoginPasswordInput } from "../../../hook/logic/login/useLoginPasswordInput";
import { Form, LoginBtn } from '../login.styles';

export default function LoginForm() {
  const { emailValue, emailValid, onChangeEmail, emailRef } =
    useLoginEmailInput();

  const { passwordValue, passwordValid, onChangePassword } =
    useLoginPasswordInput();

  const { loginIsPending, loginHandler } = useLoginDataFetch();

  const { socialLoginHandler, socialLoginIsPending } =
    useSocialLoginDataFetch();

  if (loginIsPending || socialLoginIsPending) {
    return <Loading />;
  }

  return (
    <Form onSubmit={loginHandler}>
      <LoginFormTitle />
      <InputField
        label_hidden={true}
        label={"์ด๋ฉ”์ผ"}
        name={"email"}
        id={"input-email"}
        placeholder={"Email"}
        type={"email"}
        onChange={onChangeEmail}
        value={emailValue}
        InputRef={emailRef}
        errorMsg={emailValid.errorMsg}
      />
      <InputField
        label_hidden={true}
        label={"๋น„๋ฐ€๋ฒˆํ˜ธ"}
        name={"password"}
        id={"input-password"}
        placeholder={"Password"}
        type={"password"}
        onChange={onChangePassword}
        value={passwordValue}
        errorMsg={passwordValid.errorMsg}
      />

      <FindAccountLink />
      <LoginBtn
        type='submit'
        disabled={!(emailValid.valid && passwordValid.valid)}
      >
        ๋กœ๊ทธ์ธ
      </LoginBtn>
      <SignupLink />

      <SocialLoginBtns
        buttonTypeArr={["google", "github"]}
        textArr={["๊ตฌ๊ธ€ ๊ณ„์ •์œผ๋กœ ๋กœ๊ทธ์ธ", "๊นƒ ํ—ˆ๋ธŒ ๊ณ„์ •์œผ๋กœ ๋กœ๊ทธ์ธ"]}
        onClickArr={[
          () => socialLoginHandler("google"),
          () => socialLoginHandler("github")
        ]}
      />
    </Form>
  );
}

react-hook-form ์ ์šฉ ํ›„ ์ฝ”๋“œ

formProvider ์‚ฌ์šฉ์œผ๋กœ ๋ฒ„ํŠผ ์ œ์–ด๋ฅผ ์œ„ํ•œ input ๊ฐ’๋“ค์˜ ์˜์กด์„ฑ ๋ถ„๋ฆฌ ๊ฐ€๋Šฅ

=> input๋ณ„๋กœ ์ปดํฌ๋„ŒํŠธ ์„ธ๋ถ„ํ™” (email, password) ๊ฐ€๋Šฅ

// LoginForm.tsx
import React from "react";
import { useLoginDataFetch } from "../../../hook/logic/login/useLoginDataFetch";
import { MyForm } from "../../../component/commons/UI/myForm/MyForm";
import LoginFormContent from "./LoginFormContent/LoginFormContent";

export default function LoginForm() {
  const { loginIsPending, loginHandler, loginError } = useLoginDataFetch();

  return (
    <MyForm
      onSubmit={loginHandler}
      formOptions={{
        mode: "onChange",
        defaultValues: { email: "", password: "" }
      }}
    >
      <LoginFormContent
        loginError={loginError}
        loginIsPending={loginIsPending}
      />
    </MyForm>
  );
}
// LoginFormContent.tsx
import React from "react";
import { FormContentWrapper } from "../../login.styles";
import LoginFormTitle from "./LoginFormTitle/LoginFormTitle";
import LoginEmail from "./LoginEmailField/LoginEmail";
import LoginPassword from "./LoginPasswordField/LoginPassword";
import FindAccountLink from "./FindAccountLink/FindAccountLink";
import LoginError from "./LoginError/LoginError";
import LoginBtn from "./LoginBtn/LoginBtn";
import SignupLink from "./SignupLink/SignupLink";
import { SocialLogin } from "./socialLogin/SocialLogin";

interface IProps {
  loginError: Error | null;
  loginIsPending: boolean;
}
export default function LoginFormContent({
  loginError,
  loginIsPending
}: IProps) {
  return (
    <FormContentWrapper>
      <LoginFormTitle />

      <LoginEmail />

      <LoginPassword />

      <FindAccountLink />

      {<LoginError isError={loginError} />}

      <LoginBtn loginIsPending={loginIsPending} />

      <SignupLink />

      <SocialLogin />
    </FormContentWrapper>
  );
}
// LoginFormTitle.tsx
import React from "react";
import { resolveWebp } from "../../../../../library/resolveWebp";
import { FormTitle } from "../../../login.styles";

export default function LoginFormTitle() {
  return (
    <FormTitle>
      <img src={resolveWebp("/assets/webp/icon-loginLogo.webp", "svg")} />
    </FormTitle>
  );
}
// LoginEmail.tsx
import React from "react";
import { InputField } from "../../../../../component/commons/UI/InputField/InputField";
import {
  emailRegex,
  emailRegexErrorMsg
} from "../../../../../library/validationRegex";

export default function LoginEmail() {
  return (
    <InputField
      label_hidden={true}
      label={"์ด๋ฉ”์ผ"}
      name={"email"}
      id={"input-email"}
      placeholder={"Email"}
      type={"email"}
      pattern={{
        value: emailRegex,
        message: emailRegexErrorMsg
      }}
      duplicationErrorMsg={"์ค‘๋ณต๋œ ์ด๋ฉ”์ผ ์ž…๋‹ˆ๋‹ค."}
      required={true}
    />
  );
}
// LoginPassword.tsx
import { InputField } from "../../../../../component/commons/UI/InputField/InputField";
import {
  passwordRegex,
  passwordRegexErrorMsg
} from "../../../../../library/validationRegex";

export default function LoginPassword() {
  return (
    <InputField
      label_hidden={true}
      label={"๋น„๋ฐ€๋ฒˆํ˜ธ"}
      name={"password"}
      id={"input-password"}
      placeholder={"Password"}
      type={"password"}
      pattern={{
        value: passwordRegex,
        message: passwordRegexErrorMsg
      }}
      required={true}
    />
  );
}
// FindAccountLink.tsx
import React from "react";
import { Line, StyledFindAccountLink } from "../../../login.styles";

export default function FindAccountLink() {
  return (
    <StyledFindAccountLink to={"/findAccount"}>
      ์ด๋ฉ”์ผ <Line /> ๋น„๋ฐ€๋ฒˆํ˜ธ ์ฐพ๊ธฐ
    </StyledFindAccountLink>
  );
}
// LoginError.tsx
import React from "react";
import ErrorMsg from "../../../../../component/commons/errorMsg/ErrorMsg";
import { useLoginError } from "../../../../../hook/logic/login/useLoginError";
import { useFormContext } from "react-hook-form";

interface IProps {
  isError: Error | null;
}

export default function LoginError({ isError }: IProps) {
  const { getValues, reset } = useFormContext();
  const email = getValues("email");
  const password = getValues("password");
  const { error } = useLoginError({ email, password, reset, isError });

  return error ? <ErrorMsg message={error} /> : null;
}
// LoginBtn.tsx
import React from "react";
import { StyledLoginBtn } from "../../../login.styles";
import { useFormContext } from "react-hook-form";

interface IProps {
  loginIsPending: boolean;
}
export default function LoginBtn({ loginIsPending }: IProps) {
  const { formState } = useFormContext();

  return (
    <StyledLoginBtn
      type='submit'
      disabled={!formState.isValid || loginIsPending}
    >
      {loginIsPending ? "๋กœ๊ทธ์ธ์ค‘..." : "๋กœ๊ทธ์ธ"}
    </StyledLoginBtn>
  );
}
// SignupLink.tsx
import React from "react";
import { SignupLinkWrapper, StyledSignupLink } from "../../../login.styles";

export default function SignupLink() {
  return (
    <SignupLinkWrapper>
      ์•„์ง ํšŒ์›์ด ์•„๋‹Œ๊ฐ€์š”?
      <StyledSignupLink to={"/signup"}>ํšŒ์›๊ฐ€์ž…</StyledSignupLink>
    </SignupLinkWrapper>
  );
}
// SocialLoginBtns.tsx
import Loading from "../../../../../component/commons/loading/Loading";
import { useSocialLoginDataFetch } from "../../../../../hook/logic/login/useSocialLoginDataFetch";
import { SocialLoginWrapper } from "../../../login.styles";
import SocialLoginBtn from "./SocialLoginBtn/SocialLoginBtn";

export const SocialLogin = () => {
  const { socialLoginHandler, socialLoginIsPending } =
    useSocialLoginDataFetch();

  if (socialLoginIsPending) {
    return <Loading />;
  }

  return (
    <SocialLoginWrapper>
      <SocialLoginBtn
        loginType='google'
        socialLoginHandler={socialLoginHandler}
        btnText='๊ตฌ๊ธ€ ๊ณ„์ •์œผ๋กœ ๋กœ๊ทธ์ธ'
      />

      <SocialLoginBtn
        loginType='github'
        socialLoginHandler={socialLoginHandler}
        btnText='๊นƒ ํ—ˆ๋ธŒ ๊ณ„์ •์œผ๋กœ ๋กœ๊ทธ์ธ'
      />
    </SocialLoginWrapper>
  );
};

๐Ÿชœ ํšŒ์›๊ฐ€์ž… useFunnel ์ ์šฉ

useFunnel์€์—ฌ๋Ÿฌ ๋‹จ๊ณ„๋กœ ์ด๋ฃจ์–ด์ง„ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ƒํƒœ์™€ ํ๋ฆ„์„ ํ•œ๋ฒˆ์— ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด toss์—์„œ ๊ฐœ๋ฐœํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ž…๋‹ˆ๋‹ค.

(๐Ÿ”— ๊ณต์‹์‚ฌ์ดํŠธ: https://slash.page/ko/libraries/react/use-funnel/readme.i18n/)

์ ์šฉ ์ด์œ 

  • ์—ฌ๋Ÿฌ ๋‹จ๊ณ„๊ฐ€ ์กด์žฌํ•˜๋Š” ํšŒ์›๊ฐ€์ž… ํŽ˜์ด์ง€๋ฅผ ํ๋ฆ„ ํŒŒ์•…์ด ์šฉ์ดํ•˜๊ณ , ๊ฐ€๋…์„ฑ ์ข‹์€ ํด๋ฆฐ ์ฝ”๋“œ๋กœ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ์ ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค.

์ ์šฉ ๋ฐฉ์‹

  • useFunnel customhook๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ํšŒ์›๊ฐ€์ž… ์ปดํฌ๋„ŒํŠธ์— ํšŒ์›๊ฐ€์ž… ๋‹จ๊ณ„๋ณ„ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์ ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • react-hook-form์˜ formProvider๋ฅผ ์ด์šฉํ•˜์—ฌ ๊ฐ ๋‹จ๊ณ„๋ณ„ input ๊ฐ’๋“ค์„ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • ๊ธฐ์กด useFunnel customhook์— Step๋ฅผ ๊ด€๋ฆฌํ•˜๋„๋ก setpIndex ์ƒํƒœ์™€ prevStepHandler, nextStepHandler ํ•จ์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
useFunnel ์ฝ”๋“œ
import React, { ReactElement, ReactNode, useState } from "react";

export interface StepProps {
  name: string;
  children: ReactNode;
}

export interface FunnelProps {
  children: Array<ReactElement<StepProps>>;
}

export const useFunnel = (steps: string[]) => {
  // state๋ฅผ ํ†ตํ•ด ํ˜„์žฌ ์Šคํ…์„ ๊ด€๋ฆฌ
  // setStep ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ํ˜„์žฌ ์Šคํ…์„ ๋ณ€๊ฒฝ
  const [step, setStep] = useState(steps[0]);

  // step index๋ฅผ ๊ด€๋ฆฌํ•œ๋‹ค.
  const [setpIndex, setStepIndex] = useState(0);

  // ์ด์ „ ์Šคํ…์œผ๋กœ ๋Œ์•„๊ฐ„๋‹ค.
  const prevStepHandler = () => {
    setStepIndex((prev) => prev - 1);
    setStep(steps[setpIndex - 1]);
  };

  // ๋‹ค์Œ ์Šคํ…์œผ๋กœ ๋„˜์–ด๊ฐ„๋‹ค.
  const nextStepHandler = () => {
    setStepIndex((prev) => prev + 1);
    setStep(steps[setpIndex + 1]);
  };

  // ๊ฐ ๋‹จ๊ณ„๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” Step ์ปดํฌ๋„ŒํŠธ
  // children์„ ํ†ตํ•ด ๊ฐ ์Šคํ…์˜ ์ปจํ…์ธ ๋ฅผ ๋ Œ๋”๋ง 
  const Step = (props: StepProps): ReactElement => {
    return <>{props.children}</>;
  };

  // ์—ฌ๋Ÿฌ ๋‹จ๊ณ„์˜ Step ์ปดํฌ๋„ŒํŠธ ์ค‘ ํ˜„์žฌ ํ™œ์„ฑํ™”๋œ ์Šคํ…์„ ๋ Œ๋”๋งํ•˜๋Š” Funnel
  // find๋ฅผ ํ†ตํ•ด Step ์ค‘ ํ˜„์žฌ Step์„ ์ฐพ์•„ ๋ Œ๋”๋ง
  const Funnel = ({ children }: FunnelProps) => {
    const targetStep = children.find(
      (childStep) => childStep.props.name === step
    );

    return <>{targetStep}</>;
  };

  return {
    Funnel,
    Step,
    setStep,
    currentStep: step,
    nextStepHandler,
    prevStepHandler
  } as const;
};

์ ์šฉํ›„ ์–ป๋Š” ์ด์ 

  • ์ด์ „ ์ฝ”๋“œ์— ๋น„ํ•ด ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ์ด ์ข‹์•„์กŒ์œผ๋ฉฐ, ํšŒ์›๊ฐ€์ž… ๋‹จ๊ณ„๋ณ„ ํ๋ฆ„ ํŒŒ์•…์ด ์šฉ์ดํ•ด์กŒ์Šต๋‹ˆ๋‹ค.
  • input๋“ค์˜ ์ƒํƒœ๋ฅผ ํ•˜๋‚˜์˜ form์—์„œ ๊ด€๋ฆฌํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ƒํƒœ ๊ด€๋ฆฌ๊ฐ€ ํŽธ๋ฆฌํ•ด์กŒ์Šต๋‹ˆ๋‹ค.

์ฝ”๋“œ ๋น„๊ต

์ฝ”๋“œ ๋ณด๊ธฐ

์ด์ „ ์ฝ”๋“œ

import React from "react";
import UserInfoSetting from "./userInfoSetting/UserInfoSetting";
import ProfileSetting from "./profileSetting/ProfileSetting";
import Loading from "../../component/commons/loading/Loading";
import { useUserInfoSettingEmailInput } from "../../hook/logic/signup/useUserInfoSettingEmailInput";
import { useUserInfoSettingPwInput } from "../../hook/logic/signup/useUserInfoSettingPwInput";
import { useUserInfoSettingPwChkInput } from "../../hook/logic/signup/useUserInfoSettingPwChkInput";
import { useUserInfoSettingPhoneInput } from "../../hook/logic/signup/useUserInfoSettingPhoneInput";
import { useSignupStepController } from "../../hook/logic/signup/useSignupStepController";
import { useProfileSettingDisplayNameInput } from "../../hook/logic/signup/useProfileSettingDisplayNameInput";
import { useProfileSettingImg } from "../../hook/logic/signup/useProfileSettingImg";
import { useSignupDataFetch } from "../../hook/logic/signup/useSignupDataFetch";
import { useProfileSettingIntroduceInput } from "../../hook/logic/signup/useProfileSettingIntroduceInput";
import { useSingupSetScreenSize } from "../../hook/logic/signup/useSignupSetScreenSize";
import ProgressBar from "./progressBar/ProgressBar";
import { FormWrapper, Title, Wrapper } from './signup.styles';

export default function Signup() {
  const { emailValue, emailValid, onChangeEmail } =
    useUserInfoSettingEmailInput();

  const {
    passwordValue,
    passwordValid,
    onChangePassword,
    checkPwMatchValidation
  } = useUserInfoSettingPwInput();

  const {
    passwordChkValue,
    passwordChkValid,
    onChangePasswordChk,
    checkPwChkMatchValidation
  } = useUserInfoSettingPwChkInput();

  const { phoneValue, phoneValid, onChangePhone } =
    useUserInfoSettingPhoneInput();

  const { displayNameValue, displayNameValid, onChangeDislayName } =
    useProfileSettingDisplayNameInput();

  const {
    imgInputRef,
    previewImg,
    uploadImg,
    isImgLoading,
    changeImgHandler,
    imgResetHandler
  } = useProfileSettingImg();

  const { introduceValue, onChangeIntroduce, preventKeydownEnter } =
    useProfileSettingIntroduceInput();

  const { signupHandler, signupLoading } = useSignupDataFetch({
    displayNameValue,
    uploadImg,
    emailValue,
    passwordValue,
    phoneValue,
    introduceValue
  });

  const {
    next,
    percentage,
    setPercentage,
    nextStepHandler,
    prevStepHandler,
    cancelHandler,
    completedUserInfoSetting,
    completedProfileSetting
  } = useSignupStepController({
    emailValid: emailValid.valid,
    passwordValid: passwordValid.valid,
    passwordChkValid: passwordChkValid.valid,
    phoneValid: phoneValid.valid,
    displayNameValid: displayNameValid.valid
  });

  if (signupLoading) {
    return <Loading />;
  }

  // next(๊ธฐ๋ณธ ์ •๋ณด ์ž…๋ ฅ ํ›„ ๋‹ค์Œ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅธ ๊ฒฝ์šฐ)
  const SignupForm = !next ? (
    <UserInfoSetting
      emailValue={emailValue}
      onChangeEmail={onChangeEmail}
      emailValid={emailValid}
      passwordValue={passwordValue}
      onChangePassword={onChangePassword}
      checkPwMatchValidation={checkPwMatchValidation}
      passwordValid={passwordValid}
      passwordChkValue={passwordChkValue}
      onChangePasswordChk={onChangePasswordChk}
      checkPwChkMatchValidation={checkPwChkMatchValidation}
      passwordChkValid={passwordChkValid}
      phoneValue={phoneValue}
      onChangePhone={onChangePhone}
      phoneValid={phoneValid}
      nextDisabled={
        !(
          emailValid.valid &&
          passwordChkValid.valid &&
          passwordValid.valid &&
          phoneValid.valid
        )
      }
      nextStepHandler={nextStepHandler}
      cancelHandler={cancelHandler}
    />
  ) : (
    <ProfileSetting
      setPercentage={setPercentage}
      prevStepHandler={prevStepHandler}
      signupHandler={signupHandler}
      imgInputRef={imgInputRef}
      changeImgHandler={changeImgHandler}
      previewImg={previewImg}
      imgResetHandler={imgResetHandler}
      displayNameValue={displayNameValue}
      onChangeDislayName={onChangeDislayName}
      introduce={introduceValue}
      onChangeIntroduce={onChangeIntroduce}
      displayNameValid={displayNameValid}
      signupDisabled={
        !(
          emailValid.valid &&
          passwordChkValid.valid &&
          passwordValid.valid &&
          phoneValid.valid &&
          displayNameValid.valid
        )
      }
      isImgLoading={isImgLoading}
      preventKeydownEnter={preventKeydownEnter}
    />
  );

  // ๋ชจ๋ฐ”์ผ ํ™”๋ฉด 100vh ๋†’์ด ์„ค์ •์‹œ ํ™”๋ฉด ์Šคํฌ๋กค ๋ฌธ์ œ ํ•ด๊ฒฐ
  useSingupSetScreenSize();

  return (
    <Wrapper>
      <Title>ํšŒ์›๊ฐ€์ž…</Title>
      <ProgressBar
        percentage={percentage}
        completedUserInfoSetting={completedUserInfoSetting}
        completedProfileSetting={completedProfileSetting}
      />
      <FormWrapper>{SignupForm}</FormWrapper>
    </Wrapper>
  );
}
// UserInfoSetting.tsx
import React from "react";
import { InputField } from "../../../component/commons/UI/InputField";
import { CancelBtn, SignupBtn, SignupForm } from '../signup.styles';

interface IProps {
  emailValue: string;
  onChangeEmail: (e: React.ChangeEvent<HTMLInputElement>) => void;
  emailValid: {
    errorMsg: string;
    valid: boolean;
  };
  passwordValue: string;
  onChangePassword: (e: React.ChangeEvent<HTMLInputElement>) => void;
  passwordValid: {
    errorMsg: string;
    valid: boolean;
  };
  checkPwMatchValidation: (
    e: React.ChangeEvent<HTMLInputElement>,
    passwordChkValue: string
  ) => void;
  passwordChkValue: string;
  onChangePasswordChk: (e: React.ChangeEvent<HTMLInputElement>) => void;
  passwordChkValid: {
    errorMsg: string;
    valid: boolean;
  };
  checkPwChkMatchValidation: (
    e: React.ChangeEvent<HTMLInputElement>,
    passwordValue: string
  ) => void;
  phoneValue: string;
  onChangePhone: (e: React.ChangeEvent<HTMLInputElement>) => void;
  phoneValid: {
    errorMsg: string;
    valid: boolean;
  };
  nextStepHandler: (e: React.FormEvent<HTMLFormElement>) => void;
  nextDisabled: boolean;
  cancelHandler: () => void;
}

export default function UserInfoSetting({
  emailValue,
  onChangeEmail,
  emailValid,
  passwordValue,
  onChangePassword,
  checkPwMatchValidation,
  passwordValid,
  passwordChkValue,
  onChangePasswordChk,
  checkPwChkMatchValidation,
  passwordChkValid,
  phoneValue,
  onChangePhone,
  phoneValid,
  nextStepHandler,
  nextDisabled,
  cancelHandler
}: IProps) {
  return (
    <SignupForm onSubmit={nextStepHandler}>
      <InputField
        type='text'
        label={"์ด๋ฉ”์ผ (ํ•„์ˆ˜)"}
        name={"email"}
        id={"input-email"}
        placeholder={"์ด๋ฉ”์ผ ์ฃผ์†Œ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”."}
        value={emailValue}
        onChange={onChangeEmail}
        errorMsg={emailValid.errorMsg}
      />
      <InputField
        type='password'
        label={"๋น„๋ฐ€๋ฒˆํ˜ธ (ํ•„์ˆ˜)"}
        name={"password"}
        id={"input-password"}
        placeholder={"8-16์ž ํŠน์ˆ˜๋ฌธ์ž, ์ˆซ์ž, ์˜๋ฌธ ํฌํ•จ"}
        value={passwordValue}
        onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
          onChangePassword(e);
          checkPwMatchValidation(e, passwordChkValue);
        }}
        minLength={8}
        maxLength={16}
        errorMsg={passwordValid.errorMsg}
      />
      <InputField
        type='password'
        label={"๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ (ํ•„์ˆ˜)"}
        name={"password"}
        id={"input-passwordChk"}
        placeholder={"๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”."}
        value={passwordChkValue}
        onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
          onChangePasswordChk(e);
          checkPwChkMatchValidation(e, passwordValue);
        }}
        minLength={8}
        maxLength={16}
        errorMsg={passwordChkValid.errorMsg}
      />
      <InputField
        type='text'
        label={"ํœด๋Œ€ํฐ (ํ•„์ˆ˜)"}
        name={"phone"}
        id={"input-phone"}
        placeholder={"ํœด๋Œ€ํฐ ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”. ( - ์ œ์™ธ )"}
        value={phoneValue
          .replace(/[^0-9]/g, "")
          .replace(/^(\d{2,3})(\d{3,4})(\d{4})$/, `$1-$2-$3`)}
        onChange={onChangePhone}
        maxLength={13}
        errorMsg={phoneValid.errorMsg}
      />
      <SignupBtn type='submit' disabled={nextDisabled}>
        ๋‹ค์Œ
      </SignupBtn>
      <CancelBtn type='button' onClick={cancelHandler}>
        ์ทจ์†Œ
      </CancelBtn>
    </SignupForm>
  );
}
// ProfileSetting.tsx
import React from "react";
import TextAreaField from "../../../component/commons/UI/TextAreaField";
import ProfileSettingImg from "./profileSettingImg/ProfileSettingImg";
import { InputField } from "../../../component/commons/UI/InputField";
import styled from "styled-components";

const SignupForm = styled.form`
  display: flex;
  flex-direction: column;
  gap: 20px;
  width: 100%;
`;

const SignupBtn = styled.button`
  width: 100%;
  background-color: ${(props) => (props.disabled ? "#cbcbcb" : "gold")};
  cursor: ${(props) => (props.disabled ? "default" : "cursor")};
  padding: 14px 0;
  border-radius: 4px;
  font-size: 16px;
  font-weight: 500;
  margin-top: 10px;
  transition: all 0.5s;
`;

const PrevBtn = styled.button`
  width: 100%;
  background-color: #eee;
  padding: 14px 0;
  border-radius: 4px;
  font-size: 16px;
  font-weight: 500;
  color: #111;
`;

interface IProps {
  setPercentage: React.Dispatch<React.SetStateAction<string>>;
  prevStepHandler: () => void;
  signupHandler: (e: React.FormEvent<HTMLFormElement>) => Promise<void>;
  imgInputRef: React.RefObject<HTMLInputElement>;
  changeImgHandler: (e: React.ChangeEvent<HTMLInputElement>) => Promise<void>;
  previewImg: string;
  imgResetHandler: () => void;
  displayNameValue: string;
  onChangeDislayName: (e: React.ChangeEvent<HTMLInputElement>) => void;
  introduce: string;
  onChangeIntroduce: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
  displayNameValid: {
    errorMsg: string;
    valid: boolean;
  };
  signupDisabled: boolean;
  isImgLoading: boolean;
  preventKeydownEnter: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
}

export default function ProfileSetting({
  prevStepHandler,
  signupHandler,
  imgInputRef,
  changeImgHandler,
  previewImg,
  imgResetHandler,
  displayNameValue,
  onChangeDislayName,
  introduce,
  onChangeIntroduce,
  displayNameValid,
  signupDisabled,
  isImgLoading,
  preventKeydownEnter
}: IProps) {
  return (
    <SignupForm onSubmit={signupHandler}>
      <ProfileSettingImg
        imgInputRef={imgInputRef}
        changeImgHandler={changeImgHandler}
        previewImg={previewImg}
        imgResetHandler={imgResetHandler}
        isImgLoading={isImgLoading}
      />

      <InputField
        type='text'
        label={"๋‹‰๋„ค์ž„ (ํ•„์ˆ˜)"}
        name={"nickname"}
        id={"input-nickname"}
        placeholder={"4-10์ž ์˜๋ฌธ, ์˜๋ฌธ + ์ˆซ์ž"}
        value={displayNameValue}
        onChange={onChangeDislayName}
        minLength={4}
        maxLength={10}
        errorMsg={displayNameValid.errorMsg}
      />

      <TextAreaField
        label={"์ž๊ธฐ์†Œ๊ฐœ"}
        label_hidden={true}
        name={"introduce"}
        id={"input-nickname"}
        placeholder={"์ตœ๋Œ€ 100์ž๊นŒ์ง€ ์ž‘์„ฑ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค."}
        value={introduce}
        onChange={onChangeIntroduce}
        onKeyDown={preventKeydownEnter}
        maxLength={100}
      />

      <SignupBtn type='submit' disabled={signupDisabled}>
        ํšŒ์›๊ฐ€์ž…
      </SignupBtn>
      <PrevBtn
        className='prev'
        type='button'
        onClick={() => {
          prevStepHandler();
        }}
      >
        ์ด์ „
      </PrevBtn>
    </SignupForm>
  );
}

useFuneel ์ ์šฉ ์ฝ”๋“œ

// Signup.tsx
import React from "react";
import { Wrapper, Title } from "./signup.styles";
import { MyForm } from "../../component/commons/UI/myForm/MyForm";
import FormContent from "./FormContent/FormContent";
import { useSignupDataFetch } from "../../hook/logic/signup/useSignupDataFetch";
import Loading from "../../component/commons/loading/Loading";
import { useSingupSetScreenSize } from "../../hook/logic/signup/useSignupSetScreenSize";

export default function Signup() {
  const { signupHandler, signupLoading } = useSignupDataFetch();

// ๋ชจ๋ฐ”์ผ ํ™”๋ฉด 100vh ๋†’์ด ์„ค์ •์‹œ ํ™”๋ฉด ์Šคํฌ๋กค ๋ฌธ์ œ ํ•ด๊ฒฐ
  useSingupSetScreenSize();

  if (signupLoading) {
    return <Loading />;
  }
  return (
    <Wrapper>
      <Title>ํšŒ์›๊ฐ€์ž…</Title>
      <MyForm
        onSubmit={signupHandler}
        formOptions={{
          mode: "onChange",
          defaultValues: {
            email: "",
            password: "",
            passwordChk: "",
            phone: "",
            nickname: "",
            img: process.env.REACT_APP_DEFAULT_PROFILE_IMG,
            introduce: ""
          }
        }}
      >
        <FormContent />
      </MyForm>
    </Wrapper>
  );
}
// FormContent.tsx
import React from "react";
import { FormContentWrapper } from "../signup.styles";
import { useFunnel } from "../../../hook/useFunnel";
import UserInfoSetting from "./userInfoSetting/UserInfoSetting";
import ProfileSetting from "./profileSetting/ProfileSetting";
import ProgressBar from "../progressBar/ProgressBar";

export default function FormContent() {
  const steps = ["userInfoSetting", "profileSetting"];
  const { Funnel, Step, currentStep, prevStepHandler, nextStepHandler } = useFunnel(steps);

  return (
    <FormContentWrapper>
      <ProgressBar currentStep={currentStep} steps={steps}
      />
      <Funnel>
        <Step name='userInfoSetting'>
          <UserInfoSetting nextStepHandler={nextStepHandler} />
        </Step>

        <Step name='profileSetting'>
          <ProfileSetting prevStepHandler={prevStepHandler} />
        </Step>
      </Funnel>
    </FormContentWrapper>
  );
}
// ProfileSetting.tsx
import React from "react";
import { FieldWrapper, PrevBtn } from "../../signup.styles";
import IntroduceField from "./intorduceField/IntroduceField";
import DisplayNameField from "./displayNameField/DisplayNameField";
import ProfileImgField from "./profileImgField/ProfileImgField";
import SignupBtn from './signupBtn/SignupBtn';
import { useDispatch } from "react-redux";
import { AppDispatch } from "../../../../store/store";
import { signupSlice } from "../../../../slice/signupSlice";

interface IProps {
  prevStepHandler: () => void;
}

export default function ProfileSetting({ prevStepHandler }: IProps) {
  const dispatch = useDispatch<AppDispatch>();
  const minusPercentageHandler = () => {
    dispatch(signupSlice.actions.minusPercentage(50));
  };
  return (
    <>
      <FieldWrapper>
        <ProfileImgField />

        <DisplayNameField />

        <IntroduceField />

        <PrevBtn
          onClick={() => {
            prevStepHandler();
            minusPercentageHandler();
          }}
        >
          ์ด์ „
        </PrevBtn>
        <SignupBtn />
      </FieldWrapper>
    </>
  );
}
// UserInfoSetting.tsx
import React from "react";
import { FieldWrapper } from "../../signup.styles";
import EmailField from "./emailField/EmailField";
import PasswordField from "./passwordField/PasswordField";
import PasswordChkField from "./passwordChkField/PasswordChkField";
import PhoneField from "./phoneField/PhoneField";
import NextBtn from "./nextBtn/NextBtn";

interface IProps {
  nextStepHandler: () => void;
}
export default function UserInfoSetting({ nextStepHandler }: IProps) {

  return (
    <FieldWrapper>
      <EmailField />

      <PasswordField />

      <PasswordChkField />

      <PhoneField />

      <NextBtn
        nextStepHandler={nextStepHandler}
      />
    </FieldWrapper>
  );
}

โš™ ๊ฐœ๋ฐœํ™˜๊ฒฝ

ํ”„๋ก ํŠธ์—”๋“œ ๋ฒก์—”๋“œ ๋””์ž์ธ ๋ฐฐํฌ, ๊ด€๋ฆฌ
Html CSS JavaScript TypeScript

๐Ÿ”ฉ ๋ฒก์—”๋“œ & API

  • ๋„ค์ด๋ฒ„ ๊ฒ€์ƒ‰ API๋ฅผ ํ†ตํ•ด ๋ง›์ง‘ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • ๋„ค์ด๋ฒ„ ๊ฒ€์ƒ‰ API๋กœ ์–ป์€ ๋ง›์ง‘ ์ •๋ณด์™€ ์ขŒํ‘œ๋ฅผ KakaMapAPI์— ์ „๋‹ฌํ•˜์—ฌ ์ง€๋„๋ฅผ ๊ทธ๋ฆฌ๊ณ , ๋งˆ์ปค๋กœ ํ•ด๋‹น ๋ง›์ง‘์„ ํ‘œ์‹œํ•˜๋„๋ก ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
  • ํŒŒ์ด์–ด๋ฒ ์ด์Šค๋ฅผ ์ด์šฉํ•˜์—ฌ db๋ฅผ ๊ตฌ์„ฑํ•˜๊ณ , ๋กœ๊ทธ์ธ, ๋กœ๊ทธ์•„์›ƒ, ๊ฒŒ์‹œ๋ฌผ, ๋Œ“๊ธ€, ๋‹ต๊ธ€, ํ”„๋กœํ•„ ๋“ฑ ์ฃผ์š” ๊ธฐ๋Šฅ API๋ฅผ ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

โ›“ ์•„ํ‚คํ…์ฒ˜

architecture


๐Ÿšฉ User Flow ( ์ด๋ฏธ์ง€๋ฅผ ํด๋ฆญ ํ•ด์ฃผ์„ธ์š”. )

userFlow


๐Ÿ›  ํ”„๋กœ์ ํŠธ ๊ด€๋ฆฌ

  • GitHub Issue
    • ๋น ๋ฅธ issue ์ƒ์„ฑ์„ ์œ„ํ•ด issue ํ…œํ”Œ๋ฆฟ์„ ๋งŒ๋“ค์–ด ์‚ฌ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค.
    • issue label์„ ์ƒ์„ฑํ•˜์—ฌ ์–ด๋–ค ์ž‘์—…์„ ํžˆ๋Š”์ง€ ๊ตฌ๋ถ„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
    • issue๋ฅผ ํ†ตํ•ด ๊ตฌํ˜„ํ•  ๋‚ด์šฉ๊ณผ ์ฒดํฌ๋ฆฌ์ŠคํŠธ๋ฅผ ๋งŒ๋“ค์–ด ์–ด๋–ค ์ž‘์—…์„ ํ• ์ง€ ๋ฆฌ์ŠคํŠธ ๋งŒ๋“ค์–ด ๊ด€๋ฆฌํ•˜์˜€์Šต๋‹ˆ๋‹ค.

issue

  • GitHub Project
    • ํ”„๋กœ์ ํŠธ ๋ณด๋“œ์˜ ์ด์Šˆ ๋ชฉ๋ก์„ ํ†ตํ•ด ๊ฐœ๋ฐœ ๊ณผ์ •๊ณผ ์ง„ํ–‰ ์ƒํ™ฉ์„ ํ•œ ๋ˆˆ์— ์•Œ์•„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

board

๐Ÿ“ƒ GitHub ์ปจ๋ฒค์…˜

์–ด๋–ค ์ž‘์—…์„ ํ–ˆ๋Š”์ง€ ํŒŒ์•…ํ•˜๊ธฐ ์œ„ํ•ด ์ปจ๋ฒค์…˜์„ ์ •ํ•˜์—ฌ commit๊ณผ isuue๋ฅผ ๊ด€๋ฆฌํ•˜์˜€์Šต๋‹ˆ๋‹ค.

Fix : ์ˆ˜์ •์‚ฌํ•ญ๋งŒ ์žˆ์„ ๊ฒฝ์šฐ

Feat : ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ์ด ์ถ”๊ฐ€ ๋˜๊ฑฐ๋‚˜ ์—ฌ๋Ÿฌ ๋ณ€๊ฒฝ ์‚ฌํ•ญ๋“ค์ด ์žˆ์„ ๊ฒฝ์šฐ

Style : ์Šคํƒ€์ผ๋งŒ ๋ณ€๊ฒฝ๋˜์—ˆ์„ ๊ฒฝ์šฐ

Docs : ๋ฌธ์„œ๋ฅผ ์ˆ˜์ •ํ•œ ๊ฒฝ์šฐ

Refactor : ์ฝ”๋“œ ๋ฆฌํŒฉํ† ๋ง์„ ํ•˜๋Š” ๊ฒฝ์šฐ

Remove : ํŒŒ์ผ์„ ์‚ญ์ œํ•˜๋Š” ์ž‘์—…๋งŒ ์ˆ˜ํ–‰ํ•œ ๊ฒฝ์šฐ

Rename : ํŒŒ์ผ ํ˜น์€ ํด๋”๋ช…์„ ์ˆ˜์ •ํ•˜๊ฑฐ๋‚˜ ์˜ฎ๊ธฐ๋Š” ์ž‘์—…๋งŒ์ธ ๊ฒฝ์šฐ

Relese : ๋ฐฐํฌ ๊ด€๋ จ ์ž‘์—…์ธ ๊ฒฝ์šฐ

Chore : ๊ทธ ์™ธ ๊ธฐํƒ€ ์‚ฌํ•ญ์ด ์žˆ์„ ๊ฒฝ์šฐ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.


๐Ÿ‘€ ๊ตฌํ˜„ ๊ธฐ๋Šฅ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ( ์ œ๋ชฉ ํด๋ฆญ ์‹œ ํ•ด๋‹น ๊ธฐ๋Šฅ ์ƒ์„ธ์„ค๋ช…์œผ๋กœ ์ด๋™๋ฉ๋‹ˆ๋‹ค. )

๐Ÿ”— ๋กœ๊ทธ์ธ ๐Ÿ”— ํšŒ์›๊ฐ€์ž… ๐Ÿ”— ์•„์ด๋””/๋น„๋ฐ€๋ฒˆํ˜ธ ์ฐพ๊ธฐ
๋กœ๊ทธ์ธ ํšŒ์›๊ฐ€์ž… ๊ณ„์ •์ฐพ๊ธฐ
๐Ÿ”— ๊ฒŒ์‹œ๋ฌผ ์กฐํšŒ ๐Ÿ”— ๊ฒŒ์‹œ๋ฌผ ์—…๋กœ๋“œ ๐Ÿ”— ๊ฒŒ์‹œ๋ฌผ ์ˆ˜์ •
๊ฒŒ์‹œ๋ฌผ ์กฐํšŒ ๊ฒŒ์‹œ๋ฌผ ์—…๋กœ๋“œ ๊ฒŒ์‹œ๋ฌผ ์ˆ˜์ •
๐Ÿ”— ๊ฒŒ์‹œ๋ฌผ ์‚ญ์ œ ๐Ÿ”— ๊ฒŒ์‹œ๋ฌผ ์‹ ๊ณ  ๐Ÿ”— ๋ง›์ง‘์ถ”๊ฐ€, ์ข‹์•„์š”
๊ฒŒ์‹œ๋ฌผ ์‚ญ์ œ ๊ฒŒ์‹œ๋ฌผ ์‹ ๊ณ  ๋ง›์ง‘์ถ”๊ฐ€, ์ข‹์•„์š”
๐Ÿ”— ๋Œ“๊ธ€, ๋‹ต๊ธ€ ๐Ÿ”— ํ”„๋กœํ•„ ํŽ˜์ด์ง€ ๐Ÿ”— ํŒ”๋กœ์šฐ, ํŒ”๋กœ์ž‰
๋Œ“๊ธ€,๋‹ต๊ธ€ ํ”„๋กœํ•„ ํŽ˜์ด์ง€ ํŒ”๋กœ์šฐ, ํŒ”๋กœ์ž‰

About

๐Ÿฝ ๋ง›์ง‘์„ ๊ณต์œ ํ•˜๊ณ  ์ž์‹ ์˜ ๋ง›์ง‘ ์ง€๋„๋ฅผ ์™„์„ฑ์‹œ์ผœ๊ฐ€๋Š” SNS ํ”Œ๋žซํผ

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages