비밀번호 찾기 및 재설정 처리

우리의 서버리스 노트 앱에서는 사용자 가입 및 로그인을 위해 Cognito 사용자 풀을 사용했습니다. 프론트엔드에서는 React 앱에서 AWS Amplify를 사용했습니다. 그러나 사용자가 비밀번호를 잊어버린 경우, 비밀번호를 재설정할 수 있는 방법이 필요합니다. 이번 장에서는 이를 어떻게 구현하는지 살펴보겠습니다.

이 장에서 사용된 노트 앱 버전은 별도의 GitHub 저장소에 호스팅되어 있습니다: https://github.com/AnomalyInnovations/serverless-stack-demo-user-mgmt-client.

사용자가 비밀번호를 재설정할 수 있도록 하기 위해 필요한 주요 변경 사항을 살펴보겠습니다.

비밀번호 재설정 폼 추가하기

Change indicator src/containers/ResetPassword.js 파일을 생성합니다.

import React, { useState } from "react";
import { Auth } from "aws-amplify";
import { Link } from "react-router-dom";
import {
  FormText,
  FormGroup,
  FormControl,
  FormLabel,
} from "react-bootstrap";
import { BsCheck } from "react-icons/bs";
import LoaderButton from "../components/LoaderButton";
import { useFormFields } from "../lib/hooksLib";
import { onError } from "../lib/errorLib";
import "./ResetPassword.css";

export default function ResetPassword() {
  const [fields, handleFieldChange] = useFormFields({
    code: "",
    email: "",
    password: "",
    confirmPassword: "",
  });
  const [codeSent, setCodeSent] = useState(false);
  const [confirmed, setConfirmed] = useState(false);
  const [isConfirming, setIsConfirming] = useState(false);
  const [isSendingCode, setIsSendingCode] = useState(false);

  function validateCodeForm() {
    return fields.email.length > 0;
  }

  function validateResetForm() {
    return (
      fields.code.length > 0 &&
      fields.password.length > 0 &&
      fields.password === fields.confirmPassword
    );
  }

  async function handleSendCodeClick(event) {
    event.preventDefault();

    setIsSendingCode(true);

    try {
      await Auth.forgotPassword(fields.email);
      setCodeSent(true);
    } catch (error) {
      onError(error);
      setIsSendingCode(false);
    }
  }

  async function handleConfirmClick(event) {
    event.preventDefault();

    setIsConfirming(true);

    try {
      await Auth.forgotPasswordSubmit(
        fields.email,
        fields.code,
        fields.password
      );
      setConfirmed(true);
    } catch (error) {
      onError(error);
      setIsConfirming(false);
    }
  }

  function renderRequestCodeForm() {
    return (
      <form onSubmit={handleSendCodeClick}>
        <FormGroup bsSize="large" controlId="email">
          <FormLabel>Email</FormLabel>
          <FormControl
            autoFocus
            type="email"
            value={fields.email}
            onChange={handleFieldChange}
          />
        </FormGroup>
        <LoaderButton
          block
          type="submit"
          bsSize="large"
          isLoading={isSendingCode}
          disabled={!validateCodeForm()}
        >
          인증 코드 보내기
        </LoaderButton>
      </form>
    );
  }

  function renderConfirmationForm() {
    return (
      <form onSubmit={handleConfirmClick}>
        <FormGroup bsSize="large" controlId="code">
          <FormLabel>인증 코드</FormLabel>
          <FormControl
            autoFocus
            type="tel"
            value={fields.code}
            onChange={handleFieldChange}
          />
          <FormText>
            이메일({fields.email})로 전송된 인증 코드를 확인해 주세요.
          </FormText>
        </FormGroup>
        <hr />
        <FormGroup bsSize="large" controlId="password">
          <FormLabel>새 비밀번호</FormLabel>
          <FormControl
            type="password"
            value={fields.password}
            onChange={handleFieldChange}
          />
        </FormGroup>
        <FormGroup bsSize="large" controlId="confirmPassword">
          <FormLabel>비밀번호 확인</FormLabel>
          <FormControl
            type="password"
            value={fields.confirmPassword}
            onChange={handleFieldChange}
          />
        </FormGroup>
        <LoaderButton
          block
          type="submit"
          bsSize="large"
          isLoading={isConfirming}
          disabled={!validateResetForm()}
        >
          확인
        </LoaderButton>
      </form>
    );
  }

  function renderSuccessMessage() {
    return (
      <div className="success">
        <p><BsCheck size={16} /> 비밀번호가 재설정되었습니다.</p>
        <p>
          <Link to="/login">
            새로운 비밀번호로 로그인하려면 여기를 클릭하세요.
          </Link>
        </p>
      </div>
    );
  }

  return (
    <div className="ResetPassword">
      {!codeSent
        ? renderRequestCodeForm()
        : !confirmed
        ? renderConfirmationForm()
        : renderSuccessMessage()}
    </div>
  );
}

