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

[임건우] Week20 #564

Merged

Conversation

gw-lim
Copy link
Collaborator

@gw-lim gw-lim commented Jan 21, 2024

구현 사항

  • Next.js App router + tailwind 으로 마이그레이션을 진행했습니다. 아직 랜딩페이지 및 로그인/회원가입 페이지는 완성되지 않은 상태입니다.
  • react query을 일부 적용하였습니다.

배포주소

https://linkbrary-gw-lim.vercel.app/
https://linkbrary-gw-lim.vercel.app/shared/25/283

스크린샷

스크린샷 2024-01-22 오전 3 02 18 스크린샷 2024-01-22 오전 3 03 41 스크린샷 2024-01-22 오전 3 06 41

멘토님에게

  • App router의 캐시 기능을 활용하기 위해 axios 대신 fetch로 요청을 모두 바꿨습니다. 이로 인해 axios의 interceptor이나 base url 옵션등을 사용하지 못하다보니 api 함수들이 많이 더러워졌습니다... 특히 server side api과 client side api를 나누게 되면서 중복되는 코드가 많이 생겼습니다. 멘토님께서는 서버 사이트 api과 클라이언트 사이트 api 코드를 어떠한 방식으로 나누시는지 궁금합니다. 또한 fetch에서 interceptor을 구현하는 방법이 있을지 궁금합니다.
  • 현재는 쿠키에 accessToken을 저정하고 이를 헤더에 붙여서 보내 주는 방식으로 fetch를 구현하였는데, 서버 사이드 요청에서는 쿠키를 확인하지 못하다보니 authorization이 필요한 요청은 모두 클라이언트 사이드 요청으로 구현하게 되었습니다. 멘토님꼐서는 app router에서 토큰 관리를 어떠한 방식으로 하시는지 궁금합니다.
  • 모달에 컴파운드 패턴을 적용해보았습니다. 현재 구조에서 더 개선할 점이 있을지 확인해주시면 감사하겠습니다!
  • 현재 tailwind layer에 클래스를 추가적으로 작성해두지 않은 상태인데, 멘토님께서는 tailwind layer에 주로 어떠한 클래스들을 작성해두시는 지 궁금합니다.

@gw-lim gw-lim changed the base branch from main to part3-임건우 January 21, 2024 16:41
@gw-lim gw-lim requested a review from wlgns2223 January 21, 2024 17:47
@gw-lim gw-lim self-assigned this Jan 21, 2024
@gw-lim gw-lim added 매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. 미완성 죄송합니다.. labels Jan 21, 2024
@wlgns2223
Copy link
Collaborator

코드리뷰하기전에 우선 답변부터 드리겠습니다. 멘토링때도 말씀드렸다시피 App router는 실무에서 사용하지 않아서 실무적 관점을 드릴 수 없습니다. 대신에 멘티님에게 도움 될 수 있도록 최대한 조사 후 저의 의견이 전달 될 수 있도록 하겠습니다.

@wlgns2223
Copy link
Collaborator

wlgns2223 commented Jan 22, 2024

  • App router의 캐시 기능을 활용하기 위해 axios 대신 fetch로 요청을 모두 바꿨습니다. 이로 인해 axios의 interceptor이나 base url 옵션등을 사용하지 못하다보니 api 함수들이 많이 더러워졌습니다... 특히 server side api과 client side api를 나누게 되면서 중복되는 코드가 많이 생겼습니다. 멘토님께서는 서버 사이트 api과 클라이언트 사이트 api 코드를 어떠한 방식으로 나누시는지 궁금합니다. 또한 fetch에서 interceptor을 구현하는 방법이 있을지 궁금합니다.
    답변
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는 따로 받지 않았는데
이 부분은 개발하면서 필요하면 매개변수를 추가해가면서 인터페이스를 발전시켜나가도 되구요.

