Published on

tWIL 2022.09 2주차

Authors

추석연휴 첫날 40km를 다녀왔다. 이렇게 조금씩 거리를 늘리다가 하트코스를 다녀올 수 있을지... 이정도 되니 무릅이 조금 아파왔다. 자세가 안좋은것인지 다음번에도 아프면 클릿 피팅을 다시해야할 것 같기도 하다. 여튼 돌아올 땐 무릅이 조금 아파서 거의 당기는 힘을 주로 써서 라이딩을 하니 좀 편했다.

Typescript Apollo Next

그간 미루던 typescript-apollo-next 플러그인으로 만든 page.tsx에서 withSSRgetServerPage를 써보다가 이 플러그인이 만들어주는 페이지 컴포넌트 타입이 React.FC를 쓰고 있는 것을 알았다. 이 방식은 문제가 있다. 참고

  • "암묵적으로 children props 가 포함된다"
type PropsWithChildren<P> = P & { children?: ReactNode | undefined };
  • "제네릭 문법을 지원하지 않는다."
type GenericComponentProps<T> = {
  prop: T;
  callback: (t: T) => void;
};

const GenericComponent = <T>(props: GenericComponentProps<T>) => {
  /*...*/
};

const GenericComponent: React.FC</* 제네릭을 받는 props 를 전달할 수 없다. */> = <T>(
  props: GenericComponentProps<T>
) => {
  /*...*/
};
  • "불필요한 속성들을 포함한다." 리액트 컴포넌트는 propTypes, defaultProps 등과 같은 속성을 가질 수 있다. 이러한 속성들은 타입스크립트를 사용하지 않는 경우 해당 컴포넌트가 받는 props 들에 대한 타입 정의 역할을 수행할 수 있고, 기본값 등을 설정하는데 도움을 주는데, FC 타입으로 선언된 컴포넌트의 경우 defaultProps 속성이 먹히질 않는다.

typescript-apollo-next로 생성된 타입은 아래와 같이 만들어진다.

export type PageFindAllPostsPaginationComp = React.FC<{
  data?: Types.FindAllPostsPaginationQuery;
  error?: Apollo.ApolloError;
}>;

React.Fc만 걷어낸 타입도 생성해주면 좋을텐데 그런 옵션은 없었다. 따라서 이렇게 생성된 React.FunctionComponent으로 만들어진 컴포넌트 타입의 제네릭을 다시 꺼내는 조건부 타입을 만들어서 사용하였다.

type ExtractPropType<T> = T extends React.FunctionComponent<infer X> ? X : never;

type PropType = ExtractPropType<PageFindAllPostsPaginationComp>;

이제 withPage방식을 시작하기 전에 GraphQL 오퍼레이터는 아래와 같이 만들고 제너레이션 시켜준다. codegen.yml설정은 지난 2022-08-28-tWIL 참고

query findAllPostsPagination($take: Int, $skip: Int) {
  findManyPost(take: $take, skip: $skip) {
    id
    title
    content
    author {
      name
      _count {
        posts
      }
    }
    authorId
    referredBlogs
  }
  aggregatePost {
    _count {
      id
    }
  }
}

Pagination을 위한 변수까지 설정하고 생성하면

page.tsx
export async function getServerPageFindAllPostsPagination(
  options: Omit<
    Apollo.QueryOptions<Types.FindAllPostsPaginationQueryVariables>,
    "query"
  >,
  ctx: ApolloClientContext,
) {
  const apolloClient = getApolloClient(ctx);

  const data = await apolloClient.query<Types.FindAllPostsPaginationQuery>({
    ...options,
    query: Operations.FindAllPostsPaginationDocument,
  });

  const apolloState = apolloClient.cache.extract();

  return {
    props: {
      apolloState: apolloState,
      data: data?.data,
      error: data?.error ?? data?.errors ?? null,
    },
  };
}
export const useFindAllPostsPagination = (
  optionsFunc?: (
    router: NextRouter,
  ) => QueryHookOptions<
    Types.FindAllPostsPaginationQuery,
    Types.FindAllPostsPaginationQueryVariables
  >,
) => {
  const router = useRouter();
  const options = optionsFunc ? optionsFunc(router) : {};
  return useQuery(Operations.FindAllPostsPaginationDocument, options);
};
export type PageFindAllPostsPaginationComp = React.FC<{
  data?: Types.FindAllPostsPaginationQuery;
  error?: Apollo.ApolloError;
}>;
export const withPageFindAllPostsPagination =
  (
    optionsFunc?: (
      router: NextRouter,
    ) => QueryHookOptions<
      Types.FindAllPostsPaginationQuery,
      Types.FindAllPostsPaginationQueryVariables
    >,
  ) =>
  (WrappedComponent: PageFindAllPostsPaginationComp): NextPage =>
  (props) => {
    const router = useRouter();
    const options = optionsFunc ? optionsFunc(router) : {};
    const { data, error } = useQuery(
      Operations.FindAllPostsPaginationDocument,
      options,
    );
    return <WrappedComponent {...props} data={data} error={error} />;
  };
export const ssrFindAllPostsPagination = {
  getServerPage: getServerPageFindAllPostsPagination,
  withPage: withPageFindAllPostsPagination,
  usePage: useFindAllPostsPagination,
};

마지막 ssrFindAllPostsPagination를 보통 가져와서 사용하며, getServerPage, withPage, usePage를 사용할 수 있다. usePageuseQuery를 사용하기 때문에 여기서는 제외하고, withPage 방식을 구현해보자.

withPage

일단 이 오퍼레이터의 타입을 정리하는 함수를 만들어준다.

