✨[Next13 + TypeScript] 비밀번호 재설정 _ 이름, 이메일, 인증코드 axios 연결 && URL에 데이터 (이메일, 이름) 담아서 동적라우팅

ezi·2023년 9월 8일
0

✨ 중요한 부분, 자식 컴포넌트에서 부모컴포넌트로 데이터 전달

이메일 인증 버튼을 누르면 서버에게 데이터를 보내도록 하였다

보내야하는 데이터는 사용자의 name , email 이다.

그런데 여기서 무엇이 중요하다고 한 것이냐면,

이름을 입력받는 곳은 PasswordEmailTextField ,

이메일을 입력받는 곳은 PasswordCodeTextField 인데

PasswordCodeTextField 에서 이름과 이메일을 저장해서 axios 요청을 해야 한다는 것이다.

이게 뭐가 어렵냐고 느낄 수 있지만

처음에 난 매우 어려웠다..

자식 컴포넌트에서 입력 받은 데이터를 다시 부모컴포넌트에게 전달하고
이를 또 다른 자식컴포넌트에게 전달하여 사용한다 .. 어떻게 ??

먼저 이름을 입력 받는 컴포넌트(자식 컴포넌트 1) 부터 살펴보자,

import { UseFormRegisterReturn } from 'react-hook-form';
import { TextFieldInput, TextFieldTitle, TextFieldWrap } from './styled';
import { ChangeEvent, useEffect, useState } from 'react';

interface TextFieldProps {
  title: string;
  type: string;
  placeholder: string;
  value: string;
  register: UseFormRegisterReturn;
  onChange: (value: string) => void;
}

const TextField = ({
  title,
  type,
  placeholder,
  value,
  register,
  onChange = () => null,
}: TextFieldProps) => {
  const [name, setName] = useState(value);

  const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
    const inputValue = event.currentTarget.value;
    setName(inputValue);
  };

  useEffect(() => {
    onChange(name);
  }, [name, onChange]);

  return (
    <TextFieldWrap>
      <TextFieldTitle>{title}</TextFieldTitle>
      <TextFieldInput
        type={type}
        placeholder={placeholder}
        onChange={handleInputChange}
      />
    </TextFieldWrap>
  );
};
export default TextField;

입력 받을 땐 html의 input의 변화를 감지하고 이를 name이라는 변수에 저장해주는 함수를 만들고 (handleInputChange) ,

이 함수를 input 태그에서 onChange={handleInputChange} 로 이용한다.

const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
    const inputValue = event.currentTarget.value;
    setName(inputValue);
  };


 return (
    <TextFieldWrap>
      <TextFieldTitle>{title}</TextFieldTitle>
      <TextFieldInput
   
        onChange={handleInputChange}
        
      />
    </TextFieldWrap>
  );

여기까지 하면, 입력받은 값을 name 변수에 저장하기 성공

그럼 이제 어떻게 name 을 부모컴포넌트로 전달할 것인가?

  useEffect(() => {
    onChange(name);
  }, [name, onChange]);

이 부분이 name을 부모컴포넌트에 전달하도록 도와준다.

name과 onChange가 useEffect의 의존성 배열로 지정되어 있다.

이것은 name 또는 onChange 중 하나라도 변경될 때 useEffect 함수 내의 코드 블록을 실행하도록 만든다.

useEffect 내의 코드 블록은 onChange(name) 함수를 호출한다.

이 함수는 name 값을 매개변수로 받아서 실행된다.

즉,

useEffect는 name이나 onChange 중 하나라도 변경되면 onChange(name) 함수를 호출한다.

이로써 name 상태 값이 변경될 때마다 onChange 함수가 실행되어 TextField 컴포넌트의 상위 컴포넌트에서 이벤트를 처리하거나 상태를 업데이트할 수 있도록 한다.

이런 방식으로 useEffect는 컴포넌트 내에서 상태나 함수의 변경에 따른 부수적인 작업을 수행할 때 사용된다.

name 상태 값이 변경될 때 onChange 함수를 호출하여 해당 값의 업데이트를 상위 컴포넌트에 알리는 역할을 한다.

자식컴포넌트에 전달한 데이터를 부모컴포넌트에서 어떻게 받고 사용할 것인가?

이제 부모 컴포넌트를 살펴보자,

'use client';
...

