- Published on
tWIL 2022.08 4주차
- Authors
- Name
- eunchurn
- @eunchurn
수요일 저녁에 라이딩을 했다. 18키로 라이딩 했다고 몸의 면역력이 떨어진 40대. 한강변 자전거 도로는 아직도 공사중인 곳이 있다. 한강변을 달릴 땐 기분이 매우 좋다. 경관이 좋기도 하고 뻥뚫려 있어서 시원하다. 주말엔 여의도까지 갈 수 있을지 tWIL쓰고 다녀올 기세.
지난주 tWIL에서 Notion API로 Vercel과 AWS Amplify 모두 배포해보고 얻은 결과는 Vercel은 컨텐츠가 즉각 변경되는 점에 비해 Amplify는 5-10분 정도 걸린다.(Update 2022.09.02 더 걸린다. 20분 이상. SSR에 문제가 있다.) API를 캐싱해두고 작동하는 것 같은데 이려러고 돈내고 쓰는게 아닐텐데 어디 설정도 찾기 어렵다. 그렇다고 Vercel이 전세계 CDN에 배포가 안되는 것도 아니고 너무 비교가 된다. 빨리 Amplify를 벗어나고 싶다.
Container components to Hooks
React 컴포넌트 Container, Presenter 패턴으로 개발하다 보면 Presenter 스타일이나 스타일드 컴포넌트드은 Styles.tsx
를 만들어 사용한다. 그리고 Container에서 type validator나 커스텀 패턴을 만들기 위한 Types.tsx
를 만들고 여기에 Presenter의 PropTypes를 만들어 낸다. 이런 컴포넌트들은 특정 폴더로 구분하고 Container는 index.tsx
를 사용한다. 폴더 내 package.json
을 만들어 main
필드에 index.tsx
를 두기도 한다. 이런 패턴으로 개발해온지 오래되었는데, graphql
오퍼레이션들도 같은 폴더에 위치하면 정리가 잘되는 편이였다. 그러나 Container 사이즈가 커지는 경우 대비가 잘 안되어있었는데 난 개발팀원들에게 코드라인은 120줄을 넘지 않도록, 넘더라도 160줄까진 봐주겠다고 했다. 이러한 가이드를 제시한 이유는 컨플릭이 발생했을 경우 resolve하기 어렵기 때문이다. 그리고 개발자들에겐 코드를 최적화 하기 마련이다. Container의 경우는 각종 useEffect
, useCallback
그리고 핸들러들이 무수히 많아질 수 밖에 없다. 여기에 한 패턴을 더 추가하여 custom hook을 만드는 방법으로 Container 사이즈를 줄이려고 한다.
여러 state
들이 혼재하고 핸들러들이 혼재할 때 대부분 핸들러들이나 함수들은 클로저로 state
변수들을 담는다. 예를들어 파일업로드 하는 컨테이너가 있다고 하자. Presenter 컴포넌트는 input에 업로드 button이 달려있고, 여기에 파일 업로드 핸들러 및 파일 업로드 Mutation 함수 그리고 드래그앤드롭, 파일 업로드 진행상태 Progress bar 까지 구현한다고 가정하면, 아래와 같은 크나 큰 컨테이너가 만들어진다. 이보다 더 커지는 컨테이너 예제도 있을 수 있지만 여기를 리팩토링할 수 있다면 다른 곳도 정리가 쉬울 것 같다.
import React from "react";
import { useDropzone } from "react-dropzone";
import {
useAppSelector,
useAppDispatch,
uploadSingleComplete,
designDocumentsUpload,
} from "store";
import {
useUploadOneFileMutation,
UploadStatus,
DocumentType,
} from "generated/types";
import { useSnackbar } from "notistack";
import { getFileTitle } from "utils";
import { Presenter } from "./Presenter";
export function UploadContainer() {
const [progress, setProgress] = React.useState(0);
const [uploadedFilename, setUploadedFilename] = React.useState("");
const [uploadedAt, setUploadedAt] = React.useState<string | null>(null);
const {
designDocuments: { [designKey]: designFile },
id,
} = useAppSelector((state) => state.project);
const [fileCount, setFileCount] = React.useState(0);
const authState = useAppSelector((state) => state.auth);
const dispatch = useAppDispatch();
const ref = React.useRef<HTMLInputElement>(null);
const { getRootProps, isDragActive } = useDropzone({
onDrop: (files) => {
if (!files) return;
const file = files[0];
setFileCount(files.length);
if (designKey === DocumentType.Etc && files.length > 1) {
return;
}
uploadFile(file, designKey);
},
});
const { enqueueSnackbar } = useSnackbar();
const uploadFile = (file: File, designKey: DocumentType) => {
const uploadUrl = `https://some-bucket.s3.amazonaws.com/myfile/${id}`;
dispatch(
designDocumentsUpload({
file,
designKey,
progressCallback(progress) {
setProgress((progress.loaded / progress.total) * 100);
},
projectId: id,
}),
).then(() => {
uploadSingleFileUpdate(
id,
designKey,
`${uploadUrl}/${file.name}`,
file.name,
);
});
};
const handleUpload =
(key: DocumentType) =>
async (event: React.ChangeEvent<HTMLInputElement>) => {
const {
target: { files },
} = event;
if (!files) return;
const file = files[0];
uploadFile(file, key);
};
const handleClick = React.useCallback(() => {
ref.current?.click();
}, [ref]);
const [uploadOne] = useUploadOneFileMutation();
const uploadSingleFileUpdate = React.useCallback(
(
projectId: number,
documentType: DocumentType,
url: string,
filename: string,
) => {
uploadOne({
variables: {
data: {
url,
filename,
documentType,
status: UploadStatus.Success,
project: { connect: { id: projectId } },
},
updateManyFileData: { status: { set: UploadStatus.Failed } },
updateManyFileWhere: {
projectId: { equals: projectId },
documentType: { equals: documentType },
},
},
}).then((data) => {
dispatch(uploadSingleComplete(documentType));
const filename = data.data?.createOneFile.filename;
const createdAt = data.data?.createOneFile.createdAt;
enqueueSnackbar(
`${getFileTitle(designKey)} 파일이 "업로드" 되었습니다.`,
{ variant: "success" },
);
setUploadedFilename(filename || "");
setUploadedAt(createdAt);
});
},
[designKey, dispatch, enqueueSnackbar, replacement, uploadOne],
);
return (
<Presenter
{...{
handleUpload,
handleClick,
isDragActive,
getRootProps,
progress,
ref,
file: designFile,
fileCount,
}}
/>
);
}
Presenter는 위에서 내려받은 props로 잘 만들면 되고, 여기서는 컨테이너 정리로 리팩토링 한다고 하면, 일단 하나의 큰 hook을 뽑아낸다. 말이 어렵지 Presenter return을 제외한 모든 코드를 useUpload.ts
라는 파일을 만들어 빼낸다.
import React from "react";
import { useDropzone } from "react-dropzone";
import {
useAppSelector,
useAppDispatch,
uploadSingleComplete,
designDocumentsUpload,
} from "store";
import {
useUploadOneFileMutation,
UploadStatus,
DocumentType,
} from "generated/types";
import { useSnackbar } from "notistack";
import { getFileTitle } from "utils";
import { Presenter } from "./Presenter";
export function useUpload(designKey: DocumentType, replacement?: boolean) {
const [progress, setProgress] = React.useState(0);
const [uploadedFilename, setUploadedFilename] = React.useState("");
const [uploadedAt, setUploadedAt] = React.useState<string | null>(null);
const {
designDocuments: { [designKey]: designFile },
id,
} = useAppSelector((state) => state.project);
const [fileCount, setFileCount] = React.useState(0);
const authState = useAppSelector((state) => state.auth);
const dispatch = useAppDispatch();
const ref = React.useRef<HTMLInputElement>(null);
const { getRootProps, isDragActive } = useDropzone({
onDrop: (files) => {
if (!files) return;
const file = files[0];
setFileCount(files.length);
if (designKey === DocumentType.Etc && files.length > 1) {
return;
}
uploadFile(file, designKey);
},
});
const { enqueueSnackbar } = useSnackbar();
const uploadFile = (file: File, designKey: DocumentType) => {
const uploadUrl = `https://some-bucket.s3.amazonaws.com/myfile/${id}`;
dispatch(
designDocumentsUpload({
file,
designKey,
progressCallback(progress) {
setProgress((progress.loaded / progress.total) * 100);
},
projectId: id,
}),
).then(() => {
uploadSingleFileUpdate(
id,
designKey,
`${uploadUrl}/${file.name}`,
file.name,
);
});
};
const handleUpload =
(key: DocumentType) =>
async (event: React.ChangeEvent<HTMLInputElement>) => {
const {
target: { files },
} = event;
if (!files) return;
const file = files[0];
uploadFile(file, key);
};
const handleClick = React.useCallback(() => {
ref.current?.click();
}, [ref]);
const [uploadOne] = useUploadOneFileMutation();
const uploadSingleFileUpdate = React.useCallback(
(
projectId: number,
documentType: DocumentType,
url: string,
filename: string,
) => {
uploadOne({
variables: {
data: {
url,
filename,
documentType,
status: UploadStatus.Success,
project: { connect: { id: projectId } },
},
updateManyFileData: { status: { set: UploadStatus.Failed } },
updateManyFileWhere: {
projectId: { equals: projectId },
documentType: { equals: documentType },
},
},
}).then((data) => {
dispatch(uploadSingleComplete(documentType));
const filename = data.data?.createOneFile.filename;
const createdAt = data.data?.createOneFile.createdAt;
enqueueSnackbar(
`${getFileTitle(designKey)} 파일이 "업로드" 되었습니다.`,
{ variant: "success" },
);
setUploadedFilename(filename || "");
setUploadedAt(createdAt);
});
},
[designKey, dispatch, enqueueSnackbar, replacement, uploadOne],
);
return {
uploadSingleFileUpdate,
handleUpload,
handleClick,
uploadFile,
isDragActive,
getRootProps,
progress,
ref,
file: designFile,
uploadedFilename,
uploadedAt,
fileCount,
};
이렇게 hooks을 빼어놓으면 컨테이너는 너무 간단해진다.
import React from "react";
import { useUpload } from "./useUpload";
export function UploadContainer() {
const {
handleUpload,
handleClick,
isDragActive,
getRootProps,
progress,
ref,
file: designFile,
fileCount,
} = useUpload();
return (
<Presenter
{...{
handleUpload,
handleClick,
isDragActive,
getRootProps,
progress,
ref,
file: designFile,
fileCount,
}}
/>
);
}
이제 이 hook을 맥락에 맞게 여러개의 hook으로 수정한다. 커플링된 함수들은 디커플링을 하고, state
변경에 의해 함수가 변경이 예정되어 있는 함수들은 arguments로 이 state를 받도록 한다. state
를 변경시키는 함수는 별도로 함수로 떼어낸다. 여기서는 handleUpload
와 handleClick
은 다른 맥락으로 동작하기 때문에 분리한다. handleClick
과 isDragActive
그리고 ref
는 같은 맥락으로 동작하기 때문에 합쳐서 다른 hook으로 만들어 준다. 여기서는 hooks 안에서 사용하도록 한다.
import React from "react";
import { useDropzone } from "react-dropzone";
import { DocumentType } from "generated/types";
interface UploadDnDHookProps {
uploadFile: (file: File, designKey: DocumentType) => void;
designKey: DocumentType;
}
export function useUploadDnD(hookProps: UploadDnDHookProps) {
const { uploadFile, designKey } = hookProps;
const [fileCount, setFileCount] = React.useState(0);
const ref = React.useRef<HTMLInputElement>(null);
const { getRootProps, isDragActive } = useDropzone({
onDrop: (files) => {
if (!files) return;
const file = files[0];
setFileCount(files.length);
if (designKey === DocumentType.Etc && files.length > 1) {
return;
}
uploadFile(file, designKey);
},
});
const handleClick = React.useCallback(() => {
ref.current?.click();
}, [ref]);
return {
handleClick,
isDragActive,
getRootProps,
ref,
fileCount,
};
}
useUploadDnD()
hook을 분리하고 useUpload()
hook을 정리하면
import React from "react";
import {
useAppSelector,
useAppDispatch,
uploadSingleComplete,
designDocumentsUpload,
} from "Store";
import {
useUploadOneBlueprintMutation,
UploadStatus,
DocumentType,
} from "generated/types";
import { useSnackbar } from "notistack";
import { getBlueprintTitle } from "utils";
import { useUploadDnD } from "./useUploadDnD";
export function useUpload(designKey: DocumentType, replacement?: boolean) {
const [progress, setProgress] = React.useState(0);
const [uploadedFilename, setUploadedFilename] = React.useState("");
const [uploadedAt, setUploadedAt] = React.useState<string | null>(null);
const {
designDocuments: { [designKey]: designFile },
id,
} = useAppSelector((state) => state.project);
const authState = useAppSelector((state) => state.auth);
const dispatch = useAppDispatch();
const { enqueueSnackbar } = useSnackbar();
const uploadFile = (file: File, designKey: DocumentType) => {
const uploadUrl = `https://some-bucket.s3.amazonaws.com/myfile/${id}`;
dispatch(
designDocumentsUpload({
file,
designKey,
progressCallback(progress) {
setProgress((progress.loaded / progress.total) * 100);
},
projectId: id,
}),
).then(() => {
uploadSingleFileUpdate(
id,
designKey,
`${uploadUrl}/${file.name}`,
file.name,
);
});
};
const { handleClick, isDragActive, getRootProps, ref, fileCount } =
useUploadDnD({ uploadFile, designKey });
const handleUpload =
(key: DocumentType) =>
async (event: React.ChangeEvent<HTMLInputElement>) => {
const {
target: { files },
} = event;
if (!files) return;
const file = files[0];
uploadFile(file, key);
};
const [uploadOne] = useUploadOneBlueprintMutation();
const uploadSingleFileUpdate = React.useCallback(
(
projectId: number,
documentType: DocumentType,
url: string,
filename: string,
) => {
uploadOne({
variables: {
data: {
url,
filename,
documentType,
status: UploadStatus.Success,
project: { connect: { id: projectId } },
},
updateManyBlueprintData: { status: { set: UploadStatus.Failed } },
updateManyBlueprintWhere: {
projectId: { equals: projectId },
documentType: { equals: documentType },
},
},
}).then((data) => {
dispatch(uploadSingleComplete(documentType));
const filename = data.data?.createOneBlueprint.filename;
const createdAt = data.data?.createOneBlueprint.createdAt;
enqueueSnackbar(
`${getBlueprintTitle(designKey)} 파일이 ${
replacement ? "변경" : "업로드"
}되었습니다.`,
{ variant: "success" },
);
setUploadedFilename(filename || "");
setUploadedAt(createdAt);
});
},
[designKey, dispatch, enqueueSnackbar, replacement, uploadOne],
);
return {
uploadSingleFileUpdate,
handleUpload,
handleClick,
uploadFile,
isDragActive,
getRootProps,
progress,
ref,
file: designFile,
uploadedFilename,
uploadedAt,
fileCount,
};
}
아직도 길다. uploadFile
이란 함수는 uploadSingleFileUpdate
이란 함수를 인자로 받아서 수행하도록 하여 함수를 분리시켜서 다른 hook을 만들어주도록 한다.
import React from "react";
import { useAppSelector, useAppDispatch, designDocumentsUpload } from "Store";
import { DocumentType } from "generated/types";
interface UploadFileHookProps {
uploadSingleFileUpdate: (
projectId: number,
documentType: DocumentType,
url: string,
filename: string,
) => void;
progressCallback: (progress: any) => void;
}
export function useUploadFile(hookProps: UploadFileHookProps) {
const { uploadSingleFileUpdate, progressCallback } = hookProps;
const authState = useAppSelector((state) => state.auth);
const { id } = useAppSelector((state) => state.project);
const dispatch = useAppDispatch();
const uploadFile = (file: File, designKey: DocumentType) => {
const uploadUrl = `https://some-bucket.s3.amazonaws.com/myfile/${id}`;
dispatch(
designDocumentsUpload({
file,
designKey,
progressCallback,
projectId: id,
}),
).then(() => {
uploadSingleFileUpdate(
id,
designKey,
`${uploadUrl}/${file.name}`,
file.name,
);
});
};
return { uploadFile };
}
이제 2종류의 hook을 사용하여 useUpload.ts
가 정리가 되었다.
import React from "react";
import {
useAppSelector,
useAppDispatch,
uploadSingleComplete,
} from "Store";
import {
useUploadOneBlueprintMutation,
UploadStatus,
DocumentType,
} from "generated/types";
import { useSnackbar } from "notistack";
import { getBlueprintTitle } from "utils";
import { useUploadDnD } from "./useUploadDnD";
import { useUploadFile } from "./useUploadFile";
export function useUpload(designKey: DocumentType, replacement?: boolean) {
const [progress, setProgress] = React.useState(0);
const [uploadedFilename, setUploadedFilename] = React.useState("");
const [uploadedAt, setUploadedAt] = React.useState<string | null>(null);
const {
designDocuments: { [designKey]: designFile },
} = useAppSelector((state) => state.project);
const dispatch = useAppDispatch();
const { enqueueSnackbar } = useSnackbar();
const progressCallback = (progress: any) => {
setProgress((progress.loaded / progress.total) * 100);
};
const [uploadOne] = useUploadOneBlueprintMutation();
const uploadSingleFileUpdate = React.useCallback(
(
projectId: number,
documentType: DocumentType,
url: string,
filename: string,
) => {
uploadOne({
variables: {
data: {
url,
filename,
documentType,
status: UploadStatus.Success,
project: { connect: { id: projectId } },
},
updateManyBlueprintData: { status: { set: UploadStatus.Failed } },
updateManyBlueprintWhere: {
projectId: { equals: projectId },
documentType: { equals: documentType },
},
},
}).then((data) => {
dispatch(uploadSingleComplete(documentType));
const filename = data.data?.createOneBlueprint.filename;
const createdAt = data.data?.createOneBlueprint.createdAt;
enqueueSnackbar(
`${getBlueprintTitle(designKey)} 파일이 ${
replacement ? "변경" : "업로드"
}되었습니다.`,
{ variant: "success" },
);
setUploadedFilename(filename || "");
setUploadedAt(createdAt);
});
},
[designKey, dispatch, enqueueSnackbar, replacement, uploadOne],
);
const { uploadFile } = useUploadFile({
uploadSingleFileUpdate,
progressCallback,
});
const { handleClick, isDragActive, getRootProps, ref, fileCount } =
useUploadDnD({ uploadFile, designKey });
const handleUpload =
(key: DocumentType) =>
async (event: React.ChangeEvent<HTMLInputElement>) => {
const {
target: { files },
} = event;
if (!files) return;
const file = files[0];
uploadFile(file, key);
};
return {
uploadSingleFileUpdate,
handleUpload,
handleClick,
uploadFile,
isDragActive,
getRootProps,
progress,
ref,
file: designFile,
uploadedFilename,
uploadedAt,
fileCount,
};
}
개발자 생각에 여전히 길다면, 더 스플릿하면 된다. 더 쪼갤 수 없을 때까지 쪼갤 필요는 없을 것 같다. 디버그가 가능해야하고 아주 잘게 쪼개어도 import
은 공통적으로 계속 사용하기 때문에, 전체 코드량은 늘어난다.
반면에 Container는 간단해진다. 이제 업로드를 제외한 다른 비즈니스 로직을 추가하면 된다. 복잡도는 hook으로 옮겨갔고, 나중에 이부분을 구조화 하면 될 것이다. 그리고 Container에 담긴 코드를 재사용이 매우 어려운데 비해 hooks로 분리해 두면 다른 곳에 업로드 관련 컨테이너가 필요하다면, 바로 사용이 가능해진다.
Nextjs SSR with GraphQL-codegen
지난 paljs-frontend 프로젝트에서 이어서 진행하려고 한다. paljs/admin은 멋진 PrismaTable 컴포넌트를 제공한다. 하지만 커스터마이징의 어려움이 있다. 오픈소스이기 때문에 이 프로젝트를 붙여서 커스터마이징이 가능하지만, 간편하게 쓸 수 있는 이 테이블은 그대로 두고 별도로 테이블을 만들어 커스터마이징 하는 편이 좋을 것 같다. 여기의 디자인 패턴에 따라 query
를 활용하여, SSR 페이지를 만들어보자. 우선 GraphQL-Codegen 셋팅을 한다.
GraphQL-codegen
Nextjs SSR page generator를 코드제너레이터에 추가할 생각인데, hook을 함께 사용하려고 한다. Pagination은 SSR 페이지로 쿼리하고, 옵션 및 데이터 수정은 hook을 쓰도록 하려면 2곳의 자동 생성된 코드가 필요하다. hook 이 있는 위치인 generated/types.ts
그리고 SSR페이지가 있는 generated/pages.tsx
을 생성하도록 codegen.yml
파일을 만든다.
overwrite: true
schema: ${SCHEMA_PATH}
documents:
- "src/**/*.graphql"
- "!src/graphql/**/*.graphql"
src/generated/types.ts:
plugins:
- typescript
- typescript-operations
- typescript-react-apollo
config:
reactApolloVersion: 3
withHooks: true
apolloReactComponentsImportFrom: "@apollo/client/react/components"
apolloReactHocImportFrom: "@apollo/client/react/hoc"
apolloReactHooksImportFrom: "@apollo/client"
declarationKind:
type: interface
input: interface
maybeValue: T
src/generated/page.tsx:
config:
documentMode: external
importDocumentNodeExternallyFrom: ./types
reactApolloVersion: 3
withHooks: true
contextType: ApolloClientContext
contextTypeRequired: true
apolloClientInstanceImport: ../withApollo
preset: import-types
presetConfig:
typesPath: ./types
plugins:
- graphql-codegen-apollo-next-ssr
GraphQL-codegen과 플러그인들을 설치
yarn add -D @graphql-codegen/cli \
@graphql-codegen/fragment-matcher \
@graphql-codegen/import-types-preset \
@graphql-codegen/introspection \
@graphql-codegen/typescript \
@graphql-codegen/typescript-document-nodes \
@graphql-codegen/typescript-operations \
@graphql-codegen/typescript-react-apollo \
graphql-codegen-apollo-next-ssr
package.json
에는 생성 스크립트를 넣어준다.
{
"scripts": {
"generate:schema": "rover graph introspect http://localhost:8000 | awk 1 > ./src/generated/schema@dev.graphql",
"generate:types": "SCHEMA_PATH=src/generated/schema@dev.graphql graphql-codegen --config codegen.yml",
"postgenerate:types": "prettier --write src/generated/**/*.*",
"generate": "npm -s run generate:schema && npm -s run generate:types"
}
}
generate:schema
는 API에서 GraphQL schema를 introspect 한다.generate:types
는 introspect한 schema로 GraphQL-codegen 을 수행한다.postgenerate:types
는 생성된 타입과 페이지를 prettier write 한다.generate
는 이 모두를 실행한다.
이제 첫번째 GraphQL operation을 만든다. 맨 처음으로는 Pagination을 하는 Table을 생성할 것이라. findAllPostsPagination.graphql
파일을 만들어준다. paljs
로 생성한 CRUD는 findManyPost
와 aggregatePost
가 있을 것이다.
query findAllPostsPagination($take: Int, $skip: Int) {
findManyPost(take: $take, skip: $skip) {
id
title
content
author {
name
_count {
posts
}
}
authorId
referredBlogs
}
aggregatePost {
_count {
id
}
}
}
이제 yarn generate
를 실행하면 위의 GraphQL operation을 grab하고 apollo hooks와 각종 타입들은 generated/types.ts
에 생성되고, SSR 페이지는 generated/pages.tsx
에 생성될 것이다.
다음으로는 withApollo
를 만들어준다.
withApollo
withApollo
는 withPage
를 제공하며, 이 withPage
에는 NextRouter
를 넘겨 받는다. 이 query
를 가지고 해당 GraphQL query를 수행할 수 있도록 도와준다.
import React from "react";
import { NextPage } from "next";
import {
ApolloClient,
NormalizedCacheObject,
InMemoryCache,
ApolloProvider,
createHttpLink,
from,
} from "@apollo/client";
import {
NextApiRequestCookies,
// @ts-ignore This path is generated at build time and conflicts otherwise
} from "next-server/server/api-utils";
import { IncomingMessage } from "http";
const uri = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
export type ApolloClientContext = {
req?: IncomingMessage & {
cookies: NextApiRequestCookies;
};
};
export const withApollo = (Comp: NextPage) => (props: any) => {
return (
<ApolloProvider client={getApolloClient(undefined, props.apolloState)}>
<Comp />
</ApolloProvider>
);
};
export const getApolloClient = (
ctx?: ApolloClientContext,
initialState?: NormalizedCacheObject,
) => {
if (ctx && ctx.req) {
const { req } = ctx;
req.cookies;
}
const httpLink = createHttpLink({
uri,
fetch,
});
const cache = new InMemoryCache().restore(initialState || {});
return new ApolloClient({
link: from([httpLink]),
cache,
});
};
여기서는 withApollo
를 유심히 보면 된다. generated/pages.tsx
에 해당 컴포넌트에 담기는 방식은 아래와 같다.
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,
};
즉, withPageFindAllPostsPagination
는 컴포넌트에 useQuery
를 자동생성 해주어, 편하게 data
와 error
를 Props으로 내려 받을 수 있도록 제공한다.
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,
},
};
}
여기서 SSR을 제공한다. getApolloClient
로 query를 하며, 쿼리 완료된 상태로 서버 Props를 넘겨준다. withPageFindAllPostsPagination
방식에서는 (props) => {}
형태로 함수를 넘겨주기 때문에 렌더링 시점에 데이터 fetching이 수행될 것으로 보이며, getServerPageFindAllPostsPagination
방식은 Props를 서버 프랍으로 넘겨주기 때문에, 서버 렌더링 시점에 수행될 것으로 보인다.
withPage
방식으로 리스트 컴포넌트를 만들면, 아래처럼 단순하다.
import React from "react";
import { withApollo } from "withApollo";
import {
ssrFindAllPostsPagination,
PageFindAllPostsPaginationComp,
} from "generated/page";
import { HeadlessTable } from "components";
const ListPosts: PageFindAllPostsPaginationComp = (props) => {
return (
<HeadlessTable {...props} />
);
};
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,
},
}))(ListPosts),
);
이후, getServerPageFindAllPostsPagination
는 다음 주에 조금 더 진행해보고 Page 렌더링도 체크해 볼 예정이다.