Skip to content

Conversation

@huuitae
Copy link
Collaborator

@huuitae huuitae commented Sep 18, 2025

요구사항

기본

  • 목록 조회
    • 로고’ 버튼을 클릭하면 ‘/’ 페이지로 이동합니다. (새로고침)
    • 진행 중인 할 일과 완료된 할 일을 나누어 볼 수 있습니다.
  • 할 일 추가
    • 상단 입력창에 할 일 텍스트를 입력하고 추가하기 버튼을 클릭하거나 엔터를 치면 할 일을 새로 생성합니다.
  • 할 일 완료
    • 진행 중 할 일 항목의 왼쪽 버튼을 클릭하면 체크 표시가 되면서 완료 상태가 됩니다.
    • 완료된 할 일 항목의 왼쪽 버튼을 다시 클릭하면 체크 표시가 사라지면서 진행 중 상태가 됩니다.

심화

주요 변경사항

  • Next.js App Router 사용

스크린샷

빈 화면 할 일 추가
todo_main_empty todo_add_task
할 일 완료 완료 취소
todo_done_task todo_cancel_task

멘토에게

  • Next를 처음 사용해보는거라 API 호출이나 서버, 클라이언트 사이드 코드를 맞게 사용한건지 잘 모르겠습니다..

help

  • 위처럼 할 일 완료/취소 과정에서 바로 적용되지 않고 새로운 데이터를 불러올 때 까지 바로 적용이 안됩니다.
    이 문제를 최적화 할 수 있는 방법이 궁금합니다!

huuitae added 30 commits June 18, 2025 21:23
@huuitae huuitae self-assigned this Sep 18, 2025
@huuitae huuitae requested a review from kiJu2 September 18, 2025 09:57
@kiJu2
Copy link
Collaborator

kiJu2 commented Sep 20, 2025

스프리트 미션 하시느라 수고 많으셨어요.
학습에 도움 되실 수 있게 꼼꼼히 리뷰 하도록 해보겠습니다. 😊

@kiJu2
Copy link
Collaborator

kiJu2 commented Sep 20, 2025

위처럼 할 일 완료/취소 과정에서 바로 적용되지 않고 새로운 데이터를 불러올 때 까지 바로 적용이 안됩니다. 이 문제를 최적화 할 수 있는 방법이 궁금합니다!

크으 ~ 정말 좋은 질문입니다.
수강생분들이 꼭 알았으면 하는 기술이었는데 이렇게 질문주셨네요 😊

보통 UI는 서버 응답을 받은 후에 상태를 갱신합니다.
그래서 “할 일 완료/취소” 같은 액션을 눌러도, 서버에서 새로운 데이터를 받아오기 전까지는 화면이 바로 바뀌지 않죠.

자 한 번 생각해봅시다.

완료/취소를 했을 때 통신이 실패할 확률이 얼마나 될까요?

한 100번 정도 해보시면 한 번이라도 실패를 할까요?
아마 거의 대부분이 실패할 일이 없을 액션이지요.

인스타그램이나 페이스북의 좋아요 같은 기능도 마찬가지일거예요.

따라서 방법은 "됐다고 칩시다 !" 입니다.
이를 전문 용어로 낙관적 업데이트(Optimistic Update)라고도 합니다.
image

자세한건 코드리뷰를 통해서 어떻게 바꿀 수 있는지 확인해볼게요 !

Comment on lines +6 to +31
export default async function addTaskAction(
prevData: ActionState,
formData: FormData
): Promise<ActionState> {
const name = formData.get("name")?.toString();

if (!name) return { status: false, error: "할 일을 입력해주세요" };

try {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/items`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ name }),
});

if (!response.ok) return { status: false, error: response.statusText };

revalidateTag("todo");

return { status: true, error: "" };
} catch (error) {
return { status: true, error: `할 일 등록에 실패했습니다. ${error}` };
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

와우. 서버액션을 구현하셨네요?

뭐야 무서워요.
앱 라우터를 사용하셨군요? 반갑네요 🙌🏿


if (!response.ok) return { status: false, error: response.statusText };

revalidateTag("todo");
Copy link
Collaborator

Choose a reason for hiding this comment

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

크으. revalidateTag까지 쓰시다니.

훌륭합니다. 이벤트를 기반으로 캐싱을 하고 있군요 ! 👍👍 놀라운데요 ?

Comment on lines +13 to +16
const [state, formAction, isPending] = useActionState(addTaskAction, {
status: true,
error: "",
});
Copy link
Collaborator

Choose a reason for hiding this comment

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

useActionState를 사용하셨군요 !

대단합니다 휘태님. React 도큐먼트를 꼼꼼히 읽어보신 것 같아요.
적절한 상황에 적절한 React 내장 훅을 잘 사용하셨네요 크으.. 👍👍

? `${styles.add_btn} ${styles.writing_task}`
: styles.add_btn
}
disabled={isPending ? true : false}
Copy link
Collaborator

Choose a reason for hiding this comment

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

삼항연산자가 필요 없을 수 있겠군요 !

Suggested change
disabled={isPending ? true : false}
disabled={!!isPending}

Comment on lines +3 to +7
/**
* 로딩 스피너 컴포넌트
* @param {string} size - 스피너의 크기 (e.g., '50px')
* @param {string} color - 스피너의 색상 (e.g., '#007bff')
*/
Copy link
Collaborator

Choose a reason for hiding this comment

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

jsdoc까지.. 꼼꼼합니다 🥺🥺🥺

Comment on lines +23 to +25
const onClickSetTask = () => {
setTask({ name, memo: "", imageUrl: "", isCompleted: !isCompleted });
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

보통 외부에서 전달받는 콜백을 지을 때 on~을 사용합니다.

지금은 내부 핸들링 함수이므로 다음과 같이 작성해볼 수 있겠네요 !

Suggested change
const onClickSetTask = () => {
setTask({ name, memo: "", imageUrl: "", isCompleted: !isCompleted });
};
const handleClickSetTask = () => {
setTask({ name, memo: "", imageUrl: "", isCompleted: !isCompleted });
};

Comment on lines +15 to +39
try {
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/items/${taskId}`,
{
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
}
);

if (!response.ok) {
alert(`완료하지 못했습니다`);
setError(true);
return;
}

revalidateTodo();
} catch (error) {
alert("완료하지 못했습니다");
setError(true);
} finally {
setIsLoading(false);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

(질문에 대한 답변) 아앗.. 현재 구조에서는 낙관적 업데이트는 어렵겠군요.

음. 코드를 수정하여 제안드리려 하는데 구조적인 문제로 보여요.
지금 구조는 NextJS 서버에서 캐싱 된 값을 revalidate시켜서 페이지를 갱신시키고 있군요.

만약 낙관적 업데이트로 바꾸고 싶으시다면,
체크리스트는 상태로써 관리(CSR)해야 할거예요.

그리고 서버 사이드에서는 Codeit 백엔드로부터 받은 데이터를 캐싱하시는건 유지하시고,
클라이언트 화면에서는 UI를 먼저 업데이트 -> 서버에 반영 -> 실패 시 롤백 구조를 가져가야 합니다.

지금 구조 너무 좋으니까, 서버 컴포넌트에서 초기 데이터를 클라이언트 컴포넌트로 내려주는 방식으로 리팩토링을 하면 성능이 훨씬 좋아질거로 기대가 되네요 👍👍

@kiJu2
Copy link
Collaborator

kiJu2 commented Sep 20, 2025

훌륭합니다 휘태님 !
칭찬만 하다가 끝나버린 느낌이네요 👍👍
NextJS를 여러가지로 실험하시면서 개발하신 것 같아요 !

다만, todo와 같이 유저의 인터랙션에 따라 국부적인 리렌더링이 많은 경우 CSR도 유리할 수 있습니다 😉

미션 수행하시느라 수고 많으셨습니다 휘태님 !

@kiJu2 kiJu2 merged commit 1df4002 into codeit-bootcamp-frontend:Next-황휘태 Sep 20, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants