💬 Portfolio
Noiton을 활용한 고객 예약 및 채용 서비스 개선하기
1. 핵심기능 설명1-1) 비효율적으로 진행되던 예약,신청페이지를 노션으로 일원화1-2) 별도의 어드민 없이 노션으로 수정 및 데이터 관리 가능2. 설계2-1) Notion API 쉽게 사용하기 위한 자체 API 구성API 구조 개편스웨거 문서 제작SDK key, DB id만 있으면 데이터 보내기 가능!속도 개선 방향2-2) 프론트 작업 트러블 슈팅(속성이름, 속성순서, 이미지업로드)property에 따른 객체구조 만들어서 input value로 연결하기노션의 속성은 순서를 보장하지 않는다.이미지 업로드를 위한 S3 활용3. 기대효과3-1) 여러 서비스로 확장이 가능한 구조3-2) 누구나 예약페이지를 제작, 배포 가능
1. 핵심기능 설명
이번 노션 프로젝트의 핵심은 두 가지 목표가 있습니다
1) 기존 구글폼등으로 분산된 서비스 예약 및 채용 페이지 관리 및 DB관리를 노션페이지로 일원화하는 것
2) 별도의 어드민 페이지 없이 모든 예약페이지 관리부터 신청자 데이터도 노션으로 관리할 수 있도록 하는 것.
+) Notion API 자체가 어떻게 구성되고, 어디까지 가능한지, 한계점은 어디인지는 저의 블로그에 글을 남겨놨으니 여기서 확인을 해보시면 더 많은 내용을 검토하실 수 있을 것 같습니다 🙂
1-1) 비효율적으로 진행되던 예약,신청페이지를 노션으로 일원화

[기존 채용페이지- 구글폼]

