Skip to content

[김진선] sprint9 #230

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conversation

jinsunkimdev
Copy link
Collaborator

@jinsunkimdev jinsunkimdev commented Jul 15, 2025

요구사항

목록 조회

  • ‘로고’ 버튼을 클릭하면 ‘/’ 페이지로 이동합니다. (새로고침)
  • 진행 중인 할 일과 완료된 할 일을 나누어 볼 수 있습니다.

할 일 추가

  • 완료된 할 일 항목의 왼쪽 버튼을 다시 클릭하면 체크 표시가 사라지면서 진행 중 상태가 됩니다.

할 일 완료

  • 상단 입력창에 할 일 텍스트를 입력하고 추가하기 버튼을 클릭하거나 엔터를 치면 할 일을 새로 생성합니다.
  • 진행 중 할 일 항목의 왼쪽 버튼을 클릭하면 체크 표시가 되면서 완료 상태가 됩니다.

배포 링크

https://doit-jinsunkim.netlify.app/

주요 변경사항

  • FormInput컴포넌트의 유효성 검사할 때 조언해 주신 대로 zod라이브러리를 사용해 보았습니다.
  • @tanstack/react-query라이브러리를 사용하였습니다.
  • Tailwindcss최신 버전(버전4)을 사용해서 스타일링을 적용해 보았습니다.

스크린샷

Screenshot 2025-07-15 at 14 47 38
Screen.Recording.2025-07-15.at.14.40.51.mov

멘토에게

  • 타입스크립트를 사용하면 타입확인이 더 편하다고 들었는데 처음 사용하다 보니까 타입을 정하는 거부터가 고려할 게 많은 거 같고 익숙하지가 못하다 보니까 오히려 작성하는 데에 시간이 많이 걸리는 거 같습니다. 좀 더 익숙해지도록 노력해야겠습니다...
  • page(홈페이지)에서 handleSubmit함수가 좀 길이가 긴 것 같습니다. 전체적으로 훅으로 만들기에는 재사용성이 좋지는 않은 것 같아서 validation부분만 훅으로 빼는 게 좋지 않을까 고민이 됩니다.
  • 가독성이나 응집도 관련해서 코드를 깔끔하게 작성하고 싶은데 항상 작성을 하다 보면 원하는 대로 되지 않는 것 같습니다...
  • 셀프 코드 리뷰를 통해 질문 이어가겠습니다.

@jinsunkimdev jinsunkimdev added the 매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. label Jul 15, 2025
Copy link
Collaborator

@addiescode-sj addiescode-sj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수고하셨어요!
정리할 부분도 있고 다듬어나갈 부분도 보이지만, 전체적으로 감을 잘 잡으시는군요 👍
리팩토링 고민이 많아보여서, 리팩토링 제안 위주로 리뷰 드려봤습니다!
그리고 서버 컴포넌트로 만들어지면 클라이언트 측 전송량이 줄어들어 성능상 이점이 있기때문에,
클라이언트 컴포넌트로 꼭 만들어야하는경우가 아니라면 use client 디렉티브를 붙이지 않는걸 권장합니다. 전체적으로 반복되는 부분들이 몇개 보여서 요건 꼭 수정해보세요!

주요 리뷰 포인트

  • 클라이언트 / 서버 컴포넌트 역할 구분해보기
  • 명확한 props만 남기는 습관 들여보기
  • Tailwind CSS 지시어 사용해 중복 줄이고 가독성 개선하기
  • 하드 코딩된 값 사용 지양하기
  • 리턴문 내부에서 여러번 분기가 반복된다면 조건문 분기로 바꿔주기
  • 낙관적 업데이트 + retry 옵션 같이 사용해보기
  • 컴포넌트 핵심 로직에 집중할 수 있도록 loading, error 파일 만들어주기
  • 관심사 응집 사용하기

Comment on lines +19 to +22
textStyle?: string;
leftIcon: string;
iconStyle?: string;
hoverColor?: HoverColor;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

