방구석에 놔둔 개발 노트

1년차 웹 / 앱 프론트엔드 엔지니어의 좌충우돌 얼렁뚱땅 앞뒤짱구 생존기

2023-02-19: 개인 프로젝트 도전기 (5주차)

목표 정산

이번 주에 목표로 둔 사항들은 아래와 같다.

  • 관리자 페이지 기능 구현
    • 앞서 View를 구현한 관리자 로그인, 회원가입 등의 페이지들의 Firebase 기능 연동
    • Firebase Auth의 기능들을 적극적으로 활용
  • 사용자 대기 입력 페이지 Firebase 기능 연동
  • React-Query 학습
    • 사실상 위의 두 과정을 모두 적용하기 위해서는 React-Query의 학습이 필수불가결이다.
      • 왜 서버 상태 관리가 필요한지?
      • 페칭과 캐시, 그리고 쿼리란 무엇인지?
      • React-Query의 기본 환경 설정, QueryClinet와 QueryClientProvider
      • 그리고 React-Query 사용의 핵심인 useQuery와 useMutation
        • 일단은 여기까지 우선적으로 공부하고 이해하여 정리하기로.

이번 주의 가장 큰 고비는 사실 React-Query였다. Redux Toolkit의 thunk 방식에 익숙한 입장에서는 React-Query의 useQuery와 useMutation은 너무나도 낯선 존재였고, 공부할 때마다 하나하나 동작이 되지 않는 문제를 거쳐가다보니 학습 자체가 고통스러웠다.

그래도 동작이 잘 되는 것도 보고, 하나하나씩 계속해서 쓰다보니까 안다고 말하기에는 아직 미묘하다고 생각하지만 발가락을 살짝 담가본 느낌이 드는 것 같았다.

고통스러운 React-Query 도전기

저번 주도 그랬지만 useQuery를 공부하는 데에서 많이 힘들었던 부분은 페칭, 캐시, 쿼리의 의미와 stale과 cache를 이해하는 거였다.

지금도 완전히 이해하는 건 아니지만, 어렵기 때문에 어떻게든 쉽게 이해하기 위해 구글과 유튜브를 계속해서 검색하고 보고, 읽고, 써보고, 정리해가면서 겨우겨우 내 것으로 만들어 낸 것 같다.

하지만, useQuery말고도 핵심적으로 쓰이는 다른 hook이 하나 더 있었으니, 바로 useMutation이다.

useQuery가 데이터를 받아오는 데에 쓰인다면, useMutation은 데이터를 추가, 수정, 삭제하는 데에 쓰인다.

그러다보니 useQuery 만큼이나 어떻게 쓰이는지를 이해하는 게 중요했고, 다른 사람들의 예시 코드들을 참고하면서 사용법을 알아가려고 했지만 useQuery와 마찬가지로 코드 흐름을 이해하는 것 자체가 너무나도 버거웠다.

특히 Button 요소에 onClick 이벤트의 함수로서 useMutation이 적용된 함수를 사용하려고 했더니, 인자값을 void 등으로 받아오거나 하는 이슈가 있었어서 대체 이 hook을 어떻게 써야할지 막막했었다.

declare가 무려 네 개…!

위의 이미지를 보면 알겠지만, useMutation이 가지는 형태가 여러 케이스가 있다보니 mutationFn을 작성했더니 void를 받아오거나, mutationKey를 입력했더니 options으로 받아오는 등 사용하면 사용할 수록 혼란만 가중되는 느낌을 줬다.

덕분에 하나도 이해하질 못해 삽질만 늘어가던 와중에, useMutation을 어떻게 이용하는지 여러 글을 찾던 도중 아래의 글들 덕분에 그 방법을 찾아낸 느낌이 들었다.

useMutation

react-query 개념 및 정리

[React] React Query의 useMutation에 대해 알아보기

위의 내용들을 읽어보고 그 내용을 토대로 코드를 작성해본 결과, 원하는 대로 데이터가 들어가지는 걸 확인했다.

