페이지별 요구사항
별도의 요구사항이 없는 것은 지원자가 판단하여 개발합니다.
/src
/api (backend API 관리)
/components (공용 컴포넌트)
/contexts (공용 컨텍스트)
/hooks (공용 훅)
/icons
/pages (라우트 기본 단위를 페이지로 설정)
/schemas (zod 스키마)
/styles
/types
/utils
main.tsx
구현해야 하는 페이지는 총 3개
/auth/signup
/auth/login
/
최근에는 주로 Nextjs를 사용했었기 때문에, 페이지 라우팅을 위해 react-router
를 사용하는 것은 익숙하진 않았었어요.
공식 문서를 살펴보면서, 라우팅을 위한 작업을 진행했습니다.
디렉토리
/src
/pages
page.tsx
/messages
page.tsx
/auth
/login
page.tsx
/register
page.tsx
createBrowserRouter
공식 문서 상, 웹 서비스의 라우팅을 위해 권장하는 방식인 createBrowserRouter를 사용했습니다.
// main.ts
const router = createBrowserRouter([
{
path: "/",
element: <RootLayout />,
children: [
{
path: "/",
element: <RootPage />,
},
{
path: "/messages",
element: <MessagesPage />,
},
],
},
{
path: "/auth/login",
element: <LoginPage />,
},
{
path: "/auth/register",
element: <RegisterPage />,
},
]);
layout
RootPage와 MessagesPage에서 공통으로 사용하는 레이아웃 및 로직이 존재했기에, 이를 위해 RootLayout을 구성했습니다.
공통 로직
로그인 여부를 체크하고, 로그인이 되어있지 않다면 로그인 페이지로 리다이렉트합니다.
// layout.tsx
const RootLayout = () => {
// 로그인 체크
return (
<LayoutWrapper>
<Sidebar />
<Outlet />
</LayoutWrapper>
);
};
로그인, 회원가입, 메시지 발송 등 여러 폼을 구현해야 했기 때문에, 폼의 구성요소를 컴포넌트화하여, 재사용할 수 있도록 했습니다.
공용 컴포넌트는 shadcn/ui 를 참고하여 구조화했습니다.
<form>
<FormField>
<FormFieldLabel>라벨</FormFieldLabel>
<Input
ref={inputRef}
value={value}
onChange={onChange}
onBlur={onBlur}
disabled={disabled}
name={name}
/>
<FormFieldError>{error.message}</FormFieldError>
</FormField>
</form>
htmlfor
값을 Context API를 통해 관리합니다.react-hook-form
을 활용했을 때, 해당 컴포넌트에 ref를 전달하기 때문에 이를 위해 forwardRef를 사용하여 컴포넌트를 정의했습니다.폼을 관리하기 위해 react-hook-form
라이브러리를 사용했습니다.
각 필드의 유효성을 검증하기 위해서 zod
와 @hookform/resolver
라이브러리를 활용했습니다.
const formSchema = z.object({
header: z.string().max(100, "헤더는 최대 100자까지 가능합니다"),
recipents: z.array(z.object({ phoneNumber: z.string() })).min(1, "수신자를 입력해주세요"),
});
type FormValues = z.infer<typeof formSchema>;
const MessageForm = () => {
const form = useForm<FormValues>({
resolver: zodResolver(formSchema), // 유효성을 검증합니다.
defaultValues: {
header: "",
recipents: [{ phoneNumber: "" }],
},
});
// ...
return (
<form>
<Controller
control={form.control}
name="header"
render={({ field, fieldState }) => (
<FormField>
<FormFieldLabel>헤더</FormFieldLabel>
<Input {...field} hasError={!!fieldState.error} />
<FormFieldError>{fieldState.error?.message}</FormFieldError>
</FormField>
)}
/>
</form>
);
};
백엔드로의 요청에 대한 응답을 성공과 실패 두가지 케이스로 나누어, 구조화하였습니다.
에러가 발생했을 때, 어떤 데이터와 관련이 있는지도 전달하여 해당 필드에 에러를 표현하였습니다.
// auth.ts
type AuthAPIResponse<D = unknown, T = unknown> =
| {
success: true;
data: D;
}
| {
success: false;
target: T;
message: string;
}
const login = async (values: {
email: string;
password: string;
}): Promise<AuthAPIResponse<unknown, Path<LoginInput>>> => {
const url = `${API_DOMAIN}/api/auth/signin`;
const res = await fetch(url, {
method: "POST",
body: JSON.stringify(values),
headers: {
"Content-Type": "application/json",
},
});
const isSuccess = res.ok;
if (isSuccess) {
const data = await res.json();
return {
success: true,
data,
};
}
switch (res.status) {
case 400: {
const data = await res.json();
// 백엔드 응답에 따른 분기처리
const isInValidEmail = data.detail === "Invalid email";
const isInValidPassword = data.detail === "Invalid password";
if (isInvalidEmail) {
return {
success: false,
target: "email",
message: "가입된 이메일이 아닙니다.",
};
}
if (isInvalidPassword) {
return {
success: false,
target: "password",
message: "비밀번호가 일치하지 않습니다.",
};
}
throw new Error("알 수 없는 오류가 발생했습니다.");
}
default: {
throw new Error("알 수 없는 오류가 발생했습니다.");
}
}
}
해당 API 메서드를 호출하는 컴포넌트에서는 다음과 같이 에러를 처리할 수 있었습니다.
const form = useForm(...);
const onSubmit = async (values) => {
try {
const res = await login(values);
const isSuccess = res.success;
if (!isSuccess) {
form.setError(
res.target,
{
type: "",
message: res.message,
},
{ shouldFocus: true },
);
}
// ...
} catch (e) {
if (e instanceof Error) {
alert(error.message);
} else {
console.error(error);
}
}
};
react-router
페이지 라우팅
zod
유효성 검증(form)
react-hook-form
form 관리
@hook-form/resolver
zod를 활용하여, react-hook-form의 유효성 검증을 수행하기 위함
react-hot-toast
토스트
기존에는 주로 tailwind를 사용하여, 스타일링을 진행했었다 보니 emotion을 통해 스타일링을 하는 것이 익숙치 않아, 고민되는 부분이 있었습니다.
css
, styled
, Global
등 스타일링을 할 수 있는 방법이 다양하게 있어, 어떤 방식으로 스타일링을 진행할 것인가?
재사용 가능한 스타일 관련 코드는 어떻게 관리할 것인가?
Global
을 사용하여, 해당 컴포넌트의 하위 컴포넌트에 대해 일관된 스타일을 적용하도록 시도해 봤지만, 이 방법은 스타일링 코드와 적용되는 부분 간 거리가 떨어져 있기에 스타일을 수정하기 위해서 찾기 힘들 수 있으며, 해당 코드가 어디에 영향을 미치는지 이해하기 어려울 수 있기에 사용하지 않는 것이 좋겠다고 판단.