스타일에 관련된 props는 제거하고, 명확한 props만 남기는 습관을 들여볼까요?
variant 기반으로 관리하면 이렇게 바뀔 수 있을거예요:

"use client";

import { MouseEventHandler } from "react";
import Icon from "./Icon";

type ButtonVariant = "primary" | "secondary" | "success";

interface CustomButtonProps {
  title: string;
  variant?: ButtonVariant;
  leftIcon?: string;
  handleClick?: MouseEventHandler<HTMLButtonElement>;
  btnType?: "button" | "submit";
  className?: string;
}

const variantStyles = {
  primary: {
    container: "w-[168px] h-14 mt-6",
    hoverColor: "group-hover:bg-violet-600",
    iconColor: "fill-slate-900",
    textColor: "text-slate-900",
  },
  secondary: {
    container: "w-[168px] h-14 mt-6",
    hoverColor: "group-hover:bg-rose-500",
    iconColor: "fill-slate-900",
    textColor: "text-slate-900",
  },
  success: {
    container: "w-[168px] h-14 mt-6",
    hoverColor: "group-hover:bg-lime-300",
    iconColor: "fill-slate-900",
    textColor: "text-slate-900",
  },
};

export default function CustomButton({
  title,
  variant = "primary",
  leftIcon,
  handleClick,
  btnType,
  className,
}: CustomButtonProps) {
  const styles = variantStyles[variant];

  return (
    <button
      type={btnType || "button"}
      disabled={false}
      onClick={handleClick}
      className={`group relative cursor-pointer ${styles.container} ${className || ""}`}
    >
      <div className="relative flex justify-center items-center gap-1 z-20 group-hover:text-amber-50 transition-colors duration-200">
        {leftIcon && (
          <Icon id={leftIcon} className={`h-5 w-5 ${styles.iconColor}`} />
        )}
        <span
          className={`font-[--font-nanumsquareB] font-bold leading-none translate-y-[1.5px] ${styles.textColor}`}
        >
          {title}
        </span>
      </div>
      <div
        className={`absolute inset-[1px] rounded-3xl bg-[#f5f8fc] border border-black transition-colors duration-200 z-10 ${styles.hoverColor}`}
      ></div>
      <div className="absolute left-[9px] right-[-4px] top-[5px] h-14 rounded-3xl bg-black z-0"></div>
    </button>
  );
}

@@ -0,0 +1,58 @@
"use client";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 컴포넌트가 클라이언트에서 쓰여야하는 이유가 있나요?

name={name}
placeholder={placeholder}
autoComplete="off"
className="absolute inset-0 px-6 py-[15px] bg-transparent z-20 text-slate-800 placeholder-slate-500 font-normal focus:outline-none"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 컴포넌트가 자주 쓰이는 공용 컴포넌트인만큼, 반복되는 스타일 규칙이 있다면 TailwindCSS 지시어를 사용해 코드 중복을 줄이고 가독성을 개선해보는건 어떨까요~?

Comment on lines +10 to +12
const isMobile = useInnerWidth() < 768;
const imgWidth = isMobile ? 71 : 151;
const imgHeight = 40;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

하드코딩한 숫자를 쓰지않고, 값의 명확한 의미를 전달할 수 있는 상수로 관리해보면 좋을것같네요 :)