[노션기반 채용페이지]
노션은 별도의 폼빌더가 없습니다.
프로덕트 헌트에서 좋은 평가를 받은 노션폼(https://notionforms.io/)라는 서비스가 흥행하고 있긴 합니다.
그러나 개인으로 사용하기엔 유료 서비스와 외국 사이트라는 것이 진입 허들을 만들고 있습니다.
또한 노션페이지에서 직접 수정할 수 있는 것이 아니라 notionForms에서 form에 대한 관리를 별도로 해줘야하기 때문에 번거로움이 있습니다.
그래서 저희 회사에서도 기존에는 위와 같이 별도의 노션 페이지를 공유해서 채용 소개 페이지가 따로 있고,
신청하기 버튼을 클릭시 구글폼의 신청링크로 신청을 하면 다시 그 신청자에 대한 관리를 노션에 별도로 기입해서 관리를 하던 상황이였습니다.
이를 해결하기 위해 노션의 페이지에 소개 문구를 작성하면, 웹에 반영되고 신청에 필요한 항목을 노션 데이터베이스의 속성으로 넣으면 해당 항목에 맞춰 반영되는 구조를 설계했습니다.
데모 영상을 보면 notion을 배포한 설문조사 페이지에서 설문을 입력하면 바로 notion DB에 반영되는 것을 알 수 있습니다.
1-2) 별도의 어드민 없이 노션으로 수정 및 데이터 관리 가능
위의 영상에서 보듯이 타이틀이나 소개 문구, 이미지 등을 수정할 경우 바로 반영이 되며, 설문조사 항목을 추가하거나, 삭제하더라도 바로 반영이 됩니다.
이를 통해 노션데이터베이스, 페이지를 통해 비개발자도 충분히 Admin을 활용할 수 있게 서비스를 만들었습니다.
2. 설계
2-1) Notion API 쉽게 사용하기 위한 자체 API 구성
Notion API를 통해서 왠만한 백엔드 작업은 가능하지만 두 가지 문제가 있습니다
- 노션에서 데이터를 넘겨줄 때 프론트엔드에서 조작하기 어려움
- 속도가 느리다. 데이터 요청 시 평균 300ms~2000ms까지 걸립니다. 보통은 300ms 안쪽으로 데이터를 받아오지만, 10번에 1번 꼴로 데이터 2000ms까지 가는 경향
우선 1번의 문제먼저 보겠습니다.
아래의 데이터 구조는 Notion API에서 데이터를 넘겨주는 형태입니다.
API 구조 개편
아래는 Notion API에서 데이터를 보내주는 방식이다.
notion api 데이터
{
"object": "list",
"results": [
{
"object": "page",
"id": "59833787-2cf9-4fdf-8782-e53db20768a5",
"created_time": "2022-03-01T19:05:00.000Z",
"last_edited_time": "2022-07-06T20:25:00.000Z",
"created_by": {
"object": "user",
"id": "ee5f0f84-409a-440f-983a-a5315961c6e4"
},
"last_edited_by": {
"object": "user",
"id": "0c3e9826-b8f7-4f73-927d-2caaf86f1103"
},
"cover": {
"type": "external",
"external": {
"url": "https://upload.wikimedia.org/wikipedia/commons/6/62/Tuscankale.jpg"
}
},
"icon": {
"type": "emoji",
"emoji": "🥬"
},
"parent": {
"type": "database_id",
"database_id": "d9824bdc-8445-4327-be8b-5b47500af6ce"
},
"archived": false,
"properties": {
"Store availability": {
"id": "%3AUPp",
"type": "multi_select",
"multi_select": [
{
"id": "t|O@",
"name": "Gus's Community Market",
"color": "yellow"
},
{
"id": "{Ml\\",
"name": "Rainbow Grocery",
"color": "gray"
}
]
},
"Food group": {
"id": "A%40Hk",
"type": "select",
"select": {
"id": "5e8e7e8f-432e-4d8a-8166-1821e10225fc",
"name": "🥬 Vegetable",
"color": "pink"
}
},
"Price": {
"id": "BJXS",
"type": "number",
"number": 2.5
},
"Responsible Person": {
"id": "Iowm",
"type": "people",
"people": [
{
"object": "user",
"id": "cbfe3c6e-71cf-4cd3-b6e7-02f38f371bcc",
"name": "Cristina Cordova",
"avatar_url": "https://lh6.googleusercontent.com/-rapvfCoTq5A/AAAAAAAAAAI/AAAAAAAAAAA/AKF05nDKmmUpkpFvWNBzvu9rnZEy7cbl8Q/photo.jpg",
"type": "person",
"person": {
"email": "cristina@makenotion.com"
}
}
]
},
"Last ordered": {
"id": "Jsfb",
"type": "date",
"date": {
"start": "2022-02-22",
"end": null,
"time_zone": null
}
},
"Cost of next trip": {
"id": "WOd%3B",
"type": "formula",
"formula": {
"type": "number",
"number": 0
}
},
"Recipes": {
"id": "YfIu",
"type": "relation",
"relation": [
{
"id": "90eeeed8-2cdd-4af4-9cc1-3d24aff5f63c"
},
{
"id": "a2da43ee-d43c-4285-8ae2-6d811f12629a"
}
],
"has_more": false
},
"Description": {
"id": "_Tc_",
"type": "rich_text",
"rich_text": [
{
"type": "text",
"text": {
"content": "A dark ",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "A dark ",
"href": null
},
{
"type": "text",
"text": {
"content": "green",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "green"
},
"plain_text": "green",
"href": null
},
{
"type": "text",
"text": {
"content": " leafy vegetable",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": " leafy vegetable",
"href": null
}
]
},
"In stock": {
"id": "%60%5Bq%3F",
"type": "checkbox",
"checkbox": true
},
"Number of meals": {
"id": "zag~",
"type": "rollup",
"rollup": {
"type": "number",
"number": 2,
"function": "count"
}
},
"Photo": {
"id": "%7DF_L",
"type": "url",
"url": "https://i.insider.com/612fb23c9ef1e50018f93198?width=1136&format=jpeg"
},
"Name": {
"id": "title",
"type": "title",
"title": [
{
"type": "text",
"text": {
"content": "Tuscan kale",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "Tuscan kale",
"href": null
}
]
}
},
"url": "https://www.notion.so/Tuscan-kale-598337872cf94fdf8782e53db20768a5"
}
],
"next_cursor": null,
"has_more": false,
"type": "page",
"page": {}
}그래서 위의 데이터를 정제해주는 작업이 필요하다.
따라서 Notion API의 데이터를 정제해주는 API 서버를 만들었다.
API에서 넘겨준 데이터 구조

스웨거 문서 제작
아래는 스웨거로 문서화를 해서 팀원들한테 공유한 결과입니다.
아래의 API를 활용하면 위와 같이 더러운 notion api 데이터 구조는 보지 않아도 됩니다..뿌듯!
다만, express를 활용해 BFF 서버를 구축해본 경험이고 문서화도 처음이라 아직 서툰 부분이 굉장히 많았습니다..

SDK key, DB id만 있으면 데이터 보내기 가능!
위의 구조를 통해서 notion sdk key와 db id만 있으면 설문을 만들 수 있는 구조를 만들었습니다.
프론트에서 query로 해당 값을 받아서 넘겨주는 형태로 설계를 우선 했습니다.
더 안전하고, 좋은 방식은 따로 DB를 만들어서 각 key와 id에 맞는 DB를 계속 누적하는 방식도 검토했으나, 시간이 너무 오래걸리고, 백엔드 작업을 최소화하는 것에 의미가 있었기 때문에 빠르게 데모를 만들기 위해 이렇게 진행이 되었습니다.
속도 개선 방향
속도 개선을 위한 SSR 도입, 캐싱전략 등을 포함할 수 있겠지만 아직 이 부분까지는 테스트를 해보지 못했으나, 충분히 속도는 개선할 여지가 많다고 생각합니다.
참고할 만한 자료로는 노션 API를 next.js 서버사이드 렌더링을 통해 블로그를 만든 morethanlog라는 프로젝트가 있습니다.
해당 프로젝트로 SSR을 도입하면 어느정도 속도 및 사용성이 개선될 수 있는 지점이 있는 것을 알 수 있었습니다.
2-2) 프론트 작업 트러블 슈팅(속성이름, 속성순서, 이미지업로드)
property에 따른 객체구조 만들어서 input value로 연결하기
노션은 객체의 key값으로 해당 프로퍼티의 이름을 받기 때문에 이 key값을 기반으로 다시 객체형태로 묶어줘야했습니다.
위와 같이 데이터를 정제해서 넘겨주면 프론트에서는 데이터베이스의 각 속성(property)에 따라서 객체구조를 다시 잡아서 type에 따른 input을 만들어줘야 합니다.
아래와 같이 input을 만들고 해당 input값의 입력값을 받는 객체에 model을 연결해주는 형태로 구조를 만들었습니다.
코드 구조
getPreview().then((res) => {
this.databaseRetrieve = res.data
this.databaseRetrieve.properties.forEach((el) => {
this.addInputValueObject(el)
})
this.finalProperties = res.data.databaseQuery[0].properties
})
<input class="form_input_wrapper"
v-if="item.type === 'title' || item.type === 'rich_text' || item.type === 'number'"
v-model="item.answer"
/>노션의 속성은 순서를 보장하지 않는다.

위 과정에서 어려운 점이 있었는데 바로 노션의 속성은 순서를 보장하지 않는다는 것입니다.선택을 해야했는데, 결국 notion의 데이터베이스에 임의적으로 순서를 적어줘야만 했습니다.
그렇지 않으면 생성된 순서대로 데이터를 읽어와 사용자단에서는 원하는 순서를 보장할 수가 없었습니다.
그래서 -를 기준으로 이름을 다시 구성해서 보여준 형태로 코드를 짰습니다.
이미지 업로드를 위한 S3 활용
S3에 이미지를 업로드하고 해당 링크를 notion에 전달해야만 했습니다.
그래서 이미지 업로드는 S3를 활용해 별도의 작업을 진행했습니다.
코드 구조
AWS.config.region = this.bucketRegion
AWS.config.credentials = new AWS.CognitoIdentityCredentials({
IdentityPoolId: this.IdentityPoolId
})
const s3 = new AWS.S3({
apiVersion: '2006-03-01',
params: {
Bucket: this.albumBucketName
}
})
await s3
.upload(
{
ACL: 'public-read',
Body: this.file,
Key: this.file.name
},
(error) => {
if (error) {
this.errorHandler(error)
// return alert('There was an error uploading your photo: ', error.message);
}
}
)
.promise()
.then((data) => {
console.log('File uploaded!!!')
console.log(data)
this.$parent.changeImageLink(data.Location, this.imageContent)
})3. 기대효과
3-1) 여러 서비스로 확장이 가능한 구조

해당 서비스를 통해서 사내에 있는 모든 예약 서비스를 위와 같은 구조로 변경이 가능합니다.
현재 교통, 통역, 유심, 투어버스 등의 신청을 받고 있는데,
이를 위와 같이 관리한다면 작업 효율이 굉장히 증가할 것으로 보입니다.
추후에는 왓츠앱으로 해외 환자들의 문의를 받고 있는데,
notion과 왓츠앱을 연동해 문의 내역 등을 자동화하는 것도 가능하다고 생각합니다.
3-2) 누구나 예약페이지를 제작, 배포 가능
하이메디는 환자의 전 과정을 계속 담당하는 컨시어지 서비스가 메인이기 때문에 온라인 서비스를 도입할 때 별도의 관리자(Admin) 페이지가 필수적으로 필요합니다.
그런데 노션을 백엔드로 활용해 예약페이지를 만들어나간다면 사업담당자가 수정 및 문의를 통해 웹페이지의 유지보수가 가능한 구조가 될 수 있습니다.
이런 부분들을 추후 확장한다면 회사차원의 비용절감이 가능하지 않을까 생각했습니다.

