์ต๊ทผ ์ด๋ ฅ์ฌํญ ๋ฐ์ดํฐ๋ฅผ ์ ๋ฆฌํ๋ฉด์, ์ด๋ ฅ์ฌํญ ๋ฐ์ดํฐ ๊ด๋ฆฌ๋ฅผ ์ํ ๋ฐฑ์๋ ์์ฉํ๋ก๊ทธ๋จ ์์ฑ์ ์งํํ๊ณ ์์์ต๋๋ค.
์ต์ํ .NET ํ๊ฒฝ์์ ์์ํ๋ ค๊ณ , ์ ์ฅ์๋ฅผ ์ถ๊ฐํ๊ณ ํ๋ก์ ํธ๋ฅผ ์ค๋นํ ํ ์ํฐํฐ ๋ชจ๋ธ์ ์ ์ํ์ต๋๋ค.
๊ณํํ๋ ๋ด์ฉ์ ์๋์ ๊ฐ์ต๋๋ค.
- ๋ฐ์ดํฐ ์ฒ๋ฆฌ๋ฅผ ์ํ ๋ฐฑ์๋ ์์ฉํ๋ก๊ทธ๋จ: ASP.NET Core
- ๋ฐฑ์๋ API ๋ฅผ ํ์ฉํ๋ ํ๋ก ํธ์๋ ์์ฉํ๋ก๊ทธ๋จ: next.js
์ต๊ทผ ์ ๋ฌด๊ฐ ๋์ด ์ง๋๊ฐ ์ ๋๊ฐ์ง ์์ต๋๋ค.
๊ทธ๋์, ๊ท์ฐฎ์์ ์ด๊ฒจ๋ด๊ธฐ ์ํด ๋ค๋ฅธ ๋๊ตฌ๋ฅผ ์ฐพ์๋ณด๊ธฐ ์์ํ์ต๋๋ค.
KeystoneJS
Theย superpoweredย CMS for developers
KeystoneJS ๋ฅผ ์ฌ์ฉํ๋ฉด, ๊ด๋ฆฌํ๋ ค๊ณ ํ๋ ๋ฐ์ดํฐ์ ์คํค๋ง๋ฅผ ๊ธฐ์ ํ๋ฉด ๋ฐ์ดํฐ๋ฅผ ๊ด๋ฆฌํ๊ธฐ ์ํ ์๋ํฌ์ธํธ์ ๊ฐ๋ตํ UI ๊น์ง ์์ฑ๋๋ค๊ณ ํฉ๋๋ค.
๋ฐ์ดํฐ๋ฅผ ๊ด๋ฆฌํ๊ธฐ ์ํ ์๋ํฌ์ธํธ๋ GraphQL API ๋ผ๊ณ ํฉ๋๋ค.
๋งค์ฐ ํธ๋ฆฌํด ๋ณด์ฌ, ๊ธฐ์กด ๊ณํํ๋ ๊ตฌํ์ฌํญ์ ๋ชจ๋ ํ๊ธฐํ๊ณ , KeystoneJS ๋ฅผ ์ฌ์ฉํด์ ์ด๋ ฅ์ฌํญ ๋ฐ์ดํฐ๋ฅผ ๊ด๋ฆฌํ๋ ๊ฒ์ผ๋ก ๊ฒฐ์ ํ์ต๋๋ค.
์ค๋น
์์ํ๊ธฐ ์ํ ๋ฌธ์๊ฐ ๊ฐ๋ตํ๊ฒ ์ ์ ๋ฆฌ๋์ด ์์ต๋๋ค.
# nodejs ํ๊ฒฝ์ด ์ค๋น๋์ด ์๋์ง ํ์ธํฉ๋๋ค
$ node -v
v18.16.0
$ npm -v
9.5.1
# ์์
๋๋ ํฐ๋ฆฌ๋ฅผ ์์ฑํ ์์น๋ก ์ด๋ํฉ๋๋ค
$ cd your/path/
# ํ๋ก์ ํธ๋ฅผ ์์ฑํฉ๋๋ค.
$ npx create-keystone-app@latest
โจ You're about to generate a project using Keystone 6 packages.
โ What directory should create-keystone-app generate your app into? resume-backend-keystone
โ Installing dependencies with yarn. This may take a few minutes.
โ Failed to install with yarn.
โ Installed dependencies with npm.
๐ Keystone created a starter project in: resume-backend-keystone
To launch your app, run:
- cd resume-backend-keystone
- npm run dev
Next steps:
- Read resume-backend-keystone/README.md for additional getting started details.
- Edit resume-backend-keystone/keystone.ts to customize your app.
- Open the Admin UI
- Open the Graphql API
- Read the docs
- Star Keystone on GitHub
create-keystone-app@latest
CLI ๋๊ตฌ๋ฅผ ์คํํ๋ฉด ํ์ํ ์ ๋ณด๋ฅผ ๋ฌธ๋ต์์ผ๋ก ์
๋ ฅ๋ฐ๋ ์ ์ฐจ๊ฐ ์์๋ฉ๋๋ค.
๊ฐ๋จํ๊ฒ ํ๋ก์ ํธ ์ด๋ฆ๋ง ์ง์ ํ๋ฉด, ์ ๋ ฅ๋ ํ๋ก์ ํธ ์ด๋ฆ์ ๊ธฐ๋ฐ์ผ๋ก ๋๋ ํฐ๋ฆฌ๋ฅผ ์์ฑํ๊ณ , ํด๋น ๋๋ ํฐ๋ฆฌ์ ๊ธฐ๋ณธ์ ์ผ๋ก ํ์ํ ํ์ผ๋ค์ด ๋ณต์ฌ๋ฉ๋๋ค.
# ์์ฑ๋ ํ๋ก์ ํธ ๋๋ ํฐ๋ฆฌ๋ก ์ด๋ํฉ๋๋ค.
$ cd path/to/project
# ์์กด ํจํค์ง๋ฅผ ์ค์นํฉ๋๋ค.
$ npm install
...
# ๊ธฐ๋ณธ ๊ตฌ์ฑ์ผ๋ก ๊ฐ๋ฐ์๋ฒ๋ฅผ ์์ํฉ๋๋ค.
$ npm run dev
์น ๋ธ๋ผ์ฐ์ ๋ฅผ ์ด๊ณ , ์ฃผ์์ฐฝ์ ๊ฐ๋ฐ์๋ฒ์ ์ฃผ์๋ฅผ ์ ๋ ฅํฉ๋๋ค.
http://localhost:3000
์น ๋ธ๋ผ์ฐ์ ์์ ์ฌ์ฉ์ ์์ฑ ์ ์ฐจ๊ฐ ์งํ๋๊ณ , ์ด ํ ์ถ๊ฐํ ์ฌ์ฉ์๋ก ๋ก๊ทธ์ธํ๋ฉด, User ์ Post ํญ๋ชฉ์ ํ์ธํ ์ ์์ต๋๋ค.
์น ๋ธ๋ผ์ฐ์ ์์ ํ์ธํด๋ณด๋, ๊ธฐ๋ณธ์ ์ธ ์กฐํ, ์ ๋ ฅ, ์์ , ์ญ์ ๊ฐ ๊ตฌํ๋์ด ์์ต๋๋ค.
GraphQL API ๋ฐฑ์๋ ์๋ฒ์ nextjs ํ๋ก ํธ์๋ ์๋ฒ๊ฐ ํตํฉ๋์ด ์คํ๋๋ ๊ตฌ์กฐ์ธ ๊ฒ ๊ฐ์ต๋๋ค.
์ํ๋ ๊ธฐ๋ฅ์ ์ถ๊ฐํ๋ ค๊ณ ํ์ง๋ง ์์ผ๋ฉด, ๋งค์ฐ ํธ๋ฆฌํ๊ฒ ๊ธ๋ฐฉ ์๋ฃํ ์ ์์ ๊ฒ ๊ฐ์ต๋๋ค.
์คํค๋ง
ํ๋ก์ ํธ ๋๋ ํฐ๋ฆฌ์ schema.ts
ํ์ผ์ ์ด๊ณ , ํธ์ง์ ์์ํฉ๋๋ค.
ํ์ํ ์ํฐํฐ๋ฅผ ๊ฒฐ์ ํ๊ณ , ์คํค๋ง๋ฅผ ๊ธฐ์ ํ์ต๋๋ค.
์คํค๋ง๋ฅผ ์ ์ํ๋ ํ์์ ์๋ ์ฝ๋์ ๊ฐ์ต๋๋ค.
๋ชฉ๋ก์ ์ด๋ฆ์ ๊ฒฐ์ ํ๊ณ , ์ก์ธ์ค์ ํ๋ ๋ชฉ๋ก, ํ์ํ๋ฉด ํ ์ ์์ฑํฉ๋๋ค.
import { list } from '@keystone-6/core';
export const lists: Lists = {
// ๋ชฉ๋ก (๋ฐ์ดํฐ ์งํฉ)์ ์ด๋ฆ; ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ํ
์ด๋ธ
User: list({
// ๋ชฉ๋ก ์ ๊ทผ ์ ์ด
access: {
// ์์ฑ๋ UI ์์ ๋ฐ์ดํฐ ์ ๊ทผ์ ์ด
operation: {},
// ... ํ์๊ฐ ์์ ๊ฒ ๊ฐ์ ์ฐพ์๋ณด์ง ์์์ต๋๋ค.
item: {},
// GraphQL API ์์ ๋ฐ์ดํฐ ์ ๊ทผ์ ์ด
filter: {},
},
// ๊ด๋ฆฌํ ๋ฐ์ดํฐ ๋ชฉ๋ก; ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ด(Column)
fields: {},
// ๋ฐ์ดํฐ ์
์ถ๋ ฅ์ ์ถ๊ฐ์ ์ผ๋ก ๋์ํ ๊ธฐ๋ฅ
hooks: {},
}),
// ...
};
๋ชฉ๋ก์ ํ๋๋ฅผ ์ ์ํ ๋, relationship
ํ์์ ์ฌ์ฉํด์ ๋ค๋ฅธ ๋ชฉ๋ก๊ณผ์ ๊ด๊ณ๋ฅผ ์ค์ ํ ์ ์๊ฒ ๋์ด ์์ต๋๋ค.
import { relationship } from '@keystone-6/core/fields';
Post ์ Tag ๋ชฉ๋ก์ ๋ค๋๋ค ๊ด๋ก์ค์ ์ ์๋ ์๋์ ๊ฐ์ต๋๋ค.
import { list } from '@keystone-6/core';
import { relationship } from '@keystone-6/core/fields';
export const lists: Lists = {
// ...
Post: list({
// ...
fields: {
tags: relationship({
ref: 'Tag.posts',
many: true,
ui: {
displayMode: 'cards',
cardFields: ['name'],
inlineEdit: { fields: ['name'] },
linkToItem: true,
inlineConnect: true,
inlineCreate: { fields: ['name', 'owner'] },
},
}),
},
// ...
}),
// ...
Tag: list({
// ...
fields: {
posts: relationship({
ref: 'Post.tags',
many: true,
ui: { hideCreate: true, displayMode: 'count' },
}),
},
// ...
}),
// ...
};
์ ๊ทผ์ ์ด๋ฅผ ์ค์ ํ๊ธฐ ์ํด ์ฌ์ฉํ ์ ์๋ ํจ์๋ ์๋์ ๊ฐ์ต๋๋ค.
์1) ๋ก๊ทธ์ธ๋ ์ฌ์ฉ์๋ง UI ์์ ๋ฆฌ์คํธ ๋ชฉ๋ก์ ํ์ธ ๊ฐ๋ฅ
import { list } from '@keystone-6/core';
const isAuthorized = ({ session }: { session: Session }) =>
Boolean(session?.data?.id);
export const lists: Lists = {
User: list({
access: {
operation: {
query: isAuthorized,
// ...
},
item: {},
// GraphQL API ์์ ๋ฐ์ดํฐ ์ ๊ทผ์ ์ด
filter: {},
},
// ...
}),
// ...
};
์2) ๋ก๊ทธ์ธ ์ฌ์ฉ์๋ ๋ณธ์ธ์ด ์์ฑ์ํ ํญ๋ชฉ๋ง ์กฐํ ๊ฐ๋ฅ
๋ชฉ๋ก์ ํ๋์ ์์ ์๋ฅผ ํ์ํ๋ owner ํ๋๊ฐ ์์ด์ผ ํฉ๋๋ค.
import { list } from '@keystone-6/core';
const filterByOwner = ({ session }: { session: Session }) => {
if (session?.data?.isAdmin ?? true) {
return true;
}
if (session?.data?.id) {
return {
owner: {
id: {
equals: session.data.id,
},
},
};
}
return false;
};
export const lists: Lists = {
User: list({
access: {
filter: {
query: filterByOwner,
},
},
// ...
}),
// ...
};
๋ฌธ์๋ฅผ ํ์ธํ๊ณ , ์ง์ํ๋ ํ๋๋ฅผ ํ์ฉํด์ ํ์ํ ํ๋๋ค์ ๋ชจ๋ ์ ์ํฉ๋๋ค.
ํ๋๋ฅผ ๋ชจ๋ ์ ์ํ ํ ๊ฐ๋ฐ์๋ฒ๋ฅผ ์์ํด์ ๋ฐ์ดํฐ ์ ์ถ๋ ฅ์ด ๋ชจ๋ ์ ์์ ์ผ๋ก ๋์ํ๋์ง ํ์ธํด๋ณด๋, ์๋ํ ๊ฒ๊ณผ ๊ฐ์ด ์ ๋์ํ๋ ๊ฒ์ ํ์ธํ์ต๋๋ค.
๋ฐ์ดํฐ๋ฒ ์ด์ค
๋งค์ฐ ์ค์ํ ๋ฐ์ดํฐ๊ฐ ์๋๊ณ , ๋ง์ ํธ๋ํฝ์ ์ฒ๋ฆฌํ ๋์์ด ์๋๊ธฐ ๋๋ฌธ์ ํธ๋ฆฌํ๊ฒ ํ์ผ๋ก ๊ด๋ฆฌํ ์ ์๋ Sqlite
๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ผ๋ก ๊ฒฐ์ ํ์ต๋๋ค.
Sqlite ๋ฅผ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ก ์ฌ์ฉํ๋ฉด ์คํค๋ง๊ฐ ๋ณ๊ฒฝ๋ ๋ ๋ณต์กํ๊ฒ ๋ง์ด๊ทธ๋ ์ด์ ์ ์งํํ ํ์๋ ์๋ ๊ฒ์ผ๋ก ํ์ธํ์ต๋๋ค.
๋์ปค
GitHub workflows ๋ฅผ ํ์ฉํด์ ์ด๋ฏธ์ง๋ฅผ ๋น๋ํ๊ณ , ๊ด๋ฆฌ์ค์ธ ๋ ์ง์คํธ๋ฆฌ์ ์ ๋ก๋ํ๋ ค๊ณ ํ๋ ์ด๋ฏธ์ง ํ์ผ์ ํฌ๊ธฐ๊ฐ ์ฒ๋ฆฌํ ์ ์๋ ํ๊ณ๋ฅผ ๋์ด์ ๋ฒ๋ ค ๋ก์ปฌ ์๋ฒ์์ ๋น๋ํด์ ์ ๋ก๋ํ๋ ๊ฒ์ผ๋ก ๊ด๋ฆฌ๋ฐฉ๋ฒ์ ์ฑํํ์ต๋๋ค.
์๋ํํ๋ฉด ํธ๋ฆฌํ๋ฐ, GitHub workflows ์์๋ 500MB (์ ํํ ํฌ๊ธฐ๋ ์ ๋ชจ๋ฅด๊ฒ ์ต๋๋ค) ๋ณด๋ค ํฐ ํ์ผ์ ์ฒ๋ฆฌํ ์ ์๋ ๊ฒ ๊ฐ์ต๋๋ค.
413 Request Entity Too Large
๋ก์ปฌ์๋ฒ์์ ์ด๋ฏธ์ง๋ฅผ ๋น๋ํ ํ ๋์ผํ ๋ ์ง์คํธ๋ฆฌ์ docker push ๋ฅผ ์คํํ๋ฉด ๋ฌธ์ ์์ด ์ ๋ก๋๋๊ณ ์์ด ๋์ด์ ํด๊ฒฐ๋ฐฉ๋ฒ์ ์ฐพ๋ ๊ฒ์ ์ค์งํ์ต๋๋ค.
๊ฒ์
์์ฑ๋ ์ด๋ ฅ์ฌํญ ๋ฐฑ์๋ ์์ฉํ๋ก๊ทธ๋จ์ด ์คํ๋ ์๋ฒ์ docker-compose.yml
ํ์ผ์ ์์ฑํด์ ์ปจํ
์ด๋๋ฅผ ์คํํ ์ค๋น๋ฅผ ํฉ๋๋ค.
sqlite ํ์ผ์ ๋ง์ดํธํด์ ์ปจํ ์ด๋ ์ธ๋ถ์์ ํ์ผ์ ๊ด๋ฆฌํฉ๋๋ค.
๋ฌธ์ ์์ด ์คํ๋ ๊ฒ์ ํ์ธํ๊ณ , ์ญ ํ๋ก์ ๊ตฌ์ฑ์ผ๋ก, ๋๋ฉ์ธ์ ์ฐ๊ฒฐํด์ ์น๋ธ๋ผ์ฐ์ ์์ ํ์ธํฉ๋๋ค.
๋ก๊ทธ์ธ, ๋ฐ์ดํฐ ์กฐํ, โฆ ์ ๋์ํฉ๋๋ค.
๊ทธ๋ฐ๋ฐ, ๋ฐ์ดํฐ๋ฅผ ์ ๋ ฅํ๊ฑฐ๋, ๋ณ๊ฒฝ, ์ญ์ ๋ฅผ ์๋ํ๋ฉด ์ค๋ฅ ๋ฉ์์ง๊ฐ ์ถ๋ ฅ๋๊ณ , ๋ฐ์ดํฐ ๋ณ๊ฒฝ์ด ๋ฐ์๋์ง ์์ต๋๋ค.
์ปจํ ์ด๋์ ์ฌ์ฉ์๊ฐ ์ปจํ ์ด๋์ ๋ง์ดํธํ ํ์ผ์ ์ฐ๊ธฐ ๊ถํ์ด ์๋ ๊ฒ ๊ฐ์ต๋๋ค.
์ด๋ฏธ์ง๋ฅผ ๋น๋ํ ๊ธฐ๋ฐ ์ด๋ฏธ์ง๋ฅผ ํ์ธํ๋, ์ฌ์ฉ์ ์ด๋ฆ node ๊ทธ๋ฃน node ๋ก ํ์ธ๋ฉ๋๋ค.
# ์ปจํ
์ด๋ ๋ด๋ถ ์ฌ์ฉ์ ์ ๋ณด
$ id node
uid=1000(node) gid=1000(node) groups=1000(node)
ํธ์คํธ ์๋ฒ์์ sqlite ๋ฐ์ดํฐ๋ฒ ์ด์ค ํ์ผ์ ์์ ์๋ฅผ 1000:1000 ์ผ๋ก ๋ณ๊ฒฝํด์ ๊ถํ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์์ต๋๋ค.
$ chwon -R 1000:1000 ./sqlite.db
์์ ์๋ฅผ ๋ณ๊ฒฝํ ํ ๋ฐ์ดํฐ ์ ๋ ฅ์ด ์ ์์ ์ผ๋ก ์คํ๋ฉ๋๋ค.
์ฐ๋
ํ๊ฒฝ๋ณ์๋ก CORS ํ์ฉ ์๋ณธ์ ์ค์ ํ ์ ์๊ฒ ๊ตฌ์ฑํด๋์ต๋๋ค.
์ปจํ ์ด๋ ์คํ์ ์ด๋ ฅ์ฌํญ ์น์ฑ์ ์ฃผ์๋ฅผ ํ๊ฒฝ๋ณ์์ ์ ๋ ฅํด์, ์ด๋ ฅ์ฌํญ ์น์ฑ์ด ์์ฒญํ๋ฉด ์ ์์๋ตํ๋๋ก ํ์ต๋๋ค.
์ด๋ ฅ์ฌํญ ์น์ฑ ์๋ฒ์ธก์์ ์ด๋ ฅ์ฌํญ ๋ฐฑ์๋ GraphQL API ๋ฅผ ์ฌ์ฉํด์ ๋ฐ์ดํฐ๋ฅผ ์์ฒญํ๊ฒ ๋ณ๊ฒฝํ ํ ๊ฒ์ํ์ฌ ๋ฐ์ดํฐ ์ฐ๋์ ์๋ฃํฉ๋๋ค.
query User($where: UserWhereUniqueInput!) {
user(where: $where) {
# ...
aboutMe: # ...
contentCategories: # ...
contents: # ...
skillCategories: # ...
skills: # ...
}
}
GraphQL ์๋ฒ๋ฅผ ์์ฑํ๋ ๊ฒ์ ์ซ์ดํ์ง๋ง ์ฌ์ฉํ๋ ์ ์ฅ์์ ์๊ฐํ๋ฉด ํ๋ฒ์ ํ์ํ ๋ฐ์ดํฐ๋ฅผ ์ํ๋ ๋ชจ์์ผ๋ก ๊ฐ์ ธ์ฌ ์ ์์ด ์ข์ ๊ฒ ๊ฐ์ต๋๋ค.
์๋ฃ
๊ธธ๊ฒ ๊ณํํ๊ณ ์ฒ์ฒํ ์งํํ๋ ค๊ณ ํ๋๋ฐ, ๋๋ถ๋ถ์ ๊ตฌํ์ด KeystoneJS ๋ด๋ถ์์ ์ฒ๋ฆฌ๋์ด ๊ธ๋ฐฉ ๋๋๋ฒ๋ ธ์ต๋๋ค.
๋ง์น๋ฉฐ
์ฌ์ด๋ ํ๋ก์ ํธ๋ก ์ฌ๋ฏธ๋ ์๋น์ค๋ฅผ ์ค๋นํ ๋, KeystoneJS๋ฅผ ์ฌ์ฉํด์ ๋ฐฑ์๋๋ฅผ ๊ตฌ์ฑํ๋ฉด ๋งค์ฐ ๋น ๋ฅด๊ฒ ๊ตฌํํ ์ ์์ ๊ฒ์ด๋ผ ์๊ฐํฉ๋๋ค.
์๋น์ค๋ฅผ ํ์ฅํด์ผ ํ๋ ์์ ์ด ์ค๋ฉด, ๊ทธ ๋ ํ์ํ ๊ธฐ๋ฅ์ ๋ฐฑ์๋๋ฅผ ๊ตฌํํ๊ธฐ ์์ํด๋ ๋ ๊ฒ ๊ฐ์ต๋๋ค.
๊ด๋ จ ์ ์ฅ์: