4월 둘째주 인턴일지 🤓: 리소스에 따라 list, read, edit 페이지 자동으로 생성하기 (슈퍼어드민)

Ko Seoyoung·2021년 4월 22일
0

왜 만들었을까?

우리는 장고에서 제공하는 장고 어드민을 기본 어드민 홈페이지로 사용하고 있었다. 하지만, 백앤드를 장고->node.js로 이전하면서 기존 장고 어드민을 사용할 수 없게 될 것이고, 따라서 같은 기능을 하는 어드민 프로그램이 필요해졌다!

이를 슈퍼어드민에 구현하기로 결정했다. 각 리소스는 기본적으로 list, read, edit, delete과 같은 기능들이 필요했다.

각각의 리소스는 각기 다른 field를 갖고, 어떤 리소스는 자식 필드를 추가하거나 삭제하는 등의 특별한 action을 추가적으로 필요로 할 수도 있다. 하지만 이 같은 이유로 각 리소스에 대한 페이지를 각각 만든다면??

User: UserListPage, UserReadPage, UserEditPage
Item: ItemListPage, ItemReadPage, ItemEditPage
....

분명 같은 구조를 갖는 페이지이므로 WET한 코드를 작성하게 될 것이다.
(DRY: 'Don't Repeat Yourself' / WET: 'Write Everything Twice' - 얼마전에 알게된 줄임말 한번 써봄ㅎ)

이런 문제를 해결하기위해, Admin Bro의 리소스 구조를 참고하여 리소스를 만들고 (참고링크), 제공된 리소스에 따라 list, read, edit 페이지를 자동으로 생성하게끔 어드민 페이지를 만들어보았다.

--> adminbro demo site


Index

  1. base resource 인터페이스 구조
  2. 필요한 페이지
  3. 알게된 next.js 개념 (getStaticPaths, getStaticProps란?)
  4. 그 외 알게된 점, 자잘한 것 등등

1. base resource 인터페이스 구조

export interface IResource {
  type: string;
  title: string;
  properties: Record<string, TResourceProperty>;
  actions?: Record<string, TResourceAction>;
}

base resource의 인터페이스는 위와 같다.

  • type: User, Item 등의 resource의 타입
  • title: 'User -> 사용자'라고 적어서 '사용자 목록', '사용자 추가', '사용자 편집' 등에 쓰는 용도
  • properties: 리소스의 column 요소와 같이 목록에 보여주거나, 생성, 편집할 모든 속성들을 적는다. type은 리소스 속성의 타입을 명시하는 field로 input 형태나 view 형태를 결정한다. 나는 이를 5가지 타입으로 분류했다.
export type TResourcePropertyType =
  | 'string'
  | 'number'
  | 'boolean'
  | 'date'
  | 'object';

Ex) 판매자(Seller) 리소스의 properties 예:

  properties: Record<string, TResourceProperty> = {
    id: {
      label: 'id',
      type: { name: 'number', required: true },
      isVisible: {
        add: false,
        edit: false,
      },
    },
    businessCode: {
      label: '사업자등록번호',
      type: { name: 'string', required: true },
    },
    businessName: {
      label: '회사명',
      type: { name: 'string', required: true },
    },
    phoneNumber: {
      label: '고객센터',
      type: { name: 'string', required: true },
    },
    // ...중략
    shippingPolicy: {
      label: '배송정책',
      type: { name: 'object' },
      name: {
        add: 'shippingPolicyInput',
      },
      isVisible: {
        list: false,
        edit: false,
      },
      fieldsEditHook: { // object를 편집하기 위한 훅
        handlerHook: () => {
          const [update] = useUpdateMySellerShippingPolicy();
          return update;
        },
        getVariables: 'updateSellerShippingPolicyInput',
      },
      fields: { // properties와 타입이 비슷하다 (fieldsEditHook, fields 만 제외한 구조)
        fee: {
          label: 'fee',
          type: { name: 'number', required: true },
        },
        minimumAmountForFree: {
          label: 'minimumAmountForFree',
          type: { name: 'number', required: true },
        },
      },
    },
    createdAt: {
      // ...
    },
    updatedAt: {
      // ...
    },
  };
  • actions: 액션은 리소스에서 가능한 액션들을 정의하는 필드이다.
    액션의 타입은 resouce와 record 두 종류로 나눠볼 수 있다.
    resouce에는 list, create와 같은 전체 테이블의 액션을 지정할 수 있고, recordread, edit과 같이 테이블에서 각 record에 대한 액션을 지정할 수 있다.