const CheckDataModal: React.FC<{
  userInfo: UserData;
  close: Dispatch<SetStateAction<boolean>>;
}> = ({ userInfo, close }) => {
  const visionState = useRecoilValue<boolean>(lowVisionState);

    const db = getFirestore();
  const waitingCol = collection(db, "testData");

  // 3)
  // useMutation에서 동작할 mutation 함수
  // 여기서는 handleSubmit 통해서 useMutation에 mutate로 연결해준 값을
  // 이 함수를 통해 인자로 받아와서 함수 내용을 동작하게 함
  // 여기서 Firebase를 쓰면 된다.
    const sendWaitingDataToDatabase = async (userInfo: UserData) => {
    const addWaitingData = await addDoc(waitingCol, userInfo)
      .then((data) => {
        return data.id;
      })
      .catch((error) => console.error(error.message));
    return addWaitingData;
  };

  // 1)
  // 우선 useMutation Hook으로 mutation 함수를 지정함
  const waitingMutation = useMutation(sendWaitingDataToDatabase, {
    // 4) 
    // 뮤테이션 함수가 동작하기 전에 돌아갈 내용 = onMutate
    // 받아오는 인자는 variable:변수 -> submitUserWaitingData에 mutate로 넣은 인자가 여기 변수로 쓰임
    // 반환된 값은 onError와 onSettled 쪽에 전달됨.
    onMutate: (variable) => console.log(variable),
    // 뮤테이션 함수가 동작했더니 에러가 발생할 경우 돌아갈 내용 = onError
    // 인자는 세 가지, error / variables / context
    onError: (error, variable) => console.log(error, variable),
    // 뮤테이션 함수가 동작했더니 성공적으로 적용했을 때 돌아갈 내용 = onSuccess
    onSuccess: (data, variable, context) => {
      // setRegisterState(true);
    },
    // 뮤테이션 함수가 동작하고 에러든 성공이든 상관없이 돌아갈 내용 = onSettled
    onSettled: (data, error, variable) => console.log(data, error, variable),
  });

  // close(false)

  // 2)
  // onClick으로 동작할 함수 -> useMutation을 통해서 mutate를 연결
  // mutate로 연결된 값은 위의 MutationFn의 인자로 들어감
  const submitUserWaitingData = (e: React.MouseEvent, userData: UserData) => {
    e.preventDefault();
    waitingMutation.mutate(userData);
  };

  return (
    <Box>
      <Button
        type="button"
        background="#58a6dc"
        color="#ffffff"
        padding="1.5rem"
        borderRadius="0.25rem"
        fontSize={visionState === false ? "1.25rem" : "1.625rem"}
        onClick={(e) => submitUserWaitingData(e, userInfo)}
      >
        버튼
      </Button>
    </Box>
  );
};

드디어 데이터가 잘 들어오는 것을 확인했다.

위의 내용을 이해하고 나니 이제 status에 따른 response를 사용해볼 수 있게 되면서 관리자 입력 페이지의 데이터를 정상적으로 처리할 수 있게 됐다.

학습하면서 공식 문서를 찾아보기도 했는데, 공식 문서의 내용들은 이해하기엔 너무나도 어려운 부분들이 많았다. 코드의 내용을 이해하지 못 하거나, 용어를 몰라서 이해하지 못 하거나.. 물론 영어라서 이해하지 못하는 점이 가장 큰 것 같지만.

다른 라이브러리를 학습할 때도 CSS 프레임워크 외에는 대체적으로 공식 문서에서 벽을 느끼던 점이 많았고, 실제로 여러가지를 사용하면서 깨달은 뒤에서야 공식 문서를 보면서 ‘이 내용이 요 내용이었구나.’라는 걸 알게될 때가 많았는데 React-Query도 언젠가 그럴 때가 오겠지.. 그랬으면 좋겠다.

Firebase Auth를 연동하여 관리자 계정 관리하기

저번에 View 작업을 진행해서 관리자 기본 입력 사항 페이지들을 다 만들어놨으니, 이제는 기능들이 정상적으로 동작하도록 구현해보려고 한다.

코드의 구현 방식은 전부 동일한 형태였다.

  • 우선 useMutation을 관리할 변수를 하나 선언함.
  • useMutation에 쓰일 뮤테이션 함수를 사용한다.
    • 여기에 Firebase Auth에 존재하는 각 함수들을 이용해 계정 관리를 한다.
  • onSubmit 이벤트가 적용되는 부분에서 유효성을 전부 거친 뒤에 작동될 부분에 useMutation 변수에 .mutate() 메소드를 이용해 유저 데이터를 보내준다.
  • 위의 과정을 거쳐서 적용될 성공(onSuccess)과 실패(onError)를 체크하여 내용에 적용해주면 끝.

구현 내용은 위의 절차를 거치므로 작성한 코드의 예시로서 회원가입 구현 코드를 가져와봤다.

const firebaseAuth = getAuth();

const signUpAccount = async (userData: AdminData) => {
    // 계정을 생성해주는 Firebase Auth의 함수
  const createState = createUserWithEmailAndPassword(
    firebaseAuth,
    userData.email,
    userData.password!
  )
    .then((cred) => cred.user)
    .catch((error) => error.message);
  return createState;
};

const signupMutation = useMutation(signUpAccount, {
  onError: (error, variable) => console.log(error, variable),
  onSuccess: (data, variable, context) => {
    // Firebase auth에서 catch를 통해 받은 error Message도 onSuccess로 들어온다.
    if (typeof data === "string") {
      const errorType = data.substring(22, data.length - 2);
        // 에러 메시지 양식에서 동일 부분만 제거해주기 위해 이렇게 작성했다.
      if (errorType === "email-already-in-use") {
        // toast 메시지 띄워줄것 - 이미 존재하는 이메일 주소입니다.
      }
    } else if (typeof data === "object") {
      sendEmailVerification(data);
      // 회원 가입 완료하면 이메일 연동 페이지로 보내주기
    }
  },
});

const submitSignUpData = (e: React.FormEvent) => {
  e.preventDefault();
  if (
    signUpData.email.trim() === "" ||
    signUpData.password?.trim() === "" ||
    signUpData.passwordcheck?.trim() === ""
  ) {
    return // 토스트 메시지 1 - 빈칸 있음
  } else if (emailRegex.test(signUpData.email) === false) {
    return // 토스트 메시지 2 - 이메일 양식이 안 맞음
  } else if (
    passwordRegex.test(signUpData.password!) === false ||
    passwordRegex.test(signUpData.passwordcheck!) === false
  ) {
    return // 토스트 메시지 3 - 비밀번호가 양식과 맞지 않음
  } else if (signUpData.password! !== signUpData.passwordcheck!) {
    return // 토스트 메시지 4 - 비밀번호와 확인이 서로 다름
  }

  signupMutation.mutate(signUpData);
};

다른 페이지들도 대체적으로 이런 형태로 구현해나갔다.

이렇게 구현해서 테스트한 결과는 아래와 같다.

물론 이 과정을 진행하면서 순탄지 않은 부분도 당연히 존재했다.

지금부터는 그 부분들을 좀 서술해보고자 한다.

비밀번호 변경과 이메일 인증 페이지를 하나의 페이지로 병합하기

이메일 인증과 비밀번호 분실 페이지는 이메일을 보내 첨부된 링크를 타고 들어오게 된다.

그래서 각 이메일 템플릿별로 내가 사용할 주소에 분리된 라우팅별로 적용하려고 했으나, 안내사항 마크에 적혀있듯이 ‘커스텀 작업 URL은 모든 이메일 템플릿에 적용됩니다.’라는 내용에 따라 하나의 도메인으로 다 통일되어 보내지다 보니, 원하던 그림대로 라우팅을 분리해 사용할 수 없는 문제가 발생했다.

이 부분에 관한 해결을 어떻게 할까 고민하던 중, 받은 이메일들의 양식을 확인해보니 몇 가지 공통적인 쿼리가 존재하고 있는 걸을 확인했다.

이 쿼리를 이용하면 한 페이지에서 상황에 따라 컴포넌트 내용을 다르게 보이게할 수 있지 않을까? 그래서 이 내용을 고려하여 두 페이지의 병합 작업을 시도해봤다.

우선 이전에 사용하던 두 페이지들은 병합을 해야하니 당연히 삭제하고, 각 페이지에 배치된 컴포넌트 내용들을 한 페이지에 전부 옮겼다.

문제는 이제 이메일을 받을 때 현재 타고 들어온 링크가 ‘이메일 인증’인지, ‘비밀번호 변경’인지를 구분시켜주는 것인데, 이전에는 라우팅의 path를 통해서 그 부분을 분리시켜놨는데 현 상황에서는 위에서 언급한 쿼리의 내용을 받아와서 페이지의 내용을 구분시켜줘야 한다.

쿼리의 내용을 받아올 수 있는 방법이 없을까? 찾아보던 중에 아래의 글을 발견했다.

리액트 기초, 쿼리 스트링, useSearchParams

여기서 쓰이는 useSearchParams를 통해서 쿼리의 정보를 받아올 수 있을 것 같았다.