바퀴를 새로 발명하지마라라는 말이 있듯이 외부라이브러리도 찾아보면 좋을것 같네요.
구글링 해보니 누가 만든 라이브러리가 있더라구요 ( https://return-fetch.myeongjae.kim/#2-throw-an-error-if-a-response-status-is-more-than-or-equal-to-400 ).

@wlgns2223
Copy link
Collaborator

  • 현재는 쿠키에 accessToken을 저정하고 이를 헤더에 붙여서 보내 주는 방식으로 fetch를 구현하였는데, 서버 사이드 요청에서는 쿠키를 확인하지 못하다보니 authorization이 필요한 요청은 모두 클라이언트 사이드 요청으로 구현하게 되었습니다. 멘토님꼐서는 app router에서 토큰 관리를 어떠한 방식으로 하시는지 궁금합니다.

Authorization헤더에 넣어야되서 그런가보네요. 넥스트 문서를 읽고 구글링을 좀 해봤는데 이 부분을 실제로 시행착오를 겪어봐야될것 같아
지금은 답변드릴수가 없습니다 ㅜㅜ

@wlgns2223
Copy link
Collaborator

wlgns2223 commented Jan 22, 2024

  • 모달에 컴파운드 패턴을 적용해보았습니다. 현재 구조에서 더 개선할 점이 있을지 확인해주시면 감사하겠습니다!

제가 코드를 보고 좀 궁금한점이 생겼는데 , 왜 리액트 엘러먼트를 state로 선언을 했죠? 사실 이 부분은 제가 관습적으로 봐오지 못했던 패턴이라 조금 찾아봤는데, 공식문서를 보면 그렇게 하지말라는 말은 없는데 추천은 안한다는 말이 stackoverflow에도 올라 와 있네요.
( https://stackoverflow.com/questions/47875097/add-element-to-a-state-react )

이 모달 부분은 멘토링때 다 같이 이야기하면 좋을 것 같습니다. 왠지 state에 Element를 넣는 패턴을 멘티님들께서 많이 쓰시는것 같네요

컴파운드 패턴을 적용했으면 컴파운드 패턴을 적용해야할 이유가 있어야 된다고 생각합니다. 우선 그런 이유를 좀 덧붙여 주셨으면 제가 좀 더 고려해서 피드백을 드릴 수 있었을것 같아요 😇

컴파운드 패턴의 장점은 state를 공유, 부품들을 조립해가며 재사용성을 높인다. 이정도로 생각이 납니다.
건우님 코드에서는 지금 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>

@wlgns2223
Copy link
Collaborator

  • 현재 tailwind layer에 클래스를 추가적으로 작성해두지 않은 상태인데, 멘토님께서는 tailwind layer에 주로 어떠한 클래스들을 작성해두시는 지 궁금합니다.

저희는 따로 정의한 클래스들을 추가안합니다. 복잡도를 늘리고 싶지 않기 때문입니다. 디자이너와 잘 협업하면 테일윈드에 있는 색상코드,너비단위등으로 충분이 디자인이 가능하고 예외적인 수치라면 임의의 수치를 사용해도 됩니다.

팀원 중 한명이 중복코드를 줄인다는 명목으로 임의의 클래스 이름을 사용해서 스타일링을 하다보면 다른 팀원들도 새로 클래스 이름을 지정을 하게 될께 뻔합니다. 그러면 계속해서 새로운 스타일을 가진 클래스 이름이 프로젝트에 생기게 됩니다.
해당 클래스가 어떤 스타일을 정의하고 있는지 봐야하기때문에 유지보수측면에서 좋지 않다고 생각합니다.

@wlgns2223
Copy link
Collaborator

디자인 패턴도 적용하시고 TailwindCSS에 사용에 대해서 물어보시는것을 보니까 기본적인 사용법을 빨리 익히시고 좋은 패턴을 찾기위해서 노력하시는것 같네요 . 보기 좋습니다 ㅎㅎ

Comment on lines +8 to +22
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>
);
}
Copy link
Collaborator

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로 관리를 못할수도 있기때문에 결국 모달 프로바이더를 따로 만드는게 쉬울 수 있습니다.

Copy link
Collaborator Author

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}>
Copy link
Collaborator

Choose a reason for hiding this comment

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

제가 부트캠프 멘토링을 하면서 대부분의 멘티님들이 이 key를 index로 하시더라구요.
리액트에서 그러면 리렌더링시에 리스트 요소들의 위치들이 정확하게 렌더링 안될수가있는데
건우님은 이 부분을 id로 하셨네요. 잘 하셨습니다.

Comment on lines +34 to +36
<DeferredSuspense isFetching={isPending} fallback={<LinkSkeleton />}>
<Cards type="folder" data={filteredLinks} />
</DeferredSuspense>
Copy link
Collaborator

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등으로 컨트롤하는것은 대부분 잘못된 방법이었습니다 😅

Copy link
Collaborator Author

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" />
Copy link
Collaborator

Choose a reason for hiding this comment

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

저도 학습하다가 알게 된 사실인데 Next Image에 placeholder="blur"를 이용하면 이미지 로딩할때 블러이미지를 제공합니다.

Comment on lines +7 to +9
const user = await SERVER_API.user.getUser();
const isLoggedIn = Boolean(user.state === "success");
const userInfo = user?.data;
Copy link
Collaborator

Choose a reason for hiding this comment

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

저는 주로 최상단에서 UserProvider를 만들어서 유저 상태를 관리합니다. 서비스 전반에 유저 객체가 이용할때가 많더라구요.

Copy link
Collaborator

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>
    );
};

Comment on lines +22 to +24
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;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Default 값들을 사용하셔서 좋습니다. 방어적으로 코딩한다고 하는데,
좋은 습관입니다. ! 😇

Comment on lines +32 to +33
<p className="mb-5 text-12 text-gray-100">{createdDate.toLocaleDateString("ko")}</p>
<p className="mb-3 text-16">{title}</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에서 Locale이란 기능을 제공합니다.
유저의 시스템 위치,국가,언어들을 ko,en,fr등으로 나누는데 이러한 기능을 이용해서 다국어,글로벌 서비스를 개발 할 수 있습니다.

Comment on lines +10 to +14
const isEmpty = data?.length === 0;

return (
<>
{isEmpty ? (
Copy link
Collaborator

Choose a reason for hiding this comment

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

제가 좋아하는 코딩스타일입니다 ㅎㅎ JSX return하는 부분에 조건문 수식을 그대로 나타내는게 가독성을 떨어뜨린다고 생각해서 저도 조건문이 있으면 대부분 변수로 만들어주는 편입니다.
클린코드에서도 나오는 내용입니다.

Comment on lines +11 to +34
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;
Copy link
Collaborator

Choose a reason for hiding this comment

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

추상화와 컴포넌트 개발 시도하신게 좋습니다 ! 근데 쓰지는 않으셨더라구요 ㅠㅠ

Copy link
Collaborator Author

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";
Copy link
Collaborator

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

@wlgns2223 wlgns2223 merged commit bc5b4f7 into codeit-bootcamp-frontend:part3-임건우 Jan 24, 2024
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