Next 유선향 sprint10#86
Hidden character warning
Conversation
|
스프리트 미션 하시느라 수고 많으셨어요. |
| // | ||
| interface Props { | ||
| onClick: onClick; | ||
| onClick?: onClick; |
There was a problem hiding this comment.
(선택) 타입은 보통 파스칼 케이스로 사용하는게 일반적이예요 !
| onClick?: onClick; | |
| onClick?: OnClick; |
global.d.ts에서 위와 같이 명칭을 바꾸는건 어떨까요?
| // | ||
| interface Props { | ||
| onClick: onClick; | ||
| onClick?: onClick; |
There was a problem hiding this comment.
또한, 해당 타입은 필요 없을 수도 있겠네요 !
| onClick?: onClick; |
(이어서)
| export default function Button({ | ||
| onClick, | ||
| children, | ||
| className, | ||
| disabled, | ||
| ...props | ||
| }: Props) { | ||
| return ( | ||
| <> | ||
| <button | ||
| onClick={onClick} | ||
| className="flex bg-blue-500 justify-center items-center w-full h-full rounded-[8px] border-none font-Pretendard text-H5Bold text-gray-100 bg-light-blue cursor-pointer" | ||
| className={clsx( | ||
| "flex bg-blue-500 justify-center items-center w-full h-full rounded-[8px] border-none font-Pretendard text-H5Bold text-gray-100 bg-light-blue cursor-pointer", | ||
| disabled ? "bg-gray-400" : "", | ||
| className | ||
| )} |
There was a problem hiding this comment.
(이어서) onClick을 지우고 다음과 같이 작성해볼 수 있겠어요
| export default function Button({ | |
| onClick, | |
| children, | |
| className, | |
| disabled, | |
| ...props | |
| }: Props) { | |
| return ( | |
| <> | |
| <button | |
| onClick={onClick} | |
| className="flex bg-blue-500 justify-center items-center w-full h-full rounded-[8px] border-none font-Pretendard text-H5Bold text-gray-100 bg-light-blue cursor-pointer" | |
| className={clsx( | |
| "flex bg-blue-500 justify-center items-center w-full h-full rounded-[8px] border-none font-Pretendard text-H5Bold text-gray-100 bg-light-blue cursor-pointer", | |
| disabled ? "bg-gray-400" : "", | |
| className | |
| )} | |
| export default function Button({ | |
| children, | |
| className, | |
| disabled, | |
| ...props | |
| }: Props) { | |
| return ( | |
| <> | |
| <button | |
| className={clsx( | |
| "flex bg-blue-500 justify-center items-center w-full h-full rounded-[8px] border-none font-Pretendard text-H5Bold text-gray-100 bg-light-blue cursor-pointer", | |
| disabled ? "bg-gray-400" : "", | |
| className | |
| )} |
There was a problem hiding this comment.
추가로 disabled도 지워볼 수 있을 것 같네요 !:
export default function Button({
children,
className,
...props
}: Props) {
return (
<>
<button
disabled={disabled}
className={clsx(
"flex bg-blue-500 justify-center items-center w-full h-full rounded-[8px] border-none font-Pretendard text-H5Bold text-gray-100 bg-light-blue cursor-pointer disabled:bg-gray-400", className
)}disabled를 버튼의 속성으로 넣게 되면 덩달아 접근성도 좋아지겠어요 !
또한, disabled를 상태 선택자를 통해서 스타일도 입힐 수 있구요 😊😊
| <input | ||
| className="relative w-full h-full bg-gray-100 font-Pretendard text-H6Regular border-none rounded-xl text-gray-800 px-4 py-6 | ||
| <div className="flex flex-col gap-6 mobile:gap-[10px] "> | ||
| <p className="text-[18px] font-Pretendard text-H4Bold">{label}</p> |
There was a problem hiding this comment.
<p> 태그는 단락을 나타냅니다 !
HTML <p> 요소는 하나의 문단을 나타냅니다. 시각적인 매체에서, 문단은 보통 인접 블록과의 여백과 첫 줄의 들여쓰기로 구분하지만, HTML에서 문단은 이미지나 입력 폼 등 서로 관련있는 콘텐츠 무엇이나 될 수 있습니다.
예를 들어 다음과 같은 값이 <p>태그에 적절할 수 있어요 😊:
Geckos are a group of usually small, usually nocturnal lizards. They are found on every continent except Antarctica.
✨ 대체 제안: <label> 태그로 변경해보세요!
| <p className="text-[18px] font-Pretendard text-H4Bold">{label}</p> | |
| <label htmlFor="fileUpload" className="text-[18px] font-Pretendard text-H4Bold"> | |
| {label} | |
| </label> |
혹은 이 텍스트가 진짜 단순한 스타일용 텍스트라면 도 괜찮아요. 하지만 입력 폼의 설명이나 타이틀 역할을 하고 있다면 <label>을 쓰는 걸 추천드려요!
| useEffect(() => { | ||
| const hasAccessToken = Boolean(localStorage.getItem("accessToken")); | ||
| setIsLogin(hasAccessToken); | ||
| }, []); | ||
|
|
||
| const handleClickLogin = async () => { | ||
| await signIn(); | ||
| setIsLogin(true); | ||
| }; |
There was a problem hiding this comment.
isLogin이라는 상태는 nav 뿐 아니라 여러 곳에서 사용될 수 있겠네요 !
이럴 때, Context API와 훅을 하나 만들어두는건 어떨까요? 추가로 isLoggin 보다 userData 상태를 가지고 있어도 되겠어요. 😊
const AuthContext = createContext(null);
export const AuthProvider = ({ children }) => {
const [userData, setUserData] = useState(null);
useEffect(() => {
const token = localStorage.getItem("accessToken");
if (token) {
const userData = await getMy(); // 예시 입니다 !
setUserData({ name: userData.name, email: userData.email }); // 예시 입니다 !
}
}, []);
const signIn = async () => {
const token = await login(); // 기존 코드에 `signIn` 함수와 같음
localStorage.setItem("accessToken", token);
const userData = await getMy(); // 예시 입니다 !
setUserData({ name: userData.name, email: userData.email }); // 예시 입니다 !
};
// 추가로 signOut 함수도 필요하겠어요 !
return (
<AuthContext.Provider value={{ userData, signIn }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => useContext(AuthContext);| ? useFormatUpDate(value.createdAt) | ||
| : useFormatDate(value.createdAt); |
There was a problem hiding this comment.
두 함수는 리액트의 훅이라고 보기는 어려운 것 같아요.
유틸 함수로 사용하시는건 어떠세요?:
| ? useFormatUpDate(value.createdAt) | |
| : useFormatDate(value.createdAt); | |
| ? formatUpDate(value.createdAt) | |
| : formatDate(value.createdAt); |
| instance.interceptors.request.use( | ||
| (config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => { | ||
| if (typeof window !== "undefined") { | ||
| const accessToken = localStorage.getItem("accessToken"); | ||
| if (!accessToken) return config; | ||
| config.headers.set("Authorization", `Bearer ${accessToken}`); | ||
| } | ||
| return config; | ||
| } | ||
| ); | ||
|
|
||
| instance.interceptors.response.use((response: AxiosResponse) => { | ||
| const accessToken = response.data.accessToken; | ||
| if (typeof window !== "undefined" && accessToken) { | ||
| localStorage.setItem("accessToken", accessToken); | ||
| localStorage.setItem("refreshToken", response.data.refreshToken); | ||
| } | ||
| return response; | ||
| }); |
|
선향님 ~! 너무 수고 많으셨습니다. |
| export const getServerSideProps: GetServerSideProps = async (ctx) => { | ||
| const cookies = parseCookies(ctx); | ||
| const accessToken = cookies.accessToken; | ||
| const refreshToken = cookies.refreshToken; | ||
| if (!accessToken) { | ||
| return { | ||
| redirect: { | ||
| destination: "/", | ||
| }, | ||
| props: {}, | ||
| }; | ||
| } | ||
| try { | ||
| await instance.get(`/user/me`); | ||
| } catch (err: any) { | ||
| if (err.response?.status === 401) { | ||
| try { | ||
| const refreshRes = await instance.post( | ||
| `/auth/refresh-token`, | ||
| {}, | ||
| { headers: { Cookie: `refreshToken=${refreshToken}` } } | ||
| ); | ||
| const newAccessToken = refreshRes.data.accessToken; | ||
| setCookie(ctx, "accessToken", newAccessToken, { | ||
| path: "/", | ||
| httpOnly: true, | ||
| sameSite: "lax", | ||
| }); | ||
| } catch { | ||
| return { | ||
| redirect: { destination: "/" }, | ||
| props: {}, | ||
| }; | ||
| } | ||
| } | ||
| } | ||
| return { props: {} }; | ||
| }; |
There was a problem hiding this comment.
서버사이드에서 인가처리도 훌륭히 하셨네요 ! 👍
NextJs에서 서버 자원을 사용하는 방법도 훌륭히 익히신 것 같아요 😆😆
요구사항
기본
심화
주요 변경사항
스크린샷
멘토에게