물론, JavaScript의 함수인 URLSearchParams 인스턴스를 써도 된다. 저 React Router의 Hook인 useSearchParams은 이 URLSearchParams를 기반으로 한다. 다만, 해당 내용에서 setState와 같이 쿼리를 쉽게 변경해주는 setParams 기능이 추가되어 있다.

아무튼, URL에 있는 쿼리 중에서 현재 필요한 것은 지금 유저가 들어온 경로가 ‘이메일 인증’인지 ‘비밀번호 변경’인지를 확인해야하는 거였다.

그래서 serachParams를 통해서 쿼리의 정보들을 받아오고, 이 안에 있는 mode라는 key를 통해서 현재 페이지의 상태를 받아지는지 확인해보니, 정상적으로 ‘resetPassword’와 ‘emailverify’가 들어오는 것을 확인했다.

내용이 받아지는지 확인됐으니 남은 건 이 케이스들에 따라 화면이 나오는 걸 다르게 구성해주면 되는 것 뿐이다.

const [searchParams] = useSearchParams();

// 쿼리 안의 mode key의 값이 이메일 인증일 경우
if (searchParams.get("mode") === "verifyEmail") {
  return (<>
    <Heading as="h2" fontSize="1.25rem">
      이메일 인증
    </Heading>
    <Text>
      이메일 인증이 완료됐습니다. 로그인 페이지에서 로그인을 부탁드립니다.
    </Text>
  </>);
// 쿼리 안의 mode key의 값이 비밀번호 변경일 경우
} else if (searchParams.get("mode") === "resetPassword") {
  return (<>
    <Heading as="h1" textAlign="center" marginBottom="2rem">
      웨잇세컨드
    </Heading>
    <Heading as="h2" fontSize="1.25rem">
      비밀번호 변경
    </Heading>
    <Text
      fontSize="1rem"
      lineHeight="1.5rem"
      whiteSpace="pre-wrap"
      textAlign="left"
      letterSpacing="-1px"
      margin="1rem 0"
    >
      새로 변경할 비밀번호를 입력해주세요.
    </Text>
   ...
  </>);
} else {
// 둘 다 아닐 경우 - 잘못된 접근
  return (<>
    <Heading as="h1" textAlign="center" marginBottom="2rem">
      웨잇세컨드
    </Heading>
    <Heading as="h2" fontSize="1.25rem">
      잘못된 접근입니다.
    </Heading>
    <Text>5초 후 로그인 페이지로 이동합니다.</Text>
  </>);
}

위의 내용들을 토대로 구현을 한 뒤, 이제 마지막으로 작동이 잘 되는지를 테스트해봤다.

GIF 2023-02-20 오후 11-44-17.gif

원하는 대로 쿼리의 mode 값에 따라 페이지 내용이 출력되는 것을 확인했다.

난감했던 비밀번호 변경 기능 구현

Firebase 연동을 통해 제일 구현이 힘들었던 건 비밀번호 변경 기능이었다.

이 기능을 사용하기 위해서는 이메일 인증을 통해 메일을 받은 뒤에, 그 메일 안에 있는 링크를 통해서 비밀번호를 수정하는 식으로 진행이 된다.

이야기만 들으면 간단해보이지만, 문제는 ‘링크를 통해서 비밀번호를 수정하는 방식’이었다.

Firebase 공식 문서 내에서 안내하는 기본적인 함수들에서는 updatePassword라는 함수를 통해 비밀번호를 재설정할 수 있었다.

Firebase에서 사용자 관리

그러나, 이 경우에는 user 변수를 통해서 알 수 있듯이 현재 로그인한 유저의 정보가 있어야 한다.

즉, 이 기능은 로그인을 한 유저가 비밀번호를 변경할 때 사용될 수 있는 기능이므로 로그인하지 않은 상태에서 비밀번호를 변경해야하는 현 상황하고는 맞지 않았다.

방법이 없을까 싶어서 이래저래 검색을 하던 와중에, 우선은 요즘 많이 애용되는 ChatGPT에 내용을 물어봤다.

위의 방식을 참고해서 코드를 작성을 시도해봤지만, signInWithEmailLink에서 계정 인증이 제대로 이루어지지 않았다.

인증이 안 됐던 이유는 쿼리로 받아온 oobCode가 없는 코드(Invaild)로 인증 받아져서 발생한 이슈였는데, 왜 없는 코드로 나오는지 확인할 방법이 없어서 (만료됐을 경우에는, 코드가 만료됐다고 나온다.) 몇 번을 시도해보고도 같은 상황이 나오는 걸 확인 후 해당 방법을 폐기하기로 했다.

다시 구글링이나 공식 문서를 통해서 해결책이 없을까? 라며 생각하던 중에, Firebase의 공식 문서에서 아래와 같은 글을 발견하게 됐다.

사용자 지정 이메일 작업 처리기 만들기  |  Firebase

위의 코드 내용을 참고해 아래와 같이 코드를 작성해봤다.

const firebaseAuth = getAuth();
  const actionCode = searchParams.get("oobCode");

  const resetPasswordAccount = async (userData: AdminData) => {
    const resetPasswordState = **verifyPasswordResetCode**(
      firebaseAuth,
      actionCode!
    )
      .then((data) =>
        **confirmPasswordReset**(firebaseAuth, actionCode!, userData.password!)
      )
      .then((data) => console.log("수정된거에요?"))
      .catch((error) => console.log(error.message));
    return resetPasswordState;
  };

  const resetPasswordMutation = useMutation(resetPasswordAccount, {
    onSuccess: (data) => console.log("수정 완료!"),
    onError: (error) => console.log(error),
  });

verifyPasswordResetCode는 보낸 이메일을 통해 들어온 링크에서 oobCode를 통해서 보냈던 이메일의 정보가 맞는지를 체크하는 함수이다.

이 부분은 위쪽의 ChatGPT와 동일한 내용인데, 그 다음에 쓰이는 함수가 달랐다.

ChatGPT는 이메일만 가지고 로그인을 하고, 그 다음에 바뀐 비밀번호를 변경하게 하는 식이었다.

그래서 함수를 signInWithEmailLink을 사용했지만, 위에서 적었다시피 없는 코드라고 나오면서 해당 방법은 사용될 수 없었다.

여기서는 confirmPasswordReset을 사용하여 유저가 입력한 비밀번호로 초기화시켜주는 건데 로그인을 할 필요도 없고 obbCode가 있다면 유저 정보를 식별해줄 수 있다.

위의 방식을 사용해서 테스트를 해본 결과,

GIF 2023-02-21 오전 1-57-47.gif

원하던 그림대로 나와줬다. 구현할 때 이 부분이 정말 애를 많이 먹었는데, 여기까지 하니까 마음이 좀 많이 놓였다.

우선은 이렇게 관리자 기본 입력 사항과 관련한 페이지들의 기능들까지도 구축해냈다.

문맥을 이해하지 못해 혼자서 오해하던 암호화 해프닝

이 내용은 문제 해결이라기 보다는 해프닝에 관련한 이야기이다.

Firebase Auth를 통해서 받아온 값과 관련하여 코딩하는곰 님(@CodingBear03)과 YG 님(@YG1ee)께서 업로드했던 이미지를 보시더니 아래와 같은 문의를 주셨다.

문제의 그 이미지. passwordHash라는 값을 보고 이게 비밀번호라고 생각했다.

비밀번호가 혹시 REDACTED였나요?

어, 아닌데? 무슨 말씀을 하시는거지? 싶어서 물어보니

비밀번호 문자열이 "REDACTED"인지 물어보시는 거 같아요. 해시값으로 디코딩하신 건지? 저도 궁금하네요.

라는 답변을 받았다.

지금 내용을 읽어보니 문해력이 나쁜 나머지 상황을 잘못 오해해서 혼자서 멘붕에 빠진 상황을 초래했던 것 같은데.. 아무튼, 저 내용을 볼 당시에는 REDACTED라는 단어만 보고 이게 무슨 상황인지 파악이 안되어서 당황해했다.

저게 해시가 아니라 Base64된 값이라 그냥 쓰시면 위험하긴 합니다.

맙소사. 그럼 이 문제를 어떻게 해결해야하나, 암호화와 복화화의 개념을 하나도 모르는데..

갑자기 눈 앞이 확 까매지고 뒷골이 팍 땡기는 느낌이 왔다. 스트레스의 징조라 어쩔 수 없이 잠시 손을 놓고 부족한 수면을 채워 좀 가라앉힌 뒤 다시 컴퓨터 앞에 앉아 방법이 없는지를 다시금 좀 조사해봤다.

