-
Notifications
You must be signed in to change notification settings - Fork 51
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
[임건우] Week20 #564
The head ref may contain hidden characters: "part3-\uC784\uAC74\uC6B0-week20"
[임건우] Week20 #564
Conversation
코드리뷰하기전에 우선 답변부터 드리겠습니다. 멘토링때도 말씀드렸다시피 App router는 실무에서 사용하지 않아서 실무적 관점을 드릴 수 없습니다. 대신에 멘티님에게 도움 될 수 있도록 최대한 조사 후 저의 의견이 전달 될 수 있도록 하겠습니다. |
interface RequestProps {
baseUrl?: string;
headers?: {};
beforeRequest?: () => void;
afterRequest?: () => void;
}
const createInstance = (instanceProps: RequestProps) => async <T>({ method, url, body }: InstanceProps) => {
const accessToken = getAccessToken();
const predefinedHeaders = instanceProps.headers || {};
if (instanceProps.beforeRequest) {
instanceProps.beforeRequest();
}
const response = await fetch(`${instanceProps.baseUrl}${url}`, {
method,
body: JSON.stringify(body),
headers: { ...predefinedHeaders, ...(accessToken && { Authorization: `Bearer ${accessToken}` }) },
});
if (instanceProps.afterRequest){
instanceProps.afterRequest();
}
if (response.status >= 400) {
const res: ResponseProps<T> = {
state: "error",
error: { status: response.status, ...(await response.json()) },
};
return res;
} else {
const res: ResponseProps<T> = {
state: "success",
data: await response.json(),
};
return res;
}
};
```javascript
const requestInstance = createInstance({ baseUrl: BASE_URL, headers: { "Content-Type": "application/json" } }); 건우님의 코드를 참고해서 좀 급하게 짜본 코드이긴합니다. 저도 질문을 보고 fetch API를 axios처럼 못쓴다는 이슈가 많은걸 알게됐습니다. const createInstance = (instanceProps: RequestProps) => async <T>({ method, url, body }: InstanceProps) => { ... }; 이렇게 함수를 리턴하는 함수를 작성해서 인스턴스를 생성하구요 if (instanceProps.beforeRequest) {
instanceProps.beforeRequest();
}
if (instanceProps.afterRequest){
instanceProps.afterRequest();
} 인터셉터 함수가 필요하면 직접 정해줄것 같습니다. 인터셉터함수에 props는 따로 받지 않았는데 바퀴를 새로 발명하지마라라는 말이 있듯이 외부라이브러리도 찾아보면 좋을것 같네요. |
Authorization헤더에 넣어야되서 그런가보네요. 넥스트 문서를 읽고 구글링을 좀 해봤는데 이 부분을 실제로 시행착오를 겪어봐야될것 같아 |
제가 코드를 보고 좀 궁금한점이 생겼는데 , 왜 리액트 엘러먼트를 state로 선언을 했죠? 사실 이 부분은 제가 관습적으로 봐오지 못했던 패턴이라 조금 찾아봤는데, 공식문서를 보면 그렇게 하지말라는 말은 없는데 추천은 안한다는 말이 stackoverflow에도 올라 와 있네요. 이 모달 부분은 멘토링때 다 같이 이야기하면 좋을 것 같습니다. 왠지 state에 Element를 넣는 패턴을 멘티님들께서 많이 쓰시는것 같네요 컴파운드 패턴을 적용했으면 컴파운드 패턴을 적용해야할 이유가 있어야 된다고 생각합니다. 우선 그런 이유를 좀 덧붙여 주셨으면 제가 좀 더 고려해서 피드백을 드릴 수 있었을것 같아요 😇 컴파운드 패턴의 장점은 state를 공유, 부품들을 조립해가며 재사용성을 높인다. 이정도로 생각이 납니다. 저는 현업에서 아래와 같이 사용했습니다. 질문이 있으시면 편하게 해주세요~ const MenuBody: React.FC<PropsWithChildren> = ({ children }) => {
const router = useRouter();
const [curPath, setCurPath] = useState<string>(PATH.dashBoard);
const handleRouter = (path?: string) => () => {};
return (
<MenuProvider.Provider
value={{
router,
curPath,
setCurPath,
handleRouter,
}}
>
<ul className="hover:cursor-pointer">{children}</ul>
</MenuProvider.Provider>
);
}; const SubItem: React.FC<PropsWithChildren<{ path: string }>> = ({ children, path }) => {
const { handleRouter } = useMenuContext();
return (
<div onClick={handleRouter(path)} className={classes}>
<span>{children}</span>
</div>
);
}; 그리고 합성 export const Menu = Object.assign(MenuBody, {
List: MenuList,
Item: Item,
SubMenu: SubMenu,
SubItem: SubItem,
}); 사용할때는 아래와 같이 사용합니다. <Menu>
<Menu.SubMenu>
<SomeComponent />
</Menu.SubMenu>
</Menu> <Dialog >
<Dialog.Title >
{someThing}
</Dialog.Title>
<Dialog.Content>
{SomeChildren}
</Dialog.Content>
</Dialog> |
저희는 따로 정의한 클래스들을 추가안합니다. 복잡도를 늘리고 싶지 않기 때문입니다. 디자이너와 잘 협업하면 테일윈드에 있는 색상코드,너비단위등으로 충분이 디자인이 가능하고 예외적인 수치라면 임의의 수치를 사용해도 됩니다. 팀원 중 한명이 중복코드를 줄인다는 명목으로 임의의 클래스 이름을 사용해서 스타일링을 하다보면 다른 팀원들도 새로 클래스 이름을 지정을 하게 될께 뻔합니다. 그러면 계속해서 새로운 스타일을 가진 클래스 이름이 프로젝트에 생기게 됩니다. |
디자인 패턴도 적용하시고 TailwindCSS에 사용에 대해서 물어보시는것을 보니까 기본적인 사용법을 빨리 익히시고 좋은 패턴을 찾기위해서 노력하시는것 같네요 . 보기 좋습니다 ㅎㅎ |
export const ModalContext = createContext<{ modal: JSX.Element | null; setModal: Dispatch<SetStateAction<JSX.Element | null>> }>({ modal: <></>, setModal: function () {} }); | ||
|
||
function Providers({ children }: React.PropsWithChildren) { | ||
const [client] = useState(new QueryClient()); | ||
const [modal, setModal] = useState<JSX.Element | null>(null); | ||
|
||
return ( | ||
<QueryClientProvider client={client}> | ||
<ReactQueryStreamedHydration> | ||
<ModalContext.Provider value={{ modal, setModal }}>{children}</ModalContext.Provider> | ||
</ReactQueryStreamedHydration> | ||
<ReactQueryDevtools initialIsOpen={false} /> | ||
</QueryClientProvider> | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
react-query용 Provider 컴포넌트와 Modal용 컴포넌트따로 분리하면 어떨까요 ?
프론트엔드 개발에서 "관심사의 분리"라는 용어를 좋아하고 이러한 개념을 컴포넌트 개발에 적용하려고 합니다.
SOLID원칙에서 단일책임의 원칙과 의미가 똑같다고 생각합니다.
이렇게 분리하는 이유는 개발자의 멘탈모델 및 유지보수성과 연관이 있습니다.
내가 리액트 쿼리 프로바이더를 수정하고 싶으면 해당 파일만 찾아가서보고 그 파일에는 리액트 쿼리에 관한 로직만 있을거라고 생각하는거죠. 그게 생각을 덜 하게되고 변경이 생기더라도 유지보수성이 높아집니다.
현업에서 수십개의 모달이 생기고 그걸 하나의 Provider로 관리를 못할수도 있기때문에 결국 모달 프로바이더를 따로 만드는게 쉬울 수 있습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
한번 해보도록 하겠습니다!
<ul className="flex gap-8"> | ||
<FolderButton selected={!selectedFolderId}>전체</FolderButton> | ||
{folders?.data?.map((folder) => ( | ||
<li key={folder.id}> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
제가 부트캠프 멘토링을 하면서 대부분의 멘티님들이 이 key를 index로 하시더라구요.
리액트에서 그러면 리렌더링시에 리스트 요소들의 위치들이 정확하게 렌더링 안될수가있는데
건우님은 이 부분을 id로 하셨네요. 잘 하셨습니다.
<DeferredSuspense isFetching={isPending} fallback={<LinkSkeleton />}> | ||
<Cards type="folder" data={filteredLinks} /> | ||
</DeferredSuspense> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DeferredSuspense
가 기존 Suspense
와 어떻게 다른지 궁금합니다.
내부 구현을 보니까 DEFERRED_MS
라는 임의의 시간으로 children 대신에 null을 리턴키도록 컨트롤하려는것 같은데, 제가 건우님의 의도를 모른상태에서 현재 답변을 드리면 임의의 시간과 SetTimeout등으로 컨트롤하는것은 대부분 잘못된 방법이었습니다 😅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
스켈레톤 UI가 로딩 시간이 매우 짧을 경우에도 나타나서 오히려 사용 경험을 저해할 수 있다는 아티클을 참고해서 로딩 시간이 300ms 이상일 때만 스켈레톤 UI가 보이도록 해놓은 코드입니다..!
|
||
return ( | ||
<section className="flex h-250 w-full flex-col items-center bg-primary-light pt-20"> | ||
<Image src={userInfo?.image_source ?? ""} alt={"공유된 프로필 사진"} width={60} height={60} className="mb-12 h-60 w-60 rounded-full" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저도 학습하다가 알게 된 사실인데 Next Image에 placeholder="blur"를 이용하면 이미지 로딩할때 블러이미지를 제공합니다.
const user = await SERVER_API.user.getUser(); | ||
const isLoggedIn = Boolean(user.state === "success"); | ||
const userInfo = user?.data; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저는 주로 최상단에서 UserProvider를 만들어서 유저 상태를 관리합니다. 서비스 전반에 유저 객체가 이용할때가 많더라구요.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
유저정보를 어디서든 꺼내 쓸 수 있습니다.
page라우터 기준으로는 _app 파일에 뒀습니다.
const GlobalProviders: React.FC<IProps> = ({ children }) => {
return (
<AProvider>
<BProvider>
<CProvider>
<CookiesProvider>
<UserProvider>
{children}
</UserProvider>
</CookiesProvider>
</CProvider>
</BProvider>
</AProvider>
);
};
const createdDate = new Date(data?.created_at ?? DEFAULT_CARD.create_at); | ||
const url = data?.url ?? DEFAULT_CARD.url; | ||
const imageSrc = data?.image_source ?? DEFAULT_CARD.imageSrc; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Default 값들을 사용하셔서 좋습니다. 방어적으로 코딩한다고 하는데,
좋은 습관입니다. ! 😇
<p className="mb-5 text-12 text-gray-100">{createdDate.toLocaleDateString("ko")}</p> | ||
<p className="mb-3 text-16">{title}</p> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
좀 더 심화적인 주제로는 Next.js에서 Locale이란 기능을 제공합니다.
유저의 시스템 위치,국가,언어들을 ko,en,fr등으로 나누는데 이러한 기능을 이용해서 다국어,글로벌 서비스를 개발 할 수 있습니다.
const isEmpty = data?.length === 0; | ||
|
||
return ( | ||
<> | ||
{isEmpty ? ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
제가 좋아하는 코딩스타일입니다 ㅎㅎ JSX return하는 부분에 조건문 수식을 그대로 나타내는게 가독성을 떨어뜨린다고 생각해서 저도 조건문이 있으면 대부분 변수로 만들어주는 편입니다.
클린코드에서도 나오는 내용입니다.
const Input = <T extends FieldValues>({ children, placeholder, type: initialType = "text", ...controls }: Props<T>) => { | ||
const { field, fieldState } = useController(controls); | ||
const [type, setType] = useState(initialType); | ||
|
||
const togglePasswordShow = () => { | ||
if (type === "password") setType("text"); | ||
else if (type === "text") setType("password"); | ||
}; | ||
|
||
return ( | ||
<div className="relative"> | ||
<label htmlFor={field.name}>{children}</label> | ||
<input id={field.name} placeholder={placeholder} type={type} {...field} className={`input mt-10 ${fieldState?.error && "border-red"}`} /> | ||
{initialType === "password" && ( | ||
<button onClick={togglePasswordShow} type="button" className="absolute right-0 top-58 h-24 w-24 -translate-x-1/2 -translate-y-1/2"> | ||
{type === "password" ? <IconEyeOff /> : <IconEyeOn />} | ||
</button> | ||
)} | ||
<div className="body2-normal mt-5 h-10 text-red">{fieldState?.error?.message}</div> | ||
</div> | ||
); | ||
}; | ||
|
||
export default Input; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
추상화와 컴포넌트 개발 시도하신게 좋습니다 ! 근데 쓰지는 않으셨더라구요 ㅠㅠ
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이전 위클리 미션 때 로그인/회원가입 페이지를 만들면서 사용했던 코드입니다! 이번에 마이그레이션을 진행하면서 해당 컴포넌트는 가져왔지만 로그인 페이지를 만들 시간이 부족해 사용하지는 못했습니다 ㅜㅜ
@@ -0,0 +1,15 @@ | |||
import { Skeleton } from "@mui/material"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
테일윈드용으로 나온 mui가 있습니다. 참고바랍니다 ㅎㅎ 써보니까 괜찮더라구요
https://www.material-tailwind.com
구현 사항
배포주소
https://linkbrary-gw-lim.vercel.app/
https://linkbrary-gw-lim.vercel.app/shared/25/283
스크린샷
멘토님에게