export type TResourceAction = {
  type: TResourceActionType;
  name: string;
  hook?: THook; // 각 action을 실행하는 hook을 적어준다
};

ex) 판매자(Seller) 리소스의 actions 예:

actions: Record<string, TResourceAction> = {
    list: {
      type: 'resource',
      name: '목록',
      hook: {
        queryHook: useSellers,
        dataName: 'sellers',
      },
    },
    read: {
      type: 'resource',
      name: '읽기',
      hook: {
        queryHook: useSeller,
        dataName: 'seller',
      },
    },
    add: {
      type: 'resource',
      name: '추가',
      hook: {
        handlerHook: () => {
          const [add] = useCreateSeller();
          return add;
        },
        getVariables: 'createSellerInput',
      },
    },
    edit: {
      type: 'record',
      name: '수정',
      hook: {
        handlerHook: () => {
          const [edit] = useUpdateMeSeller();
          return edit;
        },
        getVariables: 'updateSellerInput',
      },
    },
  };

2. 필요한 페이지


(adminbro url들을 참고해서 만들어보았다!)

[resourceType]/index.tsx

👉 테이블의 리스트를 보여주는 페이지!
query로 resourceType에 해당하는 값을 받는다. 즉, resourceType이 User라면 User리스트를, resourceType이 Item이라면 Item 리스트를 보여주는 페이지가 생성된다.

👀 /courier 결과 화면

[resourceType]/actions/[resourceActionType].tsx/index.tsx

👉 resourceType의 리소스에 해당하는 액션 페이지!
ex) 택배사(Courier) 테이블에 새로운 택배사 추가

👀 /courier/actions/add 결과 화면

[resourceType]/records/[id]/[recordActionType].tsx

👉 resourceType의 레코드에 해당하는 액션 페이지!
resourceType의 record 중 query로 받은 id에 관한 action을 수행할 수 있고, 가능한 record action은 read, edit 등이 있다.
ex) 택배사(Courier) 테이블의 id 8에 해당하는 택배사 상세 정보

👀 /courier/records/8/read 결과 화면

3. 알게된 next.js 개념 (getStaticProps, getStaticPaths, getServerSideProps란?)

공식문서: https://nextjs.org/docs/basic-features/data-fetching

Next.js는 기본적으로 모든 페이지를 pre-rendering한다.
(pre(미리, 사전에)-rendering(HTML을 만들어놓는다), 성능이 더 좋고 SEO(검색엔진최적화)에 좋음)

pre-rendering에는 (1) Static Generation 과 (2) Server-side Rendering으로 두 가지 형태가 있는데, 둘의 가장 큰 차이점은 언제 페이지의 HTMl을 생성하느냐에 있다.

  • Static Generation: 빌드타임에 HTML이 만들어지고, 이 HTML은 매 요청마다 재사용된다.
  • Server-side Rendering: 매 요청마다 HTML이 만들어진다.
    (Next.js에서는 성능면에서 Static Generation 사용을 더 권장한다. 정적으로 생성된 페이지는 CDN에 캐시될 수 있기 때문이다.
    하지만, 어떤 경우는 Server-side Rendering을 할 수 밖에 없는 상황도 있다!)