근데 암호화와 복호화를 이해하기에는 너무 배울 양이 많고 당장 이걸 감당할 자신이 없어서, 우선은 개인 프로젝트이니까 Firebase가 암호화해줄 수 있는지, 그 방법이라도 알 수 있음 좋겠다 싶어 이것저것 검색을 해봤다.

그러다 발견한 공식 문서의 내용이 있었는데, 거기에서는 아래와 같은 내용이 담겨있었다.

사용자 가져오기  |  Firebase 인증

결과적으로, Firebase에서는 기본적으로 SCRYPT라는 알고리즘을 통해서 비밀번호를 암호화하여 보내주는 거 같다.

만약 백엔드가 있다면 이제 이것을 hash_config라는 설정을 통해서 알고리즘 등을 변경해줄 수 있어보이는데.. 프론트 입장에서도 이 설정을 건들 수 있다면 알고리즘이 다른 걸로 적용될 수 있지 않을까 싶지만, 일단 암호화를 해준다는 걸 아는 이상 당장 이걸 건드릴 이유는 없다고 생각한다.

그럼 왜 저 이야기에서 혼란을 겪었던 걸까? 왜 그렇게 멘붕에 빠졌던 걸까? 한 친구가 그 과정을 보고 있었는지 아래와 같이 정리해줬다.

  1. 그냥 고정값으로 항상 들어가있는걸 패스워드라고 착각하고 올림

  2. 코곰님도 그 값이 패스워드라고 생각하고 base64 decode해보니까 REDACTED라고 나와서 뭔가 잘못된거같다고 함

결론: 두 사람 다 지금 이상한 값 보고 있는 거 같음

요즘 새벽에 잠이 드는 일이 많아 그런지 뇌가 마비됐나보다...

그래도 문제가 없다는 걸 알았으니 다행이기도 하고, 파이어베이스에서는 어떤 알고리즘을 통해서 비밀번호를 암호화하는지 알게 됐으니 좋은 공부였다고 생각하기로 했다.

이 자리를 빌어 문맥 파악을 못해서 사오정처럼 이야기하고 있었음에도(…) 도움을 주신 코딩하는곰 (@CodingBear03)님과 YG (@YG1ee)님, 빵냥(@breadcat_nyang)님과 경단(@dango_undefined)에게 감사의 말씀을 전한다.

다음 주에 할 일들

기능들이 붙여지는 것도 어느 정도 틀이 잡혀져가게 됐고, 상태 관리도 적극적으로 쓰기 시작했으니 이제 관리자 관련 메인 기능들의 뷰를 구현할 때인 것 같다.

그 밖에도 현재 페이지들에 Toast 메시지나 라우팅 연결 등이 아직 진행되지 않았던 것도 있으므로 이번주는 자잘한 사항들도 작업을 추가해보려고 한다.

  • 관리자 페이지 View 구현 진행
    • 현재 대기 상황, 상점 관리, 설정, 비밀번호 변경, 회원 탈퇴 등
  • 폰트, 공용 색상 등 선정
    • 프리텐다드로 폰트 적용했는데, 제대로 적용되지 않는 듯하여 다시 확인해봐야겠다.
    • 색상은 Adobe Color에서 찾아보기로.
      • 이왕이면 색상에 의미를 부여하고 싶다.
  • 각 페이지별 라우팅 연결 등 설정
    • 일부 페이지에서는 다른 페이지로 넘어가져야하는 내용들이 있어서 그 내용들을 배치해보고자 한다.
  • 인증 문제 등 발생 시 출력할 Toast 메시지 각 페이지 별로 배치
    • 현재는 임시로 console.log()를 적어놨는데 이번 기회에 다 수정하기로.

다음 주는 2월 기간 중에 비교적 시간이 비어있는 한 주가 될 것이다. 그리고 그렇게 뵙고 싶었던 한 분과의 커피챗도 있어서 좀 기대가 된다.

스토리북의 사용이나 의존성에 관한 부분부터 시작해서 좀 더 개발 외적의 잡담도 나눠보고 싶어서 좋은 시간이 됐으면 한다.

참고 자료

useMutation

react-query 개념 및 정리

[React] React Query의 useMutation에 대해 알아보기

리액트 기초, 쿼리 스트링, useSearchParams

Firebase에서 사용자 관리

사용자 지정 이메일 작업 처리기 만들기  |  Firebase

사용자 가져오기  |  Firebase 인증

com.google.firebase.auth.hash  |  Firebase

Firebase authentication: What do the scrypt password hash parameters mean?