해당 프로젝트는
Next.js
+TypeScript
를 기반으로 만드는 인스타그램 클론 개인 프로젝트입니다.
📤 FE
0️⃣ react-hook-form
회원가입만이 아니라 해당 프로젝트에서 사용하는 form
을 관리하기 위해서 선택한 라이브러리입니다.
form
에서 사용할 타입만 정의하고 정해진 방식대로 속성만 넣어준다면, form
에 대한 데이터 관리, 감시, 유효성 검사 등의 많은 기능들을 쉽게 사용하도록 도와주기 때문에 유용하게 사용했습니다.
해당 포스트에서는 어떻게 활용했는지에 대해서만 작성하고 어떻게 사용하는지에 대해서 궁금하다면 공식 문서를 참고해주세요!
1. 설치
1
npm i react-hook-form
2. 구현한 코드
구현에 사용한 모든 코드를 작성하기에는 너무 많아서 상세한 코드는 깃헙 링크로 대체하겠습니다.
대부분의 기능은 react-hook-form
에서 제공해주는 기능을 그대로 사용했고, 이번에 사용할 형태에 따라서 커스텀 컴포넌트를 제작해서 사용했습니다.
<input />
을 사용할 때 자주 사용하는 기능들을 모두 묶어서 하나의 컴포넌트로 만들었습니다.
label
:id
속성에 맞게 라벨을 좌측 상단에 작게 생성subText
: 유효성 검사를 통과하지 못하면 좌측 하단에 작게 에러 문구 생성 (react-hook-form
의error
객체 사용 )
register()
를 이용하면 ref
를 전달해야하기 때문에 React.forwardRef()
를 사용했고, 그에 맞는 props
타입을 지정해줬습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import React from "react";
// style
import StyledInput from "./style";
// type
import type { UseFormRegister } from "react-hook-form";
import type { LogInForm, SignUpForm } from "@src/types";
interface Props {
id: string;
type: React.HTMLInputTypeAttribute;
placeholder: string;
subText?: string;
[index: string]: any;
}
/** 2023/03/25 - Input 컴포넌트 - by 1-blue */
const Input = React.forwardRef<
HTMLInputElement,
Props &
(
| ReturnType<UseFormRegister<SignUpForm>>
| ReturnType<UseFormRegister<LogInForm>>
)
>(({ id, type, placeholder, subText, ...props }, ref) => (
<StyledInput>
<label htmlFor={id}>{id}</label>
<input
id={id}
type={type}
placeholder={placeholder}
autoComplete="current-password"
{...props}
ref={ref}
/>
<span>{subText}</span>
</StyledInput>
));
Input.displayName = "Input";
export default Input;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import { useForm } from "react-hook-form";
// style
import StyledLogInForm from "./style";
// type
import type { LogInForm } from "@src/types";
/** 2023/03/24 - 로그인 페이지 - by 1-blue */
const LogInPage = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<LogInForm>();
/** 2023/03/25 - 로그인 수행 핸들러 - by 1-blue */
const onLogIn: React.FormEventHandler<HTMLFormElement> = handleSubmit(
useCallback(
async (body) => {
try {
// ... 로그인 요청 처리
} catch (error) {
console.error(error);
}
},
[router]
)
);
return (
<StyledLogInForm onSubmit={onLogIn}>
{/* 나머지 생략 ... */}
<FormToolkit.Input
id="id"
type="text"
placeholder="아이디를 입력해주세요."
subText={errors.id?.message}
{...register("id", {
required: "아이디를 입력해주세요!",
pattern: {
value: /(?=.*\d)(?=.*[a-zA-ZS]).{6,16}/,
message: "숫자와 영어가 최소 한 글자 이상 포함되고, 최소 6자리, 최대 16자리여야 합니다!"
},
})}
/>
<FormToolkit.Input
id="password"
type="password"
placeholder="비밀번호를 입력해주세요."
subText={errors.password?.message}
{...register("password", { required: "비밀번호를 입력해주세요!" })}
/>
{/* 나머지 생략 ... */}
</StyledLogInForm>
);
};
export default LogInPage;
📥 BE
0️⃣ prisma
데이터베이스를 쉽게 관리하기 위해 사용한 ORM
라이브러리입니다.
설치와 사용에 대한 구체적인 내용은 prisma를 참고해주시고 해당 포스트에서는 기본적인 내용을 제외하고 작성하겠습니다.
또한 아직 회원가입 부분이라 User
에 대한 모델 정의만 작성할 것이라 내용이 별로 없습니다.
계속 기능을 추가하면서 다음 포스트들을 통해서 다른 모델의 정의와 모델간의 관계에 대해 작성하겠습니다.
1. 설치
1
npm i prisma @prisma/client
2. User 모델
대부분 문자열들은 해당 문자열을 입력받을 때 글자수 제한을 걸었기 때문에 그 글자수만큼의 크기만 차지하도록 만들었습니다.
password
는 해싱을 통해서 길이가 늘어나기 때문에 더 많은 공간을 할당했고, avatar
도 S3
에 넣으면서 URL
이 추가되고 중복되지 않게 하기 위해 시간 값을 부여할 것이기 때문에 더 많은 공간을 할당했습니다.
나머지는 회원가입에 필요한 데이터들에 대해 정의를 했습니다.
( idx
는 해당 유저를 식별하는 고유의 식별자 )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model User {
idx Int @id @unique @default(autoincrement())
id String @unique @db.VarChar(16)
password String @db.VarChar(100)
name String @unique @db.VarChar(20)
email String @unique @db.VarChar(30)
phone String @unique @db.VarChar(11)
birthday String @db.VarChar(8)
introduction String @db.VarChar(100)
avatar String? @db.VarChar(160)
}
3. 타입 활용
prisma
를 이용해서 모델을 정의하고 npx prisma db push
or npx prisma migrate dev
같은 명령어를 이용해서 DB
에 동기화하면 해당 모델에 대한 타입을 만들어서 제공해줍니다.
위의 User
모델을 예시로 만들어준 타입을 보면 아래와 같이 정의되어 있습니다.
이대로 그냥 가져와서 사용하면 되기 때문에 매우 편합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import type { User } from "@prisma/client";
/**
* Model User
*
*/
export type User = {
idx: number
id: string
password: string
name: string
email: string
phone: string
birthday: string
introduction: string
avatar: string | null
}
하지만 혹시 기존 타입에서 커스터마이징이 필요하면 새로운 타입을 아예 새로 작성하기 보다는 유틸리티 타입을 이용해서 해당 타입에 의존하는 타입으로 작성하면 좋습니다.
1
export type SimpleUser = Pick<User, "idx" | "name" | "avatar">;
🕯️ 회원가입 로직
회원가입 로직은 다른 것들에 비해서 단순합니다.
회원가입할 유저의 정보를 받아서 중복 불가능한 정보는 DB
에서 확인 후 중복되면 중복되었다면 409
로 응답합니다.
만약 중복되지 않았다면 유저를 생성하고 201
로 응답합니다.
prisma
를 이용해서 중복 여부를 확인하고, 동시에 처리해도 되기 때문에 Promise.all()
을 이용해서 병렬적으로 처리해서 시간을 최소화했습니다.
생각해보면 특정 데이터에 대한 중복을 확인하는 것이라 한 번의 확인으로 체크하면 반복을 줄일 수 있을 것 같은데 정확한 방법을 못 찾아서 일단 각각 탐색하면서 중복된 데이터가 찾도록 만들었습니다… 🥲 나중에 좋은 방법을 찾으면 다시 기록하도록 하겠습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import bcrypt from "bcrypt";
interface HashingHandler {
(password: string, saltRound?: number): Promise<string>;
}
/** 2023/03/25 - 비밀번호 암호화 - by 1-blue */
export const hashing: HashingHandler = async (password, saltRound = 10) => {
// 해싱할 때 사용할 값 생성
const salt = await bcrypt.genSalt(saltRound);
// 비밀번호 해싱
const hashedPassword = await bcrypt.hash(password, salt); // password 해쉬화 완성
// 해싱한 비밀번호 반환
return hashedPassword;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// prisma
import { prisma } from "@src/prisma";
// lib
import withAuthMiddleware from "@src/lib/middleware";
import { hashing } from "@src/lib/auth";
// type
import type { NextApiHandler } from "next";
import type { SignUpForm } from "@src/types";
import type { ApiSignUpResponse } from "@src/types/api";
type SignUpBody = SignUpForm;
/** 2023/03/26 - 회원가입 - by 1-blue */
const handler: NextApiHandler<ApiSignUpResponse> = async (req, res) => {
try {
// 회원가입
if (req.method === "POST") {
// 회원가입하는 유저의 데이터
const { password, ...body } = req.body as SignUpBody;
// 아이디, 이름, 이메일, 휴대폰 번호 중복 검사 ( DB )
const exUserList = await Promise.all([
prisma.user.findUnique({ where: { id: body.id } }),
prisma.user.findUnique({ where: { name: body.name } }),
prisma.user.findUnique({ where: { email: body.email } }),
prisma.user.findUnique({ where: { phone: body.phone } }),
]);
// 아이디, 이름, 이메일, 휴대폰 번호 중복 검사 ( 응답 )
if (exUserList[0])
return res.status(409).json({ message: "아이디가 이미 존재합니다." });
if (exUserList[1])
return res.status(409).json({ message: "이름이 이미 존재합니다." });
if (exUserList[2])
return res.status(409).json({ message: "이메일이 이미 존재합니다." });
if (exUserList[3])
return res.status(409).json({ message: "폰번호가 이미 존재합니다." });
// 비밀번호 암호화
const hashedPassword = await hashing(password);
// 유저 생성
await prisma.user.create({ data: { ...body, password: hashedPassword } });
return res.status(201).json({
message: "회원가입을 성공했습니다.\n로그인 페이지로 이동됩니다.",
});
}
} catch (error) {
console.error("/api/signup error >> ", error);
return res
.status(500)
.json({ message: "서버측 문제입니다.\n잠시후에 다시 시도해주세요!" });
}
};
export default withAuthMiddleware({
methods: ["POST"],
handler,
isAuth: false,
});