스프린트 미션 11 제출 - 김윤기#83
Hidden character warning
Conversation
| const handlePasswordVisible = () => { | ||
| setPasswordVisible(!passwordVisible); | ||
| setPasswordVisible((prev) => !prev); | ||
| }; |
There was a problem hiding this comment.
- handlePasswordVisible 함수의 이름이 동작을 충분히 서술하지 못하고 있습니다. 수행하는 역할을 명확하게 드러낼 수 있게 이름을 바꿔보면 좋을 것 같습니다.
- 토글은 하나의 동작으로 보일 수 있지만, 사실은 on과 off 2가지 동작이 조합으로 구성됩니다. UI 바인딩을 위해서 함수 하나로 정의하게 되지만, 내부 로직은 명확하게 구분하는 게 좋습니다. 현재 상태가 무엇인지 모르는 상태에서
!prev같은 구문을 사용하면 의도와는 반대로 동작하는 위험이 발생할 가능성이 높습니다.
| import { Modal } from '@/components/ui/dialog'; | ||
| import { cn } from '@/libs/cn'; | ||
| import { signupFormSchema } from '@/libs/schemas/auth.schema'; | ||
| import { type signupFormSchema, signupSchema } from '@/libs/schemas/auth.schema'; |
There was a problem hiding this comment.
signupFormSchema, signupSchema 둘 다 유사한 네이밍 패턴인데 signupFormSchema은 타입이고 signupSchema은 객체인가요?
추상화 수준이 다르다면 파일 계층 자체를 분리하거나, 같은 파일에 있다면 네이밍 패턴을 다르게 해서 어떤 것이 타입이고 어떤 것이 객체인지 명확하게 인식할 수 있도록 하는 게 좋습니다.
e.g. lib/schemas.ts 파일 내 signupFormSchemaInterface와 signupFormSchema vs interfaces.ts 파일 내 signupFormSchema와 schemas.ts 파일 내 signupFormSchema
| const { | ||
| register, | ||
| handleSubmit, | ||
| reset, | ||
| formState: { errors, isSubmitting } | ||
| } = useForm<ArticleCommentValues>({ | ||
| resolver: zodResolver(articleCommentSchema), | ||
| defaultValues: { | ||
| context: "" | ||
| } | ||
| }) |
There was a problem hiding this comment.
유사한 형태로 반복되는 패턴이 여러 번 보이는데, 이 부분을 커스텀 hook으로 정의해서 사용하면 반복을 줄일 수 있습니다.
| <Button | ||
| type="submit" | ||
| disabled={isSubmitting} | ||
| className="flex justify-center items-center py-3 px-5.75 grow-0 bg-gray-400 border-none rounded-md text-white whitespace-nowrap cursor-pointer hover:bg-primary-100"> | ||
| 등록 | ||
| </Button> |
There was a problem hiding this comment.
컴포넌트로 정의한 것 치고는 className이 과하게 많이 들어가는 느낌이긴 하네요. 🤔
기본값으로 넣을 수 있는 스타일이라면 Button 컴포넌트 자체에 넣고, 유형으로 구분할 수 있는 조합이라면 variants 같은 느낌으로 사전에 정의한 유형을 지정하는 형태로 사용할 수 있다면 더 좋을 것 같습니다.
| {comments.length > 0 ? ( | ||
| comments.map((comment: ArticleComment) => ( | ||
| <li | ||
| key={comment.id} | ||
| className="border-2.5 flex flex-col gap-6 border border-t-0 border-r-0 border-l-0 border-solid border-gray-300 bg-gray-50 px-0 py-3" | ||
| > | ||
| <div className="flex justify-between"> | ||
| <p className="font-pretendard flex-1 px-2 text-gray-800"> | ||
| {comment.context} | ||
| </p> | ||
| <DropdownContent articleId={article.id} comment={comment} /> | ||
| </div> | ||
| <div className="mb-1.5 flex items-center gap-2"> | ||
| <Image | ||
| src={comment.author?.userProfile?.photoUrl || DefaultImg} | ||
| alt="avatar" | ||
| width={32} | ||
| height={32} | ||
| className="h-8 w-8 shrink-0 rounded-full object-cover" | ||
| /> | ||
| <div> | ||
| <span className="font-pretendard text-xs leading-4.5 text-gray-600"> | ||
| {comment.author?.nickname} | ||
| </span> | ||
| <p className="text-xs leading-4.5 text-gray-400"> | ||
| {formatDate(comment.createdAt)} | ||
| </p> | ||
| </div> | ||
| </div> | ||
| </li> | ||
| )) | ||
| ) : ( | ||
| <> | ||
| <Image | ||
| className="mb-4 self-center" | ||
| width={140} | ||
| height={140} | ||
| src={CommentEmptyImg} | ||
| alt="comment-empty-img" | ||
| /> | ||
| <p className="font-pretendard text-center leading-6.5 text-gray-400"> | ||
| 아직 댓글이 없어요, | ||
| <br /> | ||
| 지금 댓글을 달아보세요! | ||
| </p> | ||
| </> | ||
| )} |
There was a problem hiding this comment.
최종 render 영역은 간결하게 유지하는 편이 좋습니다.
데이터가 없을 때 노출하는 이미지 영역과 데이터가 있는 경우 구성하는 UI를 분리해서 생각해보면 어떨까요?
| export type ArticleFormValues = z.infer<typeof articleFormSchema>; | ||
| export type ArticleCommentValues = z.infer<typeof articleCommentSchema>; | ||
| export type UpdateCommentFormValues = z.infer< | ||
| typeof updateArticleCommentSchema | ||
| >; |
There was a problem hiding this comment.
이전에도 네이밍에 관해서 코멘트를 남기긴 했는데, zod에서 z.infer를 사용하는 example이나 다른 사람들이 사용할 때 나타나는 패턴을 참고해보세요.
There was a problem hiding this comment.
프로젝트 내에서 여러 번 사용될 것 같은 함수들을 모아두는 건 좋네요.
|
|
||
| export const articleService = { | ||
| getBestArticles: () => | ||
| cookieFetch<CommonResponse<Article[]>>(`/api/v1/articles/best`, { |
There was a problem hiding this comment.
반복되는 url path는 상수로 정의해서 사용하면 좋을 것 같습니다.
요구사항
[x] Github에 위클리 미션 PR을 만들어 주세요.
[x] React 및 Express를 사용해 진행합니다.
[x] TypeScript를 활용해 프로젝트의 필요한 곳에 타입을 명시해 주세요.
[x] any 타입의 사용은 최소화해 주세요.
[x] 복잡한 객체 구조나 배열 구조를 가진 변수에 인터페이스 또는 타입 별칭을 사용하세요.
[x] Union, Intersection, Generics 등 고급 타입을 적극적으로 사용해 주세요.
[x] 타입 별칭 또는 유틸리티 타입을 사용해 타입 복잡성을 줄여주세요.
[x] 타입스크립트 컴파일러가 에러 없이 정상적으로 작동해야 합니다.
프론트엔드
[x] 기존 React(혹은 Next) 프로젝트를 타입스크립트 프로젝트로 마이그레이션 해주세요.
[x] TypeScript를 활용해 프로젝트의 필요한 곳에 타입을 명시해 주세요.
백엔드 (참고:tsx사용하였습니다)
[x] 기존 Express.js 프로젝트를 타입스크립트 프로젝트로 마이그레이션 해주세요.
[x] tsconfig.json 파일을 생성하고, 필요한 컴파일러 옵션을 설정해야 합니다. (예: outDir).
[x] TypeScript 관련 명령어를 package.json에 설정해 주세요. (예: 빌드 및 개발 서버 실행 명령어).
[x] ts-node와 nodemon을 사용하여 개발 환경을 구성합니다.
[x] nodemon과 함께 ts-node를 사용하여 . ts 파일이 변경될 때 서버를 자동으로 재시작하도록 설정합니다.
[x] Mongoose나 Prisma 등 ORM을 사용하는 경우, 모델에 대한 인터페이스 또는 타입을 정의합니다.
[x] 필요한 경우, declare를 사용하여 타입을 오버라이드하거나 확장합니다.