const PasswordFind = () => {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  ...

  const handleNameChange = (value: string) => {
    setName(value);
    // console.log('name:', name);
  };

  const handleEmailChange = (value: string) => {
    setEmail(value);
    // console.log('email:', email);
  };

 ...
  return (
    <>
      <PasswordFindCotainer>
        <PasswordFindTitle>비밀번호 찾기</PasswordFindTitle>
        <PasswordFindInputWrap>
          <TextField
            title="이름"
            placeholder="이름을 입력해주세요"
            onChange={handleNameChange}
          />
          <PasswordEmailTextField
            title="이메일"
            category="email"
            buttonText="이메일 인증"
            placeholder="이메일을 입력해주세요"
            required="올바른 이메일 형식으로 입력해주세요"
            onChange={handleEmailChange}
            name={name}
          />
          <PasswordCodeTextField
            title="인증번호"
            category="number"
            buttonText="확인"
            placeholder="인증 번호를 입력해주세요"
            required="숫자만 입력해주세요"
            email={email}
            onConfirmationChange={handleConfirmationChange}
          />
        </PasswordFindInputWrap>
        ...
      </PasswordFindCotainer>
    </>
  );
};
export default PasswordFind;

코드 중 이 부분들이 name 을 부모컴포넌트에 저장하도록 해준다.


  const handleNameChange = (value: string) => {
    setName(value);
    // console.log('name:', name);
  };

----
	 <TextField
            title="이름"
            placeholder="이름을 입력해주세요"
            onChange={handleNameChange}
          />

🍯 이메일 인증 버튼을 눌렀을 때, post 해주기

 const generateAuthPwd = () => {
    instance
      .post(`/api/v1/user/email/auth/pwd/generate`, {
        email,
        name,
      })
      .then((response) => {
        console.log('이메일 전송 완료', response);
        console.log('email:', email);

        if (response.data.data == '이메일 전송 성공') {
          setEmailAthnt(true);
          console.log('emailAthnt', emailAthnt);
          showEmailModal();
        }
        if (response.data.data == '이메일이 유효하지 않습니다.') {
          showEmailFailModal();
          setEmailAthntFail(true);
          console.log('emailAthntFail:', emailAthntFail);
        }
      })
      .catch((error) => {
        console.log(error, '실패하였습니다');
      });
  };


return (
    <>
      <TextBtnFieldWrap>
        <TextBtnFieldTitle>{title}</TextBtnFieldTitle>
        <TextBtnWrap>
         ...
          <BtnWrap>
            <Btn
              text={buttonText}
              size="small"
              state={buttonColor}
              disabled={!isValid || !hasValue}
              onClick={generateAuthPwd}
            />
            {emailAthnt && (
              <EmailModal isOpen={emailModal} isClose={closeEmailModal} />
            )}
            {emailAthntFail && (
              <EmailFailModal
                isOpen={emailFailModal}
                isClose={closeEmaiFaillModal}
              />
            )}
          </BtnWrap>
        </TextBtnWrap>
        {!isValid && isEdited && <FieldRequired>{required}</FieldRequired>}
      </TextBtnFieldWrap>
    </>
  );

백엔드의 메세지에 맞게 이메일 전송 성공이면, 전송 성공 모달창을 띄우는 등 구현하였다.

🍯 이메일과 인증코드로 post 하기

이메일 인증 버튼을 눌러 이메일로 전송된 인증코드를 입력하고

[ 이메일 + 인증코드 ] 을 서버로 post 해주고,

결과에 따라 (성공 || 실패) 처리하기

인증코드를 입력받는 부분도 이름, 이메일을 입력하는 부분과 마찬가지로 컴포넌트가 따로 존재한다.

필요한 건 이메일 데이터이다.

이름을 자식에서 부모 컴포넌트로 전달한 것 처럼 이메일도 동일하게 처리해주면 된다.

전달 과정 :

이메일 입력 컴포넌트 -> 부모 컴포넌트 ( 이메일 데이터 존재 ) -> 인증번호 입력 컴포넌트

인증번호 입력 컴포넌트로 전달된 이메일 데이터를 사용하여 여기서

[ 이메일 + 인증번호 ] 를 서버에 post 해주면 된다.

//인증번호 입력 컴포넌트
...

interface TextBtnFieldProps {
  title: string;
  type: string;
  placeholder: string;
  required?: string;
  buttonText: string;
  category: 'number';
  onValueChange?: (value: string) => void;
  onClick: () => void;
  email: string;
  onConfirmationChange: (isValid: boolean) => void;
}

const PasswordCodeTextField = ({
  title,
  type,
  placeholder,
  required,
  buttonText,
  category,
  onValueChange,
  onClick,
  email,
  onConfirmationChange,
}: TextBtnFieldProps) => {
  const [authCode, setAuthCode] = useState('');
  const [codeAthnt, setCodeAthnt] = useState(false);
  const [codeAthntFail, setCodeAthntFail] = useState(false);
  const [codeModal, setCodeModal] = useState<boolean>(false);
  const [codeFailModal, setCodeFailModal] = useState<boolean>(false);
  const [isValid, setIsValid] = useState(false);
  const [isEdited, setIsEdited] = useState(false);
  const [hasValue, setHasValue] = useState(false);
  const [buttonColor, setButtonColor] = useState<'white' | 'orange'>('white');

  useEffect(() => {
    if (isValid && hasValue) {
      setButtonColor('orange');
    } else {
      setButtonColor('white');
    }
  }, [isValid, hasValue]);

  useEffect(() => {
    console.log('email', email);
  }, [email]);

  const showCodeModal = () => {
    setCodeModal(true);
  };

  const closeCodeModal = () => {
    setCodeModal(false);
  };

  const showCodeFailModal = () => {
    setCodeFailModal(true);
  };

  const closeCodeFailModal = () => {
    setCodeFailModal(false);
  };

  const generateAuthPwd = () => {
    instance
      .post(`/api/v1/user/email/auth/pwd`, {
        authCode,
        email,
      })
      .then((response) => {
        console.log('코드전송완료', response);
        console.log('email:', email, 'authCode:', authCode);

        if (response.data.data == true) {
          onConfirmationChange(true);
          setCodeAthnt(true);
          console.log('codeAthnt:', codeAthnt);
          showCodeModal();
        }
        // if (response.data.data == '404') {
        else {
          showCodeFailModal();
          setCodeAthntFail(true);
          console.log('codeAthntFail:', codeAthntFail);
        }
      })
      .catch((error) => {
        console.log(error, '실패하였습니다');
      });
  };

  const validateInput = (value: string, validationCategory: 'number') => {
    if (validationCategory === 'number') {
      const numericRegex = /^(|[0-9]*)$/;
      return numericRegex.test(value);
    }
    return false;
  };

  const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
    const inputValue = event.currentTarget.value;
    const isInputValid = validateInput(inputValue, category);
    setIsValid(isInputValid);
    setIsEdited(true);
    setHasValue(inputValue.length > 0);
    setAuthCode(inputValue);

    if (category === 'number' && inputValue.length === 0) {
      setIsValid(false);
    }

    if (onValueChange) {
      onValueChange(inputValue);
    }
  };

  return (
    <>
      <TextBtnFieldWrap>
        <TextBtnFieldTitle>{title}</TextBtnFieldTitle>
        <TextBtnWrap>
          <TextBtnFieldInput
            type={type}
            placeholder={placeholder}
            isValid={isValid}
            onChange={handleInputChange}
          />
          <BtnWrap>
            <Btn
              text={buttonText}
              size="small"
              state={buttonColor}
              disabled={!isValid || !hasValue}
              onClick={generateAuthPwd}
            />
            {codeAthnt && (
              <CodeModal isOpen={codeModal} isClose={closeCodeModal} />
            )}
            {codeAthntFail && (
              <CodeFailModal
                isOpen={codeFailModal}
                isClose={closeCodeFailModal}
              />
            )}
          </BtnWrap>
        </TextBtnWrap>
        {!isValid && isEdited && <FieldRequired>{required}</FieldRequired>}
      </TextBtnFieldWrap>
    </>
  );
};
export default PasswordCodeTextField;

✨ 중요한 부분, URL에 데이터 (이메일, 이름) 담아서 동적라우팅

동적 라우팅 사용법은 링크를 클릭하여 보길 추천한다.

이메일 인증, 인증 번호 인증이 완료되면

비밀번호 찾기 하단에 있는 완료 버튼이 활성화 되며

클릭 시 비밀번호 재설정 페이지로 라우팅 되게 된다.

부모, 자식 컴포넌트로 데이터를 전달하는 게 아닌
컴포넌트 -> 컴포넌트로 데이터를 전달해야 한다.
이것도 그러니까 어떻게 하는데 ..?

먼저 비밀번호 찾기 페이지에서 완료 버튼을 누르면 비밀번호 재설정 페이지로 라우팅 하도록 함수를 적용시켰다.

라우팅하면서 URL에 원하는 데이터(name, email) 가 담기도록 하였다.


  const handleRoute = () => {
    router.push(
      `/passwordfind/passwordreset/${email}?name=${name}`
    );
  };


 <Btn
            size="big"
            text="완료"
            disabled={isDisabled}
            state={btnState}
            onClick={() => {
              if (isConfirmationValid) {
                handleRoute();
              }
            }}
          />

이메일 : hong123@gmail.com
이름 : 홍길동

문제점 1) 이렇게 데이터를 입력 후 라우팅을 한다고 하면, URL의 이메일에 포함된 "@"가 "%40" 으로 보인다.

@로 다시 보이게 하기 위해 decodeURIComponent 를 사용해주어야 한다.


