우리는 장고에서 제공하는 장고 어드민을 기본 어드민 홈페이지로 사용하고 있었다. 하지만, 백앤드를 장고->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 페이지를 자동으로 생성하게끔 어드민 페이지를 만들어보았다.
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
에는 list, create와 같은 전체 테이블의 액션을 지정할 수 있고, record
는 read, 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',
},
},
};
(adminbro url들을 참고해서 만들어보았다!)
👉 테이블의 리스트를 보여주는 페이지!
query로 resourceType에 해당하는 값을 받는다. 즉, resourceType이 User라면 User리스트를, resourceType이 Item이라면 Item 리스트를 보여주는 페이지가 생성된다.
👀 /courier
결과 화면
👉 resourceType의 리소스에 해당하는 액션 페이지!
ex) 택배사(Courier) 테이블에 새로운 택배사 추가
👀 /courier/actions/add
결과 화면
👉 resourceType의 레코드에 해당하는 액션 페이지!
resourceType의 record 중 query로 받은 id에 관한 action을 수행할 수 있고, 가능한 record action은 read, edit 등이 있다.
ex) 택배사(Courier) 테이블의 id 8에 해당하는 택배사 상세 정보
👀 /courier/records/8/read
결과 화면
공식문서: 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을 생성하느냐에 있다.
무튼 이러한 개념들을 토대로 pre-rendering에서 data를 fetch 할 때 사용되는 함수들인 getStaticProps
, getStaticPaths
, getServerSideProps
가 뭔지에 대해 짚고 넘어가자!
getStaticProps와 getStaticPaths는 Static Generation
에서 사용되는 함수들이다.
/[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 함수를 비동기(async)로 호출하면, Next.js는 페이지에 대한 요청이 있을 때 마다 getServerSideProps에서 반환하는 데이터를 사용하여 해당 페이지를 pre-render한다.
getStaticProps과 용도가 비슷하지만 Server-side Rendering에서 사용된다는 점에서 차이가 있다!
🐛 오류 수정: mutate이후 cache업데이트가 안되어 새로고침을 해야 정보가 업데이트된 경우