여기서의 흐름을 간단히 살펴보겠습니다:

  • renderRequestCodeForm()에서 사용자에게 계정의 이메일 주소를 입력하도록 요청합니다.
  • 사용자가 폼을 제출하면 Auth.forgotPassword(fields.email)을 호출하여 프로세스를 시작합니다. 여기서 Auth는 AWS Amplify 라이브러리의 일부입니다.
  • 이렇게 하면 Cognito가 지정된 이메일 주소로 인증 코드를 보냅니다.
  • 그런 다음 사용자가 Cognito에서 보낸 코드를 입력할 수 있는 폼을 보여줍니다. 이 폼은 renderConfirmationForm()에서 렌더링되며, 사용자가 새 비밀번호를 입력할 수도 있습니다.
  • 사용자가 코드와 새 비밀번호를 입력하고 폼을 제출하면 Auth.forgotPasswordSubmit(fields.email, fields.code, fields.password)를 호출합니다. 이렇게 하면 계정의 비밀번호가 재설정됩니다.
  • 마지막으로, 비밀번호가 성공적으로 재설정되었다는 메시지를 보여주고, 새로운 비밀번호로 로그인할 수 있는 로그인 페이지로 연결합니다.

몇 가지 스타일도 추가해 보겠습니다.

Change indicator src/containers/ResetPassword.css에 다음 내용을 추가합니다.

@media all and (min-width: 480px) {
  .ResetPassword {
    padding: 60px 0;
  }

  .ResetPassword form {
    margin: 0 auto;
    max-width: 320px;
  }

  .ResetPassword .success {
    max-width: 400px;
  }
}

.ResetPassword .success {
  margin: 0 auto;
  text-align: center;
}
.ResetPassword .success .glyphicon {
  color: grey;
  font-size: 30px;
  margin-bottom: 30px;
}

라우트 추가하기

마지막으로, 이 부분을 앱의 나머지 부분과 연결해 보겠습니다.

Change indicator src/Routes.js에 라우트를 추가합니다.

<Route
  path="/login/reset"
  element={
    <UnauthenticatedRoute>
      <ResetPassword />
    </UnauthenticatedRoute>
  }
/>

Change indicator 그리고 헤더에서 해당 컴포넌트를 임포트합니다.

import ResetPassword from "./containers/ResetPassword";

로그인 페이지에서 링크 추가하기

이제 사용자가 로그인을 시도할 때 이 페이지로 이동할 수 있도록 해야 합니다.

Change indicator src/containers/Login.js에 링크를 추가해 보겠습니다. 로그인 버튼 위에 추가합니다.

<Link to="/login/reset">비밀번호를 잊으셨나요?</Link>

Change indicator 그리고 헤더에 Link 컴포넌트를 임포트합니다.

import { Link } from "react-router-dom";

Change indicator 마지막으로 src/containers/Login.css에 다음 스타일을 추가하여 링크에 스타일을 적용합니다.

.Login form a {
  margin-bottom: 15px;
  display: block;
  font-size: 14px;
}

이제 끝났습니다! 이제 /login/reset로 이동하거나, 비밀번호를 재설정해야 할 때 로그인 페이지에서 이 링크를 통해 이동할 수 있습니다.

로그인 페이지 비밀번호 찾기 링크 스크린샷

여기서 사용자는 이메일을 입력해 비밀번호를 재설정할 수 있습니다.

비밀번호 찾기 페이지 스크린샷

다음으로, 로그인한 사용자가 비밀번호를 변경하는 방법을 살펴보겠습니다.