Skip to content
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

[7주차] Team CakeWay 김류원 & 지민재 미션 제출합니다. #5

Open
wants to merge 69 commits into
base: master
Choose a base branch
from

Conversation

mimizae
Copy link

@mimizae mimizae commented Jan 4, 2025

🍰 배포 링크

🩷 CakeWay_vote

🍰 과제 후기

[류원]

이번 과제에서 맡은 작업은 후보자 조회 화면, 투표 결과 화면, 메인 화면과 투표 관련된 API 연결이었는데, 이를 통해 API와의 통신 방식에 대해 많이 배웠습니다.

API 데이터를 가져오기 위해 Tanstack Query의 캐싱 기능을 적극 활용했는데, 덕분에 데이터가 변경되었을 때만 새로운 요청을 보내고, 이미 가져온 데이터는 캐시로 처리할 수 있어서 엄청난 장점을 느꼈습니다. 캐싱된 화면을 다시 접근하면 로딩 화면을 보지 않아도 되더라구요. 이는 사용자 경험을 크게 향상시켜주었고, 성능 최적화에도 큰 도움이 되었습니다.

또한 이번 과제를 통해 백엔드와의 협업에서 중요한 점들을 배웠고, API를 활용한 데이터 처리 방식에 대한 깊은 이해를 쌓을 수 있었습니다. API 명세서를 확인하고, API POST, GET 방식으로 데이터를 전송하는 방법을 배우면서 백엔드와의 원활한 소통의 중요성을 느꼈습니다. 이번 과제를 통해 프로젝트 개발에 필요한 백엔드 협업 및 API 활용 능력을 키울 수 있어서 좋았습니당

+추가로 빌드 오류 중 api router.tsx 에서 params 에대한 타입 오류가 나서. 3시간동안 해결해보려고 했으나 빌드오류를 해결하지 못하였는데 next를 15를 13버전으로 재설치하니 오류가 사라졌는데,, 왜그런지 이유는 모르겠네요…

[민재]

어느덧 마지막 과제 제출 날이 다가오니 마음이 정말 싱숭생숭 했습니다. 😵‍💫…

합격 통보를 받고 잘하진 못하고 많은 것을 알지 못해도 최선을 다 해 스터디에 열심히 참여해야겠다고 다짐을 했었는데 잘 지켜졌을지 모르겠습니다. 그렇지만 학기와 스터디를 병행하며 얻은 것이 수천가지라 매우 기뻐요!

마지막 과제에서 제가 맡은 부분은 로그인, 회원가입 UI 퍼블리싱과 api 연결 그리고 에러 핸들링 마지막으로 반응형 디자인을 설계하는 것이었습니다.
반응형이 늘 어려워 마지막 과제에서는 라이브러리 사용하며 발전하고 싶었는데 그러지 못해 아쉽습니다. 이 아쉬움을 담아 현재 진행 중인 프로젝트에 반영하고자 합니다.

백엔드와의 협업이 처음이었는데 항상 연락도 빠르게 봐 주시고 질문에 대해 친절히 답변해 줘서 api 연결이 한층 수월했습니다.

이번에는 백엔드에 추가 요청 없이 프론트엔드단에서 처리한 것이 꽤 많은데 (로그인 시 user 정보가 제한적으로 와서 모든 것을 로컬에 때려박은 점... ㅜ.ㅜ 투표를 했는지 안 했는지에 대한 것을 투표 api 응답으로는 알 수 없었음) 이번에 느낀 것을 바탕으로 추후 프로젝트에 반영하고자 합니다.

이제는 정말 프로젝트만 남았는데 류원 언니에게 그리고 팀에게 폐 끼치지 않는 팀원으로서 열심히 노력하고 싶습니다 ㅎㅎ (이번에도 류원 언니가 맡은 것이 정말 많았어요... 제가 여행 간다고 배려해 주어 너무 감사했습니다 🩷)

Ceos 20th FE 모두 수고 많으셨습니다! 새해 복 많이 받으세요 :)

🍰 과제 녹화

녹화한 영상을 올리고 싶은데 용량이 너무 커서 zip으로 압축해 올립니다... 헤헤

🍰과제 화면 녹화🍰

mimizae and others added 30 commits December 27, 2024 19:28
mimizae 브랜치 master에 머지
Feat: 모든 페이지에 반응형 적용
Feat: 유효성 검사를 함수화해서 lib 폴더로 이동, signup 페이지의 요소들을 컴포넌트화, api 요청 함수 생성
@ddhelop
Copy link
Member

ddhelop commented Jan 5, 2025

버셀 Deployment 링크라서 안들어가지는 것 같아요. domain 링크로 올려주세요!

@mimizae
Copy link
Author

mimizae commented Jan 5, 2025

버셀 Deployment 링크라서 안들어가지는 것 같아요. domain 링크로 올려주세요!

헉 알려주셔서 감사합니다 🥲🥲🥲 변경했는데 확인 부탁드려용 ㅜ.,ㅜ @ddhelop

@ddhelop
Copy link
Member

ddhelop commented Jan 5, 2025

잘되는것같습니다~

Copy link

@Programming-Seungwan Programming-Seungwan left a comment

Choose a reason for hiding this comment

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

과제 하느라 고생 많으셨습니다!

Comment on lines +13 to +14
{process.env.NODE_ENV === "development" && (
<ReactQueryDevtools initialIsOpen={false} />

Choose a reason for hiding this comment

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

tanstack query dev tool을 적극 활용하시는 모습 너무 좋아요! 사실 tanstack query를 이용한다는 것은 api call을 그냥 fetch() 함수를 매번 불러서 사용하는 것이 아닌, useQuery()useMutation() 을 통해 한다는 것인데, 이걸 매번 네트워크 탭을 까보기보다 개발 도구의 도움을 받는 것이 훨씬 낫거든요. 너무 좋습니다!

Comment on lines +5 to +7
url: string,
method: string,
body: Record<string, unknown> | null = null

Choose a reason for hiding this comment

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

타입스크립트를 조금 더 잘 활용해볼 수 있을 것 같아요.
단순히 string이라고 타입을 명시하는 것이 아니라, type urlType = "/api/auth" | "/api/auth" | ... 같이 union type으로 정의하면 아래의 switch 문도 필요없고 그냥 컴파일 타임에 다른 경로의 요청이 이루어진다면 잡아낼 수 있을거에요!
method 역시 "GET" | "POST" | "PUT" | ... 과 같은 방식으로 정의하면 되구요

Comment on lines +1 to +43
import { NextResponse } from "next/server";

const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL;

export async function POST(req: Request) {
try {
// 요청 데이터 확인
const body = await req.json();

// 백엔드 서버 요청
const response = await fetch(`${BACKEND_URL}/auth/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});

if (!response.ok) {
const errorText = await response.text();
console.error("백엔드 오류 텍스트:", errorText);
return NextResponse.json({ success: false, error: errorText }, { status: response.status });
}

const contentType = response.headers.get('Content-Type');

let responseBody;

if (contentType && contentType.includes('application/json')) {
responseBody = await response.json();
}
else {
responseBody = await response.text(); // JSON이 아닌 경우 처리
}

console.log("백엔드 응답:", responseBody);
return NextResponse.json({ success: true, data: responseBody });
}
catch (error) {
console.error("에러 발생:", error);
return NextResponse.json({ success: false, error: "서버 요청 실패" }, { status: 500 });
}
}

Choose a reason for hiding this comment

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

이 파일이 굳이 존재해야하나... 를 잘 모르겠어요. 앞에서 api.ts 를 이용한 로직 구성까지는 너무 좋았는데,그 요청의 연결 횟수가 불필요하게 많은 것 같아요.
그냥 사용자의 브라우저 단에서 브라우저 -> 백엔드 서버 정도의 구성이면 되는데, 브라우저 -> nextJS 서버 -> 백서버 -> 다시 nextJS 서버 -> 브라우저 와 같은 구성은 비용적으로나 속도 측면에서나 좀 별로인 거 같아요.

혹시 cors 때문에 이러한 방식을 택하신 것이라면, 그냥 cors 네트워크 특성을 잘 공부하시고 해결하셔도 좋을 것 같아요

Copy link
Author

Choose a reason for hiding this comment

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

nextJS 서버에 대해 잘못 이해하고 있었던 것 같아요...🥲🥲 지난번 넷플릭스 과제 당시 토큰을 네트워크 상에서 지울 수 있는 등의 보안 측면에서 이 서버를 거치고 백엔드 서버로 요청을 보내야 한다고 생각했는데 너무 얕게 알고 있으니 이런 불필요한 연결이 늘어난 것 같습니다 ㅜ.ㅜ 짚어주셔서 정말 감사해요. 다시 공부해 이번 프로젝트에서는 이런 실수를 줄이도록 노력해 보겠습니다!!

항상 정성껏 코드 리뷰 해 주셔서 정말 감사하게 생각하고 있습니다 🥹

Comment on lines +1 to +12
export interface SignupRequest{
username: string,
password: string,
email: string
name: string,
part: string,
team: string
}

export interface SignupResponse{
message: string;
}

Choose a reason for hiding this comment

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

인터페이스를 만드시는 것 너무 좋은 것 같은데, 아예 디렉터리 구성을 _type/dto.ts 처럼 하면 더 좋을 것 같아요.
지금과 같은 방식이면 아마 도메인/api/auth/signup/dto 로도 api가 쏴질 가능성이 있으니까요

import { ReactQueryDevtools } from "@tanstack/react-query-devtools";

export default function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient());

Choose a reason for hiding this comment

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

Suggested change
const [queryClient] = useState(() => new QueryClient());
function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
// SSR에서는 클라이언트에서 즉시 refetch하는 것을 피하기 위해
// staleTime을 0보다 크게 설정하는 것이 좋다.
staleTime: 60 * 1000,
},
},
});
}
let browserQueryClient: QueryClient | undefined = undefined;
function getQueryClient() {
if (typeof window === 'undefined') {
// Server일 경우
// 매번 새로운 queryClient를 만든다.
return makeQueryClient();
} else {
// Browser일 경우
// queryClient가 존재하지 않을 경우에만 새로운 queryClient를 만든다.
// React가 새 Client를 만들게 하기 위해 중요하다.
if (!browserQueryClient) browserQueryClient = makeQueryClient();
return browserQueryClient;
}
}

QueryClient는 싱글턴으로 유지하는 것이 더 나을 것 같아요. 클라이언트에서 매 요청마다 객체가 생성되는 것은 좀 별로니까요!

Choose a reason for hiding this comment

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

문서를 참고하시면 nextJS의 서버 사이드 렌더링을 고려한 쿼리 로직을 hydration 개념과 합치실 수 있을 겁니다!

Comment on lines +1 to +136
}

return response.json();
};

// 도메인별 요청 처리
export const apiRequest = async (
domain: string,
method: string = "GET",
body: Record<string, unknown> | null = null,
endpoint: string = ""
) => {
// 도메인별 기본 URL 설정
let baseUrl = "";

switch (domain) {
case "auth":
baseUrl = "/api/auth";
break;
case "vote":
baseUrl = "/api/vote";
break;

case "demodayVote":
baseUrl = "/api/vote/demoday";
break;
case "results":
baseUrl = "/api/results";
break;
case "leader":
baseUrl = "/api/leader";
break;
default:
throw new Error("Unknown domain");
}

// 완전한 URL 생성 (baseUrl + endpoint)
const fullUrl = endpoint ? `${baseUrl}/${endpoint}` : baseUrl;
console.log("fullUrl t:" + fullUrl);
console.log("fullUrl endpoint:" + endpoint);

// 메서드에 따른 처리
if (method === "POST") {
return fetchData(fullUrl, method, body);
} else if (method === "GET") {
return fetchData(fullUrl, "GET");
} else {
throw new Error("Unsupported HTTP method");
}
};

// 투표 결과를 가져오는 GET 요청
export const fetchVoteResults = async (endpoint: string) => {
try {
const results = await apiRequest("results", "GET", null, endpoint);
console.log("투표 결과", results);

// if (!results.ok) {
// throw new Error(`${results.errorCode} 결과 조회 중 오류가 발생했습니다`);
// }이거 왜안돼?
return results;
} catch (error) {
console.error("투표 결과 가져오기 실패", error);
}
};

//파트장, 데모데이 투표하기 POST 요청
export const fetchPostVote = async (endpoint: string, demoday: boolean) => {
const response = await apiRequest(
demoday ? "demodayVote" : "vote",
"POST",
null,
endpoint
);
console.log("파트장투표하기 endpoint:" + endpoint);

// if (!response.ok) {
// throw new Error(`${response.errorCode} 투표 중 오류가 발생했습니다`);
// }
return response;
};

//

//후보자 조회
export const fetchGetLeader = async (endpoint: string) => {
try {
const response = await apiRequest("leader", "GET", null, endpoint);

// if (!response.ok) {
// throw new Error(
// `${response.errorCode} 후보자 조회 중 오류가 발생했습니다`
// );
// }
console.log("후보자조회 결과", response);
return response;
} catch (error) {
console.error("후보자조회실패", error);
}
};

Choose a reason for hiding this comment

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

스크린샷 2025-01-05 오후 4 41 43 백엔드 분들께서 http로 서버를 배포하셨네요? 원래대로라면 로컬호스트는 문제가 없고 프론트 배포 환경에서는 네트워크 에러가 나야하는데(프론트 주소는 https 일테니까요) 이를 어떻게 해결하신 건가요?
  • 그리고 백엔드 분들께 https 배포를 요청해보세욥

Copy link
Author

Choose a reason for hiding this comment

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

와 이건 진짜 등골이 서늘한데 전혀 오류가 난 적이 없었습니다... 그래서 해결이라는 것도 딱히 없었어요... 정확한 이유를 모르겠는데 뭔가... 지금 막 생각이 난 것은 브라우저 -> 백엔드 서버 정도의 구성이면 되는데, 브라우저 -> nextJS 서버 -> 백서버 -> 다시 nextJS 서버 -> 브라우저 이런 식으로 통신을 했기 때문이 아닐까요...???

Next.js가 클라이언트와 백엔드 사이에서 프록시 서버 역할을 한다면, 브라우저가 직접 백엔드에 요청을 보내는 것이 아니라 Next.js 서버가 백엔드와 통신하게 되고... 이 경우, Next.js 서버와 백엔드 서버 간의 통신은 브라우저와는 별개로 이루어지므로 오류가 발생하지 않는...??? 즉, 브라우저에서는 https로 요청을 보내고, Next.js 서버가 백엔드에 http로 요청을 보내는 방식이라서...??? API 요청을 클라이언트에서 직접 처리하지 않으니까...??? 모르겠습니다... 바로 알아봐야겠어요...

  • 브라우저 → Next.js 서버: https
  • Next.js 서버 → 백엔드: http

Copy link

@hiwon-lee hiwon-lee left a comment

Choose a reason for hiding this comment

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

마지막 과제 넘 수고많으셨습니다.
주석도 잘 달아주시고 코드 전반적으로 디버깅을 엄청 열심히 한 흔적이 많아서, 전 확인하기 넘 좋았습니다..ㅎㅎ
그 만큼 과제하는데 많이 고민하셨다는 것이겠죠..ㅜㅜ 백엔드와 api연결한다는 부분에서 이전 과제와는 성격이 달라 특히 더 고민하고 공부할 수 있었던 것 같습니다. 이번 리뷰를 통해 저도 많이 얻어갈 수 있었습니다. 감사합니다

Choose a reason for hiding this comment

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

중간에 뜨는 시간에 로딩인디케이터를 넣어주신거 넘 보기좋네요

Comment on lines +13 to +32
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
username: "",
isLoggedIn: false,
setUserName: (username) => set({ username }),
login: () => set(() => ({ isLoggedIn: true })),
logout: () => {
set(() => ({
username: "",
isLoggedIn: false, // zustand 상태 초기화
}));

localStorage.removeItem("token");
},
}),
{
name: "loggedUserInfo", // localStorage에 저장될 키 이름
}
)

Choose a reason for hiding this comment

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

저도 zustand를 써보지 않아서 조금씩 공부중인데, 이렇게 간편하게 로컬스토리지에 저장할 수 있도록하는 미들웨어도 있군요. 보면서 왜 get은 없을까 궁금했는데 로컬스토리지에서 바로 가져올 수 있어서 앞으로 유용하게 쓰기 좋을것 같습니다.

);
}

const url = `${BACKEND_URL}/${part === "team" ? "" : "leader/"}${part}`;

Choose a reason for hiding this comment

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

이렇게 동적라우트쓰니까 간편한것같아요~

Comment on lines +45 to +46
refetchOnWindowFocus: true, // 윈도우가 포커스를 받을 때 리페치
refetchInterval: 5000, // 5초마다 자동으로 리페치

Choose a reason for hiding this comment

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

진짜 궁금한데 왜 후보자들을 5초마다 리페치하도록 하신건가요??

Copy link

Choose a reason for hiding this comment

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

이거 api 연결 후 득표수가 안올라가서ㅠㅠ 리페치 문제인가 하고 넣었던 코드인데 지금에서는 불필요한 코드 같네요ㅠ

Comment on lines +52 to +71
switch (domain) {
case "auth":
baseUrl = "/api/auth";
break;
case "vote":
baseUrl = "/api/vote";
break;

case "demodayVote":
baseUrl = "/api/vote/demoday";
break;
case "results":
baseUrl = "/api/results";
break;
case "leader":
baseUrl = "/api/leader";
break;
default:
throw new Error("Unknown domain");
}
Copy link

@hiwon-lee hiwon-lee Jan 5, 2025

Choose a reason for hiding this comment

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

switch문으로 각 페이지에서 백과의 api에서 필요한 것등를 가져올 수있도록 url를 만드셨네요. 좋은 방법인 것 같아요

Comment on lines +23 to +29
const [modalState, setModalState] = useState<{
isOpen: boolean;
message: string;
}>({
isOpen: false,
message: "",
}); // 모달 상태 통합

Choose a reason for hiding this comment

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

useState훅으로 모달 개폐여부를 작성하셨군요. 간단하고 좋은것 같습니다.
모달을 표현하는 또 다른 방법으로 페러렐라우트를 이용하는 방법이 있다고 알고 있습니다. 구현 방식이 조금 복잡하긴하지만 다음 케이크 웨이 프로젝트를 진행하면서 필요할 수도(?) 있어서 정보 공유차원으로 알랴드립니당...(이미 아신다면..그냥 지나가주세요ㅎㅋ)

Copy link

@yyj0917 yyj0917 left a comment

Choose a reason for hiding this comment

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

전체적으로 깔끔한 코드 구성을 위해 노력한 흔적이 눈에 보이는 것 같습니다! 특히 tanstack query, zustand와 같이 상태관련 라이브러리를 과제에도 적용하여 프로젝트 이전에 미리 학습하는 방향이 좋은 것 같아요. UI interaction을 잘하는 CakeWay 팀분들이라서 앞으로 있을 CakeWay 프로젝트도 기대가 되는 것 같습니다! 마지막 과제 정말 고생많으셨습니다!

Copy link

Choose a reason for hiding this comment

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

저도 동일한 생각으로 nextjs는 풀스택을 지원하는 라이브러리로 기능중 route.ts는 백엔드 서버가 없이 자체적으로 서버기능을 지원하기 위한 목적으로 알고 있습니다! netflix와 같이 서버가 없을 때는 서버대용 목적으로 사용할 수 있으나 이번처럼 백엔드가 존재할 때는 route.ts없이 잘 작성해놓으신 api 만으로도 충분한 통신이 가능할 것 같습니다!

Copy link

Choose a reason for hiding this comment

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

이 부분 파일들만 route.tsx로 해주신 이유가 있는지 궁금합니다!

Copy link

Choose a reason for hiding this comment

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

개발 시 사용했던 상수는 배포 시 제거해도 괜찮을 것 같아요!

// 도메인별 기본 URL 설정
let baseUrl = "";

switch (domain) {
Copy link

Choose a reason for hiding this comment

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

동적으로 url을 정하는 것이 api 함수에 바로 변수 하나만 설정해서 할 수 있다는 장점이 있는 것 같습니다! 다만 추후에 api 함수들을 보면서 유지보수를 하게 될 때, 다른 팀원이 볼 때 url 확인을 위한 step이 하나 더 있을 것 같아요. 배포주소만 baseUrl로 하는 방법도 괜찮을 것 같습니다!

return (
<>
{/* Head 컴포넌트에서 title, meta 태그 등을 설정 */}
<Head>
Copy link

Choose a reason for hiding this comment

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

head 태그 안에서의 meta, title 관련 정보들은 layout에 단순 태그로 두고, 관리하는 게 직관적이고 더 용이할 것 같습니다! 보통 대부분 meta, title, head 관련한 내용들은 layout.tsx에 다루니까용

Copy link

Choose a reason for hiding this comment

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

tanstack query를 사용하여 자주 쓰이는 정보를 캐싱하는 것 정말 좋은 것 같습니다. 저도 앞으로 프로젝트에서 tanstack query를 자주 사용하게 될 것 같은데 많이 참고하겠습니다!

}));

// 셀렉트 필드에 전달할 props 객체
const selectFieldProps = Object.keys(selectOptions).map((key) => ({
Copy link

Choose a reason for hiding this comment

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

이런 방법으로 필드에 props 객체를 전달하는 방법이 있는 줄을 몰랐습니다. 배워가겠습니다!

console.log(data);
const router = useRouter();

const onClick = () => {
Copy link

Choose a reason for hiding this comment

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

naming 조금 변화줘도 괜찮을 것 같아요!

Copy link

@psst54 psst54 left a comment

Choose a reason for hiding this comment

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

마지막 과제 너무 수고하셨습니다~~!

public/logo.svg Outdated
Copy link

Choose a reason for hiding this comment

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

헿 케이크웨이 로고 너무 귀여운거같아요

Copy link

Choose a reason for hiding this comment

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

와 정규식으로 확인하는 부분 너무 좋아요~~! 고생하셨습니다!!

border: 0.1875rem solid rgb(255, 108, 129);
text-align: center;
width: 37.5rem; /* 기본 고정 너비 */
@media (max-width: 37.5rem) {
Copy link

Choose a reason for hiding this comment

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

반응형 설정이 굉장히 꼼꼼하네요!! 주석도 열심히 쓰시구... 고생하셨습니다...😭

Copy link

Choose a reason for hiding this comment

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

폼 상태 관리나 유효성 검사를 위해서 정말 많은 노력을 하셨네요!! 함수도 잘게 쪼개져 있고 해서 읽기 좋았어요👍

Copy link

Choose a reason for hiding this comment

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

모달을 컴포넌트화해서 재사용 잘 하시는 부분이 좋았어요~~

Comment on lines +23 to +27
<PasswordContainer>
<ToggleButton type="button" onClick={togglePasswordVisibility}>
{type === "password" ? "보기" : "숨기기"}
</ToggleButton>
</PasswordContainer>
Copy link

Choose a reason for hiding this comment

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

오 이런 방식으로 비밀번호 표시/숨기기를 구현하셨군요! 저희도 참고하겠습니다!ㅎㅎㅎ

Copy link

Choose a reason for hiding this comment

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

오 각 에러 메시지에 대해서 처리를 꼼꼼하게 해주셨네요 정말 좋은 것 같아요!

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.

7 participants