비밀번호 재설정 페이지 전체 코드

'use client';
..

const PasswordResest = ({ email }: { email: string }) => {
  
  const [password, setPassword] = useState('');

  const searchParams = useSearchParams();
  const userName = searchParams.get('name');
  const decodedEmail = decodeURIComponent(email || '').replace('%40', '@');

  const handleNewPasswordChange = (value: string) => {
    setPassword(value);
  };

  const handleConfirmationChange = (isValid: boolean) => {
    setIsConfirmationValid(isValid);
  };

  useEffect(() => {
    const decodedName = decodeURIComponent(userName || '');
    email = decodedEmail;
    console.log('email:', email, 'userName:', userName);
  }, [email, userName]);

  const reset = () => {
    instance
      .patch(`/api/v1/user/password/${email}`, { email, password, userName })
      .then((response) => {
        OpenModal();
        setCompelteReset(true);
      })
      .catch((error) => {
        console.log(error, '실패하였습니다');
      });
  };

  return (
    <>
      <PasswordFindCotainer>
        <PasswordFindTitle>비밀번호 재설정</PasswordFindTitle>
        <PasswordFindInputWrap>
          <ResetTextField
            title="새 비밀번호"
            placeholder="영문 소문자 + 숫자 + 기호 조합 8자 이상으로 입력해주세요"
            required="영문 소문자 + 숫자 + 기호 조합 8자 이상으로 입력해주세요"
            category="newpassword"
            onChange={handleNewPasswordChange}
          />
          <CheckResetTextField
            title="새 비밀번호 확인"
            placeholder="동일한 비밀번호를 입력해주세요"
            required="동일한 비밀번호를 입력하세요"
            category="checknewpassword"
            newPw={password}
            onConfirmationChange={handleConfirmationChange}
          />
        </PasswordFindInputWrap>
        <PasswordFindBtnWrap>
          <Btn
            size="big"
            text="완료"
            disabled={isDisabled}
            state={btnState}
            onClick={reset}
          />
          {completeReset && (
            <CompleteResetModal isOpen={modal} isClose={CloseModal} />
          )}
        </PasswordFindBtnWrap>
      </PasswordFindCotainer>
    </>
  );
};
export default PasswordResest;

위의 코드에서 이 부분이 @ 가 제대로 보일 수 있도록 디코드 해준다.

const decodedEmail = decodeURIComponent(email || '').replace('%40', '@');

URL 결과 :

http://localhost:3000/passwordfind/passwordreset/hong123@gmail.com?name=홍길동

이름과 이메일을 axios post를 해주기 위해선, URL에 담긴 이름과 이메일 데이터를 가져와야한다.

import { useSearchParams } from 'next/navigation';

어떻게 ?

useSearchParams 를 사용하자.

  const searchParams = useSearchParams();
  const userName = searchParams.get('name');

이렇게 하면 url에 있는 'name' 값 가져오기 성공

문제점 2) 가져온 name의 값이 한글이 아니다. 이상한 문자로 되어있다.

decodeURIComponent 를 사용하여 이름을 디코딩 해주자.

 useEffect(() => {
    const decodedName = decodeURIComponent(userName || '');
    email = decodedEmail;
  }, [email, userName]);

이 useEffect 훅은 email과 userName이 변경될 때마다 실행된다.

useEffect의 의존성 배열 [email, userName]은 이 두 상태 값에 의존하고 있으므로,

두 값 중 하나라도 변경되면 내부 코드 블록이 실행된다.

코드 블록 내부에서는 decodeURIComponent 함수를 사용하여 userName을 디코딩하고, decodedEmail 변수에 이미 디코딩 된 이메일 값을 대입한다.

이제, 제대로 만든 이메일과 이름을 서버에 post 해주면 완료.


배운점

1) 단순히 부모컴포넌트에서 자식 컴포넌트로만 데이터를 담아 전달할 수 있다고만 생각했다.

하지만 자식에서 받은 데이터를 상위컴포넌트에서도 사용할 수 있다는 것을 배웠다.

또한 useEffect 의 사용법에 대해서 더 깊게 알 수 있었다.

2) url에 데이터를 담아 라우팅을 하면 원하는대로 바로 담기지 않는 다는것을 알게되었다.

데이터를 담아 보내면 암호화되고, 이를 디코딩해주어야한다는 것을 배웠다.

아쉬운 점

데이터를 담고 전달하는 과정에서 불필요한 코드가 담기진 않았을까 걱정이된다.

(나는 필요하다고 생각하여 넣은 코드들이 불필요한 경우)

더 많이 학습하고 반복하며 사용해봐야겠다.

profile
차곡차곡

0개의 댓글