SwiftUI에서 노션 API를 사용해 노션 데이터베이스에 데이터를 업데이트하는 방법을 다룹니다.
들어가기 앞서
노션은 개발자를 위해 워크스페이스의 데이터베이스에 대한 무료 API를 제공하고, http 요청을 통해 (depth가 깊긴 하지만) 손쉽게 사용할 수 있습니다.
기본 환경 설정에 대한 건 노션 공식 홈페이지에서 제공하기도 하고, 이미 많은 블로그에서 상세히 다루기 때문에 생략하겠습니다.
API 예제
네이버 오픈 API 중 뉴스 검색 API를 사용합니다.
네이버 오픈 API에 대한 자세한 사항은 네이버 오픈 API 도큐먼트 참고 바랍니다.
뉴스 검색 API
API에서 받은 값을 노션에 업데이트하는 심플한 프로젝트를 만들 것이기 때문에 API 레퍼런스에서 필요한 요소만 사용합니다.
HTTP Request:
URL: <https://openapi.naver.com/v1/search/news.json>
Method: GET
Param:
query: String, UTF-8 Encoded
Headers:
X-Naver-Client-Id: Client ID
X-Naver-Client-Secret: Client Secret
HTTP Response:
Type: JSON
items:
title: String
originallink: String
description: String
pubDate String
“카페”를 UTF-8 인코딩하여 HTTP 요청한 결과
뉴스 검색 요청 및 응답
뉴스 검색을 요청하는 간단한 코드입니다.
텍스트필드에 입력한 문자에 대한 뉴스 검색 결과를 화면에 보여줍니다.
import SwiftUI
import Moya
struct ContentView: View {
@State var query = ""
@State var items: [[String: String]] = [[:]]
var body: some View {
VStack {
TextField(text: $query) {
Text("검색할 단어")
}
Button {
queryText()
} label: {
Text("검색하기")
}
ForEach(items, id: \\.self) { item in
HStack {
Text(item["title"] ?? "")
Text(item["description"] ?? "")
}
}
}
.padding()
}
func queryText() {
let provider = MoyaProvider<NaverAPI>()
provider.request(.requestSearch(query)) { result in
switch result {
case .success(let resp):
if let decodedData = try? JSONDecoder().decode(QueryResponse.self, from: resp.data) {
self.items = decodedData.items
}
case .failure(let error):
print(error.localizedDescription)
}
}
}
struct QueryResponse: Codable {
var items: [[String: String]]
}
}
#Preview {
ContentView()
}
import Moya
import SwiftUI
enum NaverAPI {
case requestSearch(_ query: String)
}
extension NaverAPI: TargetType {
var baseURL: URL {
let url = Authorization.naver.url()
guard let baseURL = URL(string: url) else { fatalError() }
return baseURL
}
var path: String {
switch self {
case .requestSearch:
return "/search/news.json"
}
}
var method: Moya.Method {
switch self {
case .requestSearch:
return .get
}
}
var task: Moya.Task {
switch self {
case .requestSearch(let query):
return .requestParameters(parameters: ["query": query], encoding: URLEncoding.queryString)
}
}
var headers: [String: String]? {
switch self {
default:
return Authorization.naver.headers()
}
}
}
노션 API
노션 API에 대한 코드를 작성하기 앞서 이전 코드의 뷰는 가독성이 떨어져서, 뉴스 검색 결과의 개수만 나타내도록 수정했습니다.
var body: some View {
VStack {
TextField(text: $query) {
Text("검색할 단어")
}
Button {
queryText()
} label: {
Text("검색하기")
}
Button {
} label: {
Text("업로드하기")
}
Text(items.count.description + "개")
}
.padding()
}
데이터베이스는 다음과 같이 만들었습니다.
데이터베이스의 title, description, pubDate, originallink, link는 네이버 뉴스 검색 API의 request 요소로 속성명이 같은 필요는 없지만, 편의를 위해 일치 시켰습니다.
우선, 포스트맨으로 데이터베이스에 데이터를 추가하는 걸 테스트합니다.
rich text 필드 속성 값
{
"properties": {
"Description": {
"rich_text": [
{
"type": "text",
"text": {
"content": "There is some ",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "There is some ",
"href": null
},
{
"type": "text",
"text": {
"content": "text",
"link": null
},
"annotations": {
"bold": true,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "text",
"href": null
},
{
"type": "text",
"text": {
"content": " in this property!",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": " in this property!",
"href": null
}
]
}
}
}
텍스트만 존재하면 되니까 필요한 속성만 추려냅니다.
{
"properties": {
"pubDate": {
"rich_text": [
{
// "type": "text",
"text": {
"content": "There is some ",
// "link": null
},
/*
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "There is some ",
"href": null
},
{
"type": "text",
"text": {
"content": "text",
"link": null
},
"annotations": {
"bold": true,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "text",
"href": null
},
{
"type": "text",
"text": {
"content": " in this property!",
"link": null
},
"annotations": {
"bold": false,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": " in this property!",
"href": null
}
*/
]
}
}
}
→
정상적으로 업데이트가 되는 걸 확인했으니, 포스트맨에서 사용한 request body를 참고해서 swift 코드를 작성해줍니다.
노션 API 요청
title 필드와, rich text 필드의 속성 값이 달라서, 편의를 위해 title 필드를 rich text 필드로 새로 생성했습니다.
enum NotionAPI {
case createPage(_ item: [String: String])
}
extension NotionAPI: TargetType {
var type: Authorization {
.notion
}
var baseURL: URL {
let url = type.url()
guard let baseURL = URL(string: url) else { fatalError() }
return baseURL
}
var path: String {
type.path()
}
var method: Moya.Method {
.post
}
var task: Moya.Task {
switch self {
case .createPage(let item):
let body = Body(databaseId: type.databaseId(), properties: item)
return .requestJSONEncodable(body)
}
}
struct Body: Codable {
init(databaseId: String, properties: [String: String]) {
self.parent = ["database_id": databaseId]
self.properties = properties.reduce(into: [String: [String: [[String: [String: String]]]]]()) {
$0[$1.key] = ["rich_text": [["text": ["content": $1.value]]]]
}
}
var parent: [String: String]
var properties: [String: [String: [[String: [String: String]]]]]
}
var headers: [String: String]? {
type.headers()
}
}
Moya Provider의 Target Type은 네이버 API와 노션 API로 분리했고, 네이버와 노션의 인증키는 따로 분리했습니다.
검색한 뉴스 데이터를 데이터베이스로 업데이트한 모습
전체 코드
import SwiftUI
import Moya
struct ContentView: View {
@State var query = ""
@State var items: [[String: String]] = [[:]]
var body: some View {
VStack {
TextField(text: $query) {
Text("검색할 단어")
}
Button {
queryText()
} label: {
Text("검색하기")
}
Button {
createPage()
} label: {
Text("업로드하기")
}
Text(items.count.description + "개")
}
.padding()
}
func queryText() {
let provider = MoyaProvider<NaverAPI>()
provider.request(.requestSearch(query)) { result in
switch result {
case .success(let resp):
if let decodedData = try? JSONDecoder().decode(QueryResponse.self, from: resp.data) {
self.items = decodedData.items
}
case .failure(let error):
print(error.localizedDescription)
}
}
}
struct QueryResponse: Codable {
var items: [[String: String]]
}
func createPage() {
let provider = MoyaProvider<NotionAPI>()
for item in items {
provider.request(.createPage(item)) { result in
print(result)
}
}
}
}
#Preview {
ContentView()
}
import Moya
import SwiftUI
enum NaverAPI {
case requestSearch(_ query: String)
}
extension NaverAPI: TargetType {
var type: Authorization {
.naver
}
var baseURL: URL {
let url = type.url()
guard let baseURL = URL(string: url) else { fatalError() }
return baseURL
}
var path: String {
type.path()
}
var method: Moya.Method {
.get
}
var task: Moya.Task {
switch self {
case .requestSearch(let query):
return .requestParameters(parameters: ["query": query], encoding: URLEncoding.queryString)
}
}
var headers: [String: String]? {
type.headers()
}
}
// MARK: -----
enum NotionAPI {
case createPage(_ item: [String: String])
}
extension NotionAPI: TargetType {
var type: Authorization {
.notion
}
var baseURL: URL {
let url = type.url()
guard let baseURL = URL(string: url) else { fatalError() }
return baseURL
}
var path: String {
type.path()
}
var method: Moya.Method {
.post
}
var task: Moya.Task {
switch self {
case .createPage(let item):
let body = Body(databaseId: type.databaseId(), properties: item)
return .requestJSONEncodable(body)
}
}
struct Body: Codable {
init(databaseId: String, properties: [String: String]) {
self.parent = ["database_id": databaseId]
self.properties = properties.reduce(into: [String: [String: [[String: [String: String]]]]]()) {
$0[$1.key] = ["rich_text": [["text": ["content": $1.value]]]]
}
}
var parent: [String: String]
var properties: [String: [String: [[String: [String: String]]]]]
}
var headers: [String: String]? {
type.headers()
}
}
import Foundation
enum Authorization {
case naver, notion
func url() -> String {
switch self {
case .naver:
"<https://openapi.naver.com/v1>"
case .notion:
"<https://api.notion.com/v1>"
}
}
func path() -> String {
switch self {
case .naver:
"/search/news.json"
case .notion:
"/pages/"
}
}
func databaseId() -> String {
""
}
func headers() -> [String: String] {
switch self {
case .naver:
["Content-Type": "application/json", "X-Naver-Client-Id": "", "X-Naver-Client-Secret": ""]
case .notion:
["Content-Type": "application/json", "Notion-Version": "2022-02-22", "Authorization": ""]
}
}
}
'Swift' 카테고리의 다른 글
Swift: 정규 표현식 (0) | 2024.08.12 |
---|---|
Swift: 객체지향 프로그래밍의 요소 예제 (0) | 2024.08.12 |
Swift: Dictionary (0) | 2023.12.12 |
Swift: Combine(4) - 실전압축콤바인예제 (0) | 2023.11.03 |
Swift: Combine(3) - Cancellable (0) | 2023.11.03 |