[개인 포트폴리오] [트위터 클론 SNS Deli] 분석 Part. 9 'create-account.tsx' & 'login.tsx'
2024. 7. 8. 17:08ㆍ웹개발 포트폴리오
728x90
반응형
create-account.tsx
import { createUserWithEmailAndPassword, updateProfile } from "firebase/auth";
import logo from "/logo.svg"
import { auth } from "../firebase";
import { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import {
Error,
} from "../component/auth-components";
import { FirebaseError } from "firebase/app";
import GithubButton from "../component/gitdub-btn";
import { styled } from "styled-components";
import Footer from "../component/Footer";
import { media } from "../media-query-file";
// 에러 메시지 처리 객체
const errorMessages = {
"auth/email-already-in-use": "이미 사용 중인 이메일입니다.",
"auth/invalid-email": "유효하지 않은 이메일 주소입니다.",
"auth/operation-not-allowed": "이 작업은 허용되지 않습니다.",
"auth/weak-password": "비밀번호가 너무 약합니다.",
"default": "계정 생성 중 오류가 발생했습니다. 다시 시도해 주세요."
};
// 스타일 컴포넌트 - Switcher 스타일 정의
const Switcher = styled.p`
min-width: 0;
font-size: 1rem;
margin-top: 10px;
a {
color: #9d7ff5;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
`;
// 스타일 컴포넌트 - 로고 이미지 스타일 정의
const Logo = styled.img`
margin-top: 20px;
width: 400px;
height: auto;
`;
// 스타일 컴포넌트 - Input 필드 스타일 정의
const Input = styled.input`
padding: 10px 20px;
border-radius: 50px;
border-color: #9D80F5;
font-size: 16px;
font-family: pretendard-regular;
width: 600px !important;
align-items: center;
justify-content: center !important;
&[type="submit"] {
background-color: #9d7ff5;
color: white;
cursor: pointer;
&:hover,
&:active {
opacity: 0.9;
}
}
@media (max-width: 1200px) {
width: 100% !important;
}
@media (max-width: 768px) {
width: 90% !important;
}
@media (max-width: 480px)
width: 90% !important;
}
`;
// 스타일 컴포넌트 - 전체 래퍼 스타일 정의
const Wrapper = styled.div`
margin: 0 auto;
padding: 0 20px;
max-width: 1200px;
display: flex;
flex-direction: column;
align-items: center;
flex-wrap: wrap;
${media.mobile(`
body {
padding: 0 10px !important;
}
`)}
${media.tablet(`
body {
padding: 0 30px !important;
}
`)}
${media.desktop(`
body {
padding: 0 50px !important;
}
`)}
`;
// 스타일 컴포넌트 - 폼 스타일 정의
const Form = styled.form`
margin-top: 30px;
margin-bottom: 10px;
display: flex;
flex-direction: column;
gap: 10px;
align-items: center;
justify-content: center !important;
width: 1000px;
@media (max-width: 1200px) {
width: 100% !important;
}
@media (max-width: 768px) {
width: 90% !important;
}
@media (max-width: 480px)
width: 80% !important;
}
`;
// CreateAccount 컴포넌트 정의
export default function CreateAccount() {
const navigate = useNavigate(); // 페이지 이동을 위한 네비게이트 훅
const [isLoading, setLoading] = useState(false); // 로딩 상태 관리
const [name, setName] = useState(""); // 사용자 이름 상태 관리
const [email, setEmail] = useState(""); // 이메일 상태 관리
const [password, setPassword] = useState(""); // 비밀번호 상태 관리
const [error, setError] = useState(""); // 에러 메시지 상태 관리
// Input 필드 값이 변경될 때 호출되는 함수
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
if (name === "name") setName(value);
else if (name === "email") setEmail(value);
else if (name === "password") setPassword(value);
};
// 폼 제출 시 호출되는 함수
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setError("");
if (!name || !email || !password) return; // 모든 필드가 채워지지 않으면 종료
try {
setLoading(true); // 로딩 상태 설정
const credentials = await createUserWithEmailAndPassword(auth, email, password); // Firebase를 통한 계정 생성
await updateProfile(credentials.user, { displayName: name }); // 사용자 프로필 업데이트
navigate("/"); // 메인 페이지로 이동
// Firebase 인증 과정에서 발생할 수 있는 오류를 처리하고, 해당 오류에 맞는 에러 메시지를 설정하는 부분입니다. 여기서 e는 예외 객체를 나타내며, FirebaseError 클래스의 인스턴스인 경우 특정 오류 코드에 맞는 메시지를 설정
} catch (e) {
if (e instanceof FirebaseError) {
setError(errorMessages[e.code] || errorMessages["default"]); // 에러 메시지 설정
}
} finally {
setLoading(false); // 로딩 상태 해제
}
};
// 컴포넌트 렌더링
return (
<Wrapper>
<Logo src={logo} alt="Logo" />
<Form onSubmit={onSubmit}>
<Input
onChange={onChange}
name="name"
value={name}
placeholder="이름"
type="text"
required
/>
<Input
onChange={onChange}
name="email"
value={email}
placeholder="이메일"
type="email"
required
/>
<Input
onChange={onChange}
name="password"
value={password}
placeholder="패스워드"
type="password"
required
/>
<Input
type="submit"
value={isLoading ? "로딩중..." : "계정 만들기"}
/>
</Form>
{error && <Error>{error}</Error>} {/* 에러 메시지 표시 */}
<Switcher>
<Link to="/login"> 이미 계정이 있나요? → </Link>
</Switcher>
<GithubButton />
<Footer />
</Wrapper>
);
}
이 코드는 Firebase를 사용하여 사용자 계정을 생성하는 React 컴포넌트입니다. 사용자가 이름, 이메일, 비밀번호를 입력하여 계정을 생성할 수 있으며, 각 필드의 상태를 관리합니다. 에러 메시지를 구체적으로 표시하고, 로딩 상태를 처리합니다. 계정 생성이 완료되면 사용자는 메인 페이지로 이동합니다.
onChange 함수는 React 컴포넌트에서 입력 필드의 변경 이벤트를 처리하는 함수입니다. 이 함수는 입력 필드가 변경될 때마다 호출되어 입력된 값을 상태(state)로 업데이트합니다.
- 입력 이벤트 객체(e) 처리:
- onChange 함수는 React.ChangeEvent<HTMLInputElement> 타입의 이벤트 객체를 인자로 받습니다. 이 이벤트 객체는 입력 필드에서 발생한 이벤트에 대한 정보를 담고 있습니다.
- 구조 분해 할당:
- const { name, value } = e.target;를 사용하여 이벤트 객체의 target 속성에서 name과 value를 추출합니다.
- name: 입력 필드의 name 속성 값입니다.
- value: 입력 필드에 입력된 현재 값입니다.
Q. e.target?
- e.target은 이벤트가 발생한 DOM 요소를 참조합니다.
- 이를 통해 이벤트가 발생한 요소의 속성 및 값을 접근할 수 있습니다.
- e.target을 사용하면 여러 입력 필드에서 발생하는 이벤트를 하나의 이벤트 핸들러에서 처리할 수 있습니다.
- 조건부 상태 업데이트:
- if (name === "name") setName(value);: 입력 필드의 name 속성이 "name"인 경우 setName 함수를 호출하여 name 상태를 업데이트합니다.
- else if (name === "email") setEmail(value);: 입력 필드의 name 속성이 "email"인 경우 setEmail 함수를 호출하여 email 상태를 업데이트합니다.
- else if (name === "password") setPassword(value);: 입력 필드의 name 속성이 "password"인 경우 setPassword 함수를 호출하여 password 상태를 업데이트합니다.
- useState 상태관리에 대해 더 알아보자!
- useState 훅: 함수형 컴포넌트에서 상태를 관리할 수 있도록 합니다.
- 상태 값과 업데이트 함수: useState는 현재 상태 값을 나타내는 변수와 상태를 업데이트하는 함수를 반환합니다.
- 상태 초기화: useState의 인자는 초기 상태 값을 설정합니다.
- 상태 업데이트: 상태를 업데이트하면 React는 해당 컴포넌트를 다시 렌더링하여 UI를 업데이트합니다.
- FirebaseError 클래스:
- Firebase SDK에서 발생하는 오류는 FirebaseError 클래스의 인스턴스입니다. 이 클래스는 오류 코드와 메시지를 포함합니다.
- e instanceof FirebaseError: 예외 객체 e가 FirebaseError 클래스의 인스턴스인지 확인합니다. 만약 그렇다면 Firebase 관련 오류임을 나타냅니다.
- errorMessages 객체:
- errorMessages는 오류 코드에 대한 사용자 친화적인 메시지를 매핑하는 객체입니다. 이 객체는 오류 코드를 키로 사용하여 해당하는 메시지를 값으로 저장합니다.
- setError 함수:
- setError는 상태 업데이트 함수로, 현재 컴포넌트의 에러 상태를 설정합니다. 이 함수는 주로 React의 useState 훅을 통해 생성됩니다.
어려웠던 부분 상세 설명
if (e instanceof FirebaseError) {
setError(errorMessages[e.code] || errorMessages["default"]); // 에러 메시지 설정
}
- if (e instanceof FirebaseError):
- 예외 객체 e가 FirebaseError 클래스의 인스턴스인지 확인합니다. 즉, Firebase 관련 오류인지를 체크합니다.
- errorMessages[e.code]:
- e.code는 발생한 오류의 코드입니다. errorMessages[e.code]는 이 코드에 대응하는 사용자 친화적인 에러 메시지를 반환합니다.
- || errorMessages["default"]:
- 만약 e.code에 해당하는 에러 메시지가 errorMessages 객체에 없으면 기본 에러 메시지(errorMessages["default"])를 반환합니다. 이는 예상하지 못한 오류에 대한 일반적인 메시지를 제공하기 위해 사용됩니다.
- setError 함수 호출:
- setError 함수를 호출하여 에러 메시지를 상태로 설정합니다. 이 상태는 보통 컴포넌트의 렌더링에 반영되어 사용자에게 표시됩니다.
login.tsx
// Firebase와 관련된 기능 및 필요한 모듈들을 임포트
import { auth } from "../firebase";
import logo from "/logo.svg"; // 로고 이미지 파일 경로
import { useState } from "react"; // 리액트의 useState 훅
import { Link, useNavigate } from "react-router-dom"; // 리액트 라우터 훅
import { FirebaseError } from "firebase/app"; // 파이어베이스 에러 처리 모듈
import { signInWithEmailAndPassword } from "firebase/auth"; // 파이어베이스 인증 모듈
import {
Error
} from "../component/auth-components"; // 에러 메시지 컴포넌트
import GithubButton from "../component/gitdub-btn"; // 깃허브 버튼 컴포넌트
import { styled } from "styled-components"; // 스타일드 컴포넌트
import Footer from "../component/Footer"; // 푸터 컴포넌트
import { media } from "../media-query-file"; // 미디어 쿼리 파일
// 스타일 컴포넌트 정의
const Switcher = styled.p`
min-width: 0; /* 요소가 넘치지 않도록 설정 */
font-size: 1rem;
margin-top: 10px;
a {
color: #9d7ff5;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
`;
const Logo = styled.img`
width: 400px;
height: auto;
display: flex;
justify-content: center;
margin: 20px 0;
min-width: 0; /* 요소가 넘치지 않도록 설정 */
`;
const Wrapper = styled.div`
margin: 0 auto;
max-width: 1200px;
display: flex;
flex-direction: column;
align-items: center;
flex-wrap: wrap;
${media.mobile(`
body {
padding: 0 10px !important;
}
`)}
${media.tablet(`
body {
padding: 0 30px !important;
}
`)}
${media.desktop(`
body {
padding: 0 50px !important;
}
`)}
`;
const Input = styled.input`
padding: 10px 20px;
border-radius: 50px;
border-color: #9D80F5;
font-size: 16px;
font-family: pretendard-regular;
width: 600px !important;
align-items: center;
justify-content: center !important;
&[type="submit"] {
background-color: #9D7FF5;
color: white;
cursor: pointer;
&:hover,
&:active {
opacity: 0.9;
}
}
@media (max-width: 1200px) { /* 데스크탑 */
width: 100% !important;
}
@media (max-width: 768px) { /* 태블릿 */
width: 90% !important;
}
@media (max-width: 480px)
width: 90% !important;
}
`;
const Form = styled.form`
margin-top: 30px;
margin-bottom: 10px;
display: flex;
flex-direction: column;
gap: 10px;
align-items: center;
justify-content: center !important;
width: 1000px;
@media (max-width: 1200px) { /* 데스크탑 */
width: 100% !important;
}
@media (max-width: 768px) { /* 태블릿 */
width: 90% !important;
}
@media (max-width: 480px)
width: 80% !important;
}
`;
// CreateAccount 컴포넌트 정의
export default function CreateAccount() {
const navigate = useNavigate(); // 페이지 이동을 위한 네비게이트 훅
const [isLoading, setLoading] = useState(false); // 로딩 상태 관리
const [email, setEmail] = useState(""); // 이메일 상태 관리
const [password, setPassword] = useState(""); // 비밀번호 상태 관리
const [error, setError] = useState(""); // 에러 메시지 상태 관리
// Input 필드 값이 변경될 때 호출되는 함수
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
if (name === "email") setEmail(value); // 이메일 상태 업데이트
else if (name === "password") setPassword(value); // 비밀번호 상태 업데이트
};
// 폼 제출 시 호출되는 함수
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); // 폼 기본 동작 방지
setError(""); // 에러 메시지 초기화
if (email === "" || password === "") {
setError("이메일과 패스워드를 입력해주세요."); // 이메일 또는 패스워드가 비어있을 때 에러 설정
return;
}
try {
setLoading(true); // 로딩 상태 설정
await signInWithEmailAndPassword(auth, email, password); // Firebase를 통한 로그인
navigate("/"); // 메인 페이지로 이동
} catch (e) {
if (e instanceof FirebaseError) {
// Firebase 에러 처리
switch (e.code) {
case 'auth/user-not-found':
setError("존재하지 않는 계정입니다.");
break;
case 'auth/wrong-password':
setError("비밀번호가 틀렸습니다.");
break;
case 'auth/invalid-email':
setError("유효하지 않은 이메일입니다.");
break;
default:
setError("로그인 중 오류가 발생했습니다. 다시 시도해 주세요.");
break;
}
}
} finally {
setLoading(false); // 로딩 상태 해제
}
};
// 컴포넌트 렌더링
return (
<Wrapper>
<Logo src={logo} alt="Logo" />
<Form onSubmit={onSubmit}>
<Input
onChange={onChange}
name="email"
value={email}
placeholder="이메일"
type="email"
required
/>
<Input
onChange={onChange}
name="password"
value={password}
placeholder="패스워드"
type="password"
required
/>
<Input
type="submit"
value={isLoading ? "로딩중..." : "로그인"}
/>
</Form>
{error !== "" ? <Error>{error}</Error> : null}
<Switcher>
아직 계정이 없나요? <Link to="/CreateAccount"> 계정 생성 → </Link>
</Switcher>
<GithubButton />
<Footer />
</Wrapper>
);
}
이 코드는 사용자가 이메일과 비밀번호를 사용하여 로그인할 수 있는 페이지를 구현한 것입니다. Firebase를 사용하여 인증 기능을 제공하며, 사용자가 입력한 정보를 검증하고, 로그인 성공 시 메인 페이지로 리다이렉트하는 기능을 포함하고 있습니다.
- Imports 및 Firebase 설정
- Firebase 인증 객체, React, styled-components, React Router DOM 등을 불러와서 사용합니다.
- 스타일 컴포넌트 정의
- Switcher, Logo, Wrapper, Input, Form 등 스타일 컴포넌트를 정의합니다. 이 컴포넌트들은 각각 CSS 속성을 적용하여 스타일을 지정합니다.
- CreateAccount 컴포넌트 정의
- useNavigate 훅을 사용하여 페이지 이동 기능을 추가합니다.
- useState 훅을 사용하여 로딩 상태, 이메일, 패스워드, 에러 메시지를 관리합니다.
- onChange 함수는 Input 필드 값이 변경될 때 호출되며, 상태를 업데이트합니다.
- onSubmit 함수는 폼 제출 시 호출되며, Firebase 이메일 로그인 함수를 사용하여 로그인을 시도합니다. 성공 시 메인 페이지로 이동하고, 오류 발생 시 적절한 에러 메시지를 설정합니다.
- 렌더링
- Wrapper, Logo, Form, Input, Switcher, GithubButton, Footer 컴포넌트들을 사용하여 화면을 구성합니다.
- Form 컴포넌트는 제출 시 onSubmit 함수를 호출하며, Input 필드들은 onChange 함수를 통해 상태를 업데이트합니다.
- 에러 메시지는 error 상태가 비어있지 않을 때만 표시됩니다.
- Switcher 컴포넌트는 계정 생성 페이지로 이동할 수 있는 링크를 제공합니다.
- GithubButton 컴포넌트는 Github 로그인 버튼을 표시합니다.
- Footer 컴포넌트는 페이지 하단의 푸터를 표시합니다.
728x90
반응형
'웹개발 포트폴리오' 카테고리의 다른 글
[개인 포트폴리오] [트위터 클론 SNS Deli] 분석 Part. 11 'post.tsx' (0) | 2024.07.08 |
---|---|
[개인 포트폴리오] [트위터 클론 SNS Deli] 분석 Part. 10 'profile.tsx' (0) | 2024.07.08 |
[개인 포트폴리오] [트위터 클론 SNS Deli] 분석 Part. 8 'home.tsx' (0) | 2024.07.08 |
[개인 포트폴리오] [트위터 클론 SNS Deli] 분석 Part. 7 'github-btn.tsx' & 'auth-components.ts' & 'Footer.tsx' (0) | 2024.07.08 |
[개인 포트폴리오] [트위터 클론 SNS Deli] 분석 Part. 6 'protected-route.tsx' (0) | 2024.07.08 |