무튼 이러한 개념들을 토대로 pre-rendering에서 data를 fetch 할 때 사용되는 함수들인 getStaticProps, getStaticPaths, getServerSideProps가 뭔지에 대해 짚고 넘어가자!

getStaticProps & getStaticPaths란?

getStaticProps와 getStaticPaths는 Static Generation에서 사용되는 함수들이다.

  • getStaticProps(async)에서는 빌드타임에 데이터를 fetch하고,
  • getStaticPaths에서는 데이터에 기반하여 페이지를 pre-render하기 위해 dynamic routes를 명시한다.
    (Dynamic Routes: 미리 정의된 path가 아닌 동적으로 생성될 수 있는 경로를 말한다. /[resourceType]는 dynamic route이다)

사용예시: [resourceType]/index.tsx

export const getStaticPaths = () => {
  const filteredResources = Object.values(resources).filter((resource) =>
    Object.keys(new resource().actions).includes('list')
  );

  const paths = filteredResources.map((resource) => ({
    params: { resourceType: new resource().type },
  }));
  
  return {
    paths: paths,
    fallback: false,
  };
};

export const getStaticProps = async (context) => {
  const resourceType = context.params.resourceType;

  return {
    props: {
      resourceType,
    },
  };
};
  • 예시의 getStaticPaths에서는 list action이 있는 리소스들만을 필터링한 후 Dynamic Routes에서 생성 가능한 paths를 명시한다. 즉, 필터링된 리소스가 User, Item, Seller 만 이라면 resourceType으로는 이 세가지만 올 수 있고, 가능한 path는 /User, /Item, /Seller가 된다는 것을 명시하는 것이다.

  • 이때, fallback이라는 필드를 반환하는데 fallback이 false면, 가능한 Path (ex) /User, /Item, /Seller)에 해당하는 html는 빌드타임에 만들어놓고 이 외의 /Review와 같은 다른 path가 오면 404 페이지를 보여준다는 것이다. 반면 fallback이 true면 빌드 타임에 만들어놓지 않은 html을 요청해도 404 페이지를 보여주지 않고, fallback 버전의 /Review 페이지를 제공한다. (그 후 같은 요청이 들어오면 pre-render된 /Review 페이지를 보여줌) fallback이 true인 경우가 유용한 상황은 100만개의 아이템이 있는 쇼핑몰사이트에서 아이템 상세페이지를 보여주는 경우 등 매우 큰 숫자의 정적 페이지를 생성하는 경우에 유용하다! (왜냐면 100만개의 아이템 상세페이지를 모두 미리 만들어 놓을 수는 없으니까!)

  • getStaticProps에서는 context를 인자로 받아 기본적으로 props를 반환한다. (revalidate, notFound, redirect를 반환하기도함 -> 상세문서 참고) props로 해당 페이지 컴포넌트의 props으로 넘겨질 props을 반환한다. (이때 prop은 serializable object이어야 함)

// ResourceListPage는 getStaticProps으로부터 resourceType을 넘겨받음
export default function ResourceListPage({ resourceType }) {
	return <>/* ... */</>
}

getServerSideProps란?

페이지에서 getServerSideProps 함수를 비동기(async)로 호출하면, Next.js는 페이지에 대한 요청이 있을 때 마다 getServerSideProps에서 반환하는 데이터를 사용하여 해당 페이지를 pre-render한다.

getStaticProps과 용도가 비슷하지만 Server-side Rendering에서 사용된다는 점에서 차이가 있다!

4. 그 외 알게된 점, 자잘한 것 등등

🐛 오류 수정: mutate이후 cache업데이트가 안되어 새로고침을 해야 정보가 업데이트된 경우

  1. mutate이후 cache 업데이트가 되려면 mutation 결과로 id를 반환해야한다.
  2. 반환된 Entity의 __typename이 같아야한다.
profile
Web Frontend Developer 👩🏻‍💻 #React #Nextjs #ApolloClient

0개의 댓글