function getPosts(data: FindAllPostsPaginationQuery | undefined) {
  if (!data) return defaultData;
  const { findManyPost } = data;
  const posts = compact(
    findManyPost.map((post) => {
      if (!post) return null;
      const { id, title, content, author } = post;
      if (!author) return null;
      const { name } = author;
      return { id, title, content, name };
    })
  );
  return posts;
}

const defaultData = [{ id: "", title: "", content: "", name: "" }];

이렇게 id, title, content, name을 배열로 리턴하게 한다. withPage는 아래와 같이, withApollo와 함께 사용한다.

import React from "react";
import Layout from "Layouts";
import { withApollo } from "withApollo";
import { ssrFindAllPostsPagination, PageFindAllPostsPaginationComp } from "generated/page";
import { FindAllPostsPaginationQuery } from "generated/types";
import { compact } from "lodash";

type ExtractGeneric<Type> = Type extends React.FunctionComponent<infer X> ? X : never;

type PropType = ExtractGeneric<PageFindAllPostsPaginationComp>;

function ListPosts(props: PropType) {
  console.log(props);
  return (
    <Layout title="Project List">
      {getPosts(props.data).map((item) => {
        return (
          <React.Fragment key={item.id}>
            <div>
              Title: {item.title}, Content: {item.content}, Author: {item.name}
            </div>
          </React.Fragment>
        );
      })}
    </Layout>
  );
}

export default withApollo(
  ssrFindAllPostsPagination.withPage((arg) => ({
    variables: {
      take: 10,
      skip:
        (arg?.query?.page as unknown as number) == 1
          ? 0
          : ((arg?.query?.page as unknown as number) - 1) * 10 || 0,
    },
  }))(ListPosts)
);

컴포넌트는 대충만들었기 때문에, 29-30라인을 보면 HOC로 default export한다.

getServerPage

이제 getServerPage방식으로 구현해보자.

import React from "react";
import Layout from "Layouts";
import { ssrFindAllPostsPagination, PageFindAllPostsPaginationComp } from "generated/page";
import { FindAllPostsPaginationQuery } from "generated/types";
import { compact } from "lodash";

type ExtractPropType<T> = T extends React.FunctionComponent<infer X> ? X : never;

type PropType = ExtractPropType<PageFindAllPostsPaginationComp>;

function SSRListPost(props: PropType) {
  console.log(props);
  return (
    <Layout title="Project List">
      {getPosts(props.data).map((item) => {
        return (
          <React.Fragment key={item.id}>
            <div>
              Title: {item.title}, Content: {item.content}, Author: {item.name}
            </div>
          </React.Fragment>
        );
      })}
    </Layout>
  );
}

export const getServerSideProps = ssrFindAllPostsPagination.getServerPage;

export default SSRListPost;

withPage에 비해 상당히 간단해 보이는데, variables를 넣지 않았기 때문이다. 넣는다고 해도 코드가 복잡할 것 같진 않다.

page.tsx[withPage]
// omitted...
export const withPageFindAllPostsPagination =
  (
    optionsFunc?: (
      router: NextRouter,
    ) => QueryHookOptions<
      Types.FindAllPostsPaginationQuery,
      Types.FindAllPostsPaginationQueryVariables
    >,
  ) =>
  (WrappedComponent: PageFindAllPostsPaginationComp): NextPage =>
  (props) => {
    const router = useRouter();
    const options = optionsFunc ? optionsFunc(router) : {};
    const { data, error } = useQuery(
      Operations.FindAllPostsPaginationDocument,
      options,
    );
    return <WrappedComponent {...props} data={data} error={error} />;
  };

withPage의 경우 HOC를 사용하여, 결국 useQuery가 내부에 있는것이라고 볼 수 있고,

page.tsx[getServerPage]
export async function getServerPageFindAllPostsPagination(
  options: Omit<
    Apollo.QueryOptions<Types.FindAllPostsPaginationQueryVariables>,
    "query"
  >,
  ctx: ApolloClientContext,
) {
  const apolloClient = getApolloClient(ctx);

  const data = await apolloClient.query<Types.FindAllPostsPaginationQuery>({
    ...options,
    query: Operations.FindAllPostsPaginationDocument,
  });

  const apolloState = apolloClient.cache.extract();

  return {
    props: {
      apolloState: apolloState,
      data: data?.data,
      error: data?.error ?? data?.errors ?? null,
    },
  };
}

getServerPage가 SSR로 렌더링을 하고 있는 것을 확인할 수 있다. 컴포넌트에서는 props를 로그했을 때 터미널에 기록되는 것으로, 아니면 페이지 소스보기 형태로 서버에서 렌더링이 된 것을 확인할 수 있다.

getServerPage는 next의 getStaticPropsgetStaticPaths를 활용하여 SSG로 렌더링 후 useQuery로 다시 서버 데이터를 패치하면 될 것 같다.

이제 이 조합을 구현하려면 적절한 예제를 찾아야 하는데 아직 못찾았다. 만약 업데이트가 잦은 데이터빌드하고 배포하는 시점에만 필요한 데이터 그리고 인터렉션 가능 상태에서 사용자 UI로 변동이 발생하는 데이터 이 3가지의 경우 getServerPagewithPage HOC를 걸고 컴포넌트 내부에서는 useQuery를 사용하면 될 것 같다.

다음 주 할일

  • AWS Copilot => CDK혹은 Terraform 마이그레이션
  • Cognito 에서 SuperTokens 로 마이그레이션
  • SuperTokens with Prisma 테스트 및 ECS 배포
  • 모델링 OSS 데이터 glTF로 변환하여 Three.js 렌더링
  • 변환된 모델링 데이터 메타데이터 분석

위의 일들을 진행해보고 다음 tWIL에 정리할 예정이다.