<li
className={`flex items-center justify-between px-2 py-3 h-[50px] max-w-[588px] rounded-[27px] border-2 border-slate-800 text-slate-800
${
item.isCompleted
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

내부에서 item.isCompleted가 여러 번 반복되는 것보다 따로 조건문으로 분기하는게 코드 흐름이 더 명확해질 수 있고, 수정도 쉬워지고, 성능상 이점도 있겠네요.

import { MouseEvent } from "react";
import { Todo } from "../../types/todo";
import Icon from "../common/Icon";

interface TodoItemProps {
  item: Todo;
  onToggle: (
    e: MouseEvent<HTMLButtonElement>,
    id: number,
    current: boolean
  ) => void;
  disabled: boolean;
}

export default function TodoItem({ item, onToggle, disabled }: TodoItemProps) {
  if (item.isCompleted) {
    return (
      <li className="flex items-center justify-between px-2 py-3 h-[50px] max-w-[588px] rounded-[27px] border-2 border-slate-800 text-slate-800 bg-violet-100 hover:bg-yellow-50">
        <button
          className="flex items-center gap-2 flex-1 text-left cursor-pointer line-through hover:no-underline"
          onClick={(e) => onToggle(e, item.id, item.isCompleted)}
          disabled={disabled}
        >
          <div className="w-8 h-8 flex items-center justify-center rounded-full bg-violet-500 border-0">
            <Icon id="check" size={20} className="text-yellow-50" />
          </div>
          <span>{item.name}</span>
        </button>
      </li>
    );
  }

  return (
    <li className="flex items-center justify-between px-2 py-3 h-[50px] max-w-[588px] rounded-[27px] border-2 border-slate-800 text-slate-800 bg-yellow-50 hover:bg-violet-100">
      <button
        className="flex items-center gap-2 flex-1 text-left cursor-pointer hover:line-through"
        onClick={(e) => onToggle(e, item.id, item.isCompleted)}
        disabled={disabled}
      >
        <div className="w-8 h-8 flex items-center justify-center rounded-full bg-yellow-50 border-2">
        </div>
        <span>{item.name}</span>
      </button>
    </li>
  );
}

Pick<Todo, "id" | "isCompleted">,
MutationContext
>({
mutationFn: ({ id, isCompleted }) => patchItem(id, { isCompleted }),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기 id, isCompleted가 any 타입으로 추론되고있는데 타입 안정성을 위해 함수 인자 타입을 올바르게 설정해주시면 좋을것같네요!

Suggested change
mutationFn: ({ id, isCompleted }) => patchItem(id, { isCompleted }),
mutationFn: ({ id, isCompleted }: Pick<Todo, "id" | "isCompleted">) => patchItem(id, { isCompleted }),

>({
mutationFn: ({ id, isCompleted }) => patchItem(id, { isCompleted }),
onMutate: async ({ id, isCompleted }): Promise<MutationContext> => {
// 낙관적 업데이트
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사용자 입장에서 해당 액션이 매우 빈번한 편이고, 네트워크 지연을 기다리지않고 사용성을 위해 바로 피드백을 전달한다는 측면에서 낙관적 업데이트를 적용해보셨군요!
학습한걸 바로 적용해보시는건 매우 좋은 시도입니다 :)

사용성 개선에 더해, 만약 네트워크 문제로 인해 요청이 실패한다면 자동으로 재시도해보는 옵션을 써보는건 어떨까요? 낙관적 업데이트를 적용하려 할때 같이 써주면 좋은 옵션이랍니다!

mutate 함수 옵션중에 retry 옵션을 써보시면 될것같아요!

const mutation = useMutation({
  mutationFn: patchItem,
  retry: 1, // 재시도 1회만
  retryDelay: 300, // 재시도 간격 0.3초
});

Comment on lines +22 to +23
if (isLoading) return <p>로딩 중...</p>;
if (error) return <p>에러 발생</p>;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

next.js app router 파일중에 loading과 error 파일을 써서 이런 로딩, 에러 상황에 대한 분기를 컴포넌트 핵심 로직에 집중할 수 있게끔 제거해보는건 어떨까요? :)

공식문서 참고해보시면 좋을듯합니다 :)
loading.js 참고
error.js 참고

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Todo에 관련된 동작을 담당하는 data fetching 로직들을 UI로부터 분리하고자하는 용도였다면,
여러 훅으로 분산시키지않고 useTodoQuery 커스텀훅을 만들어 관련 로직끼리 모아두는게 좋을것같아요 :)

우선 관심사 응집이 일어나게되어, Todo 관련 모든 데이터 로직이 한 곳에 모이게되고
좀 더 통합되고 일관된 방식으로 관련된 타입 / 쿼리 키 관리가 가능해지기때문이에요.

예시)

import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { addTodo, getTodos } from "../api/todos";
import { MutationContext, Todo } from "../types/todo";

export const useTodoQuery = () => {
  const queryClient = useQueryClient();

  // Todo 목록 조회
  const todosQuery = useQuery<Todo[], Error>({
    queryKey: ["todos"],
    queryFn: getTodos,
    staleTime: 5 * 60 * 1000,
    gcTime: 10 * 60 * 1000,
  });

  // Todo 추가
  const addTodoMutation = useMutation<void, Error, string, MutationContext>({
    mutationFn: (name: string) => addTodo(name),
    onMutate: async (newTodo: string): Promise<MutationContext> => {
      await queryClient.cancelQueries({ queryKey: ["todos"] });
      const previousTodos = queryClient.getQueryData<Todo[]>(["todos"]);

      queryClient.setQueryData<Todo[]>(["todos"], (old: Todo[] | undefined) => {
        if (!old) return [];
        return [
          ...old,
          {
            id: Date.now(),
            name: newTodo,
            isCompleted: false,
          },
        ];
      });

      return { previousTodos };
    },
    onError: (
      err: Error,
      variables: string,
      context: MutationContext | undefined
    ) => {
      if (context?.previousTodos) {
        queryClient.setQueryData(["todos"], context.previousTodos);
      }
    },
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ["todos"] });
    },
  });

  return {
    // Query 상태
    todos: todosQuery.data ?? [],
    isLoading: todosQuery.isLoading,
    isError: todosQuery.isError,
    error: todosQuery.error,
    getTodos: todosQuery.refetch,

    // Mutations
    addTodo: addTodoMutation.mutate,

    // Mutation 상태
    isAdding: addTodoMutation.isPending,
  };
};

Comment on lines +26 to +38
if (!validation.success) {
const flattened = z.flattenError(validation.error);
const newErrors: Record<string, string> = {};
for (const key in flattened.fieldErrors) {
const messages =
flattened.fieldErrors[key as keyof typeof flattened.fieldErrors];
if (messages && messages.length > 0) {
newErrors[key] = messages.join(" ");
}
}
setErrors(newErrors);
return;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분 로직 너무 좋은데요? ㅎㅎ 잘하셨습니다! 👍

@addiescode-sj
Copy link
Collaborator

질문에 대한 답변

멘토에게

  • 타입스크립트를 사용하면 타입확인이 더 편하다고 들었는데 처음 사용하다 보니까 타입을 정하는 거부터가 고려할 게 많은 거 같고 익숙하지가 못하다 보니까 오히려 작성하는 데에 시간이 많이 걸리는 거 같습니다. 좀 더 익숙해지도록 노력해야겠습니다...
  • page(홈페이지)에서 handleSubmit함수가 좀 길이가 긴 것 같습니다. 전체적으로 훅으로 만들기에는 재사용성이 좋지는 않은 것 같아서 validation부분만 훅으로 빼는 게 좋지 않을까 고민이 됩니다.
  • 가독성이나 응집도 관련해서 코드를 깔끔하게 작성하고 싶은데 항상 작성을 하다 보면 원하는 대로 되지 않는 것 같습니다...
  • 셀프 코드 리뷰를 통해 질문 이어가겠습니다.

현재 구조상 HomePage에서만 쓰인다면, 말씀하신것처럼 훅으로 분리해도 재사용 빈도가 낮을 수 있고, 오히려 코드가 분산되어 추적이 어려울 수 있습니다. 저는 훅으로 분리하지않는게 나을것같네요!

리팩토링 고민이 있어보이셔서 이번 리뷰는 최대한 리팩토링 제안을 많이 드려봤습니다 :)

@addiescode-sj addiescode-sj merged commit e13af2e into codeit-bootcamp-frontend:Next-김진선 Aug 6, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants