이메일 인증
버튼을 누르면 서버에게 데이터를 보내도록 하였다
보내야하는 데이터는 사용자의 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 해주면 된다.
//인증번호 입력 컴포넌트
...
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에 원하는 데이터(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
이름 : 홍길동
@로 다시 보이게 하기 위해 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=홍길동
import { useSearchParams } from 'next/navigation';
어떻게 ?
useSearchParams
를 사용하자.
const searchParams = useSearchParams();
const userName = searchParams.get('name');
이렇게 하면 url에 있는 '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에 데이터를 담아 라우팅을 하면 원하는대로 바로 담기지 않는 다는것을 알게되었다.
데이터를 담아 보내면 암호화되고, 이를 디코딩해주어야한다는 것을 배웠다.
데이터를 담고 전달하는 과정에서 불필요한 코드가 담기진 않았을까 걱정이된다.
(나는 필요하다고 생각하여 넣은 코드들이 불필요한 경우)
더 많이 학습하고 반복하며 사용해봐야겠다.