[Vapor] Client-Server-DB POST 구현

Valse·2022년 12월 24일
1

Vapor

목록 보기
3/3
post-thumbnail

본 게시글은 Vapor 4.0.0, Fluent 4.0.0, Fluent-mysql-driver 4.0.0 버전을 바탕으로 작성되었습니다.
또한 최소 타겟 버전 iOS 16.1, Swift 5.0 이상, xcode 14.1 버전 이상에서 정상 작동되는 것을 확인하였습니다.

그동안 무슨 일이?

  • Vapor를 왜 써야 하는지 고민고민.
    • MVP 앱의 db를 구현하는 동안 문서형 db의 장점도, 단점도 너무 많이 느꼈다.
    • Firebase 는 문서형 db인데, 이걸 관계형처럼 써보겠다고 DocumentReference 타입 경로를 만들면서 씨름했다.
    • 다 좋다.. 다 좋은데.. 애초에 경로를 갖고 있으려면 그 경로를 갖는 문서가 이미 내 손에 있어서 모델링을 할 때 그 문서를 static 하게 쓰게 되었다.
    • 문서 db가 하위 콜렉션-하위 문서 형태로 구성되다 보니 내 코드의 모델들도 큰 모델이 하위 모델을 갖고 있는 요상한 형태가 되었다(사실 이건 내 능력 부족이다).
    • 물론 nil 값을 가질 수 있고, 필드도 자유롭게 추가할 수 있는 점은 매우매우 좋다.

"왜 Vapor 인가"에 대한 나름의 대답들.

  • [당연히 주관적]
    1. 내 마음대로 db를 구성할 수 있고, RESTful API를 직접 설계할 수 있음.
      • 당연하게도 RESTful 할수록 클라이언트에서의 활용 편의성은 높아진다!
    2. 데이터의 관계를 설계자가 유연하게 취할 수 있음.
    3. 개인적으로는 파이어베이스의 사용이 편하지 않았음.
      • DocumentReference 타입의 "참조경로"가 나를 너무 괴롭혔기 때문.. 다른 값들은 nil일 수 있는데, 이 녀석만큼은 nil이면 안 되는 예외기도 했고.
    4. Vapor를 쓰면 서버 내에서 연산이 가능함.
      • 이게 나에겐 꽤 매력적으로 느껴졌는데, 그 이유는 클라이언트의 부담을 조금 더 줄여줄 수 있을 것 같아서이다.
    5. 백엔드를 구성하는 로직을 직접 쓰면서 데이터의 흐름을 익힐 수 있다.
      • 이건 순전히 학습자의 입장에서 가져갈 수 있는 유리함이다.
        특히 클라이언트 개발자가 손 써볼 일이 없는 서버쪽 코드를 써보는 것만으로도 나는 꽤 많은 걸 배울 수 있었다.

성과들

  1. SwiftUI 뷰에서의 유저 인터랙션으로 트리거 되는 POST 구현과 Fluent ORM Framework를 활용한 db 연동 성공

모델 구현 과정

  1. 로컬 호스트가 관리하는 db와 Vapor Fluent를 아래와 같이 연결한다.
public func configure(_ app: Application) throws {	
/// test DBTable을 db로 사용, 당연히 테스트라서 이름과 패스워드는 valse, (검열)로 설정하고 따로 보안조치 하지 않음.
/// db의 이름은 db의 이름을 써주면 된다(db테이블 이름이 아님!).
    app.databases.use(.mysql(hostname: "localhost", username: "valse", password: "********", database: "test"), as: .mysql)

    app.migrations.add(CreateTodo())

// register routes
    try routes(app)
}
  1. 해당 db의 각 스키마를 Vapor의 클래스로 선언한다.
    모든 스키마 객체는 id를 필수로 가져야 하며, 기본적으로 UUID 타입을 생성할 수 있으나 커스텀하여 Int로도 사용이 가능한 것으로 보인다.
    이렇게 생성되면 id는 자동으로 increment 된다고 하는데 내 db에서는 왜인지 1,2,3 다음에 14로 찍혀있다.
    아마 genereatedBy 아규먼트를 지정하지 않아서 그런듯
final class TestTable: Model, Content {
    static let schema = "test_table"
    
    @ID(custom: "id")
    var id: Int?
    
    @Field(key: "name")
    var name: String
    
    @Field(key: "job")
    var job: String
    
    @Field(key: "age")
    var age: Int
    
    init() { }

    init(id: Int?, name: String, job: String, age: Int) {
        self.id = id
        self.name = name
        self.job = job
        self.age = age
    }
}
  1. Route를 관리하는 콜렉션 클래스 내부에서 각 메소드를 아래와 같이 정의한다.
public func boot(routes: Vapor.RoutesBuilder) throws {
    /// "api" 엔드포인트를 갖는 route를 묶고 각 요청에 대해 아래의 메소드로 대체한다.
    let testTables = routes.grouped("api")
    testTables.get(use: index)
    testTables.post(use: create)
    
    /// 상단의 엔드포인트에 아이디가 포함되어 있고 delete 요청의 경우엔 delete 메소드로 대체한다.
    testTables.group(":id") { eachInfo in
        eachInfo.delete(use: delete)
    }
}

// MARK: Create New item in db(?)
/// testable이 라우터에 의해 post 될 때, 이 메소드를 사용한다.
/// HTTP body로 전달된 application/json을 decode하고 db에 등록한다.
/// 저장된 구조체를 리턴해서 db에 저장된 내용을 확인할 수 있다.
public func create(req: Request) async throws -> TestTable {
    dump("++++ CREATE ++++")
    let testTable = try req.content.decode(TestTable.self)
    dump("---- STEP 1 ----")
    try await testTable.save(on: req.db)
    dump("---- STEP 2 ----")
    return testTable
}

-- 모델 작업 완료 --

뷰 클라이언트 작업

  1. Post는 HTTP 통신 중, Content Body, Method, Header 지정이 필요하다.
    우선 body에 심을 데이터를 encode 한다.
    httpMethod로 "POST"를, HTTPRequest에 Content-Type 헤더를 직접 지정한다.
    에러를 따로 확인하기 위해 메소드를 분리했다.
public func createUserInfo(with newUser: UserInfo) async -> Void {
    do {
        let encodedUserData = try encoder.encode(newUser)
        
        let postEndPoint = URL(string: "\(url)/create")!
        var request = URLRequest(url: postEndPoint)
        request.httpBody = encodedUserData
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        
        await runCreateSession(withRequest: request, withNewUserData: encodedUserData)
        
    } catch {
        dump("\(newUser) - CANT BE ENCODED : \(error.localizedDescription)")
    }
}
  1. URLSession 객체를 만들고 비동기로 .upload(for:from:) 메소드를 호출한다.
    각각 HTTPRequest, Data 타입 아규먼트를 전달하고 그 결과로 uploadData를 받아온다.
    어떤 값이 저장되었는지 받아와서 다시 디코딩한 뒤 dump()로 확인한다.
private func runCreateSession(withRequest: URLRequest, withNewUserData: Data) async -> Void {
    do {
        let uploadData = try await URLSession(configuration: .default).upload(for: withRequest, from: withNewUserData)
        
        let answer = try JSONDecoder().decode(UserInfo.self, from: uploadData.0)
        dump("++++ SUCCESS, \(answer.name) \(answer.job)")
    } catch {
        dump("+++ \(error.localizedDescription), \(withNewUserData)")
    }
}
  1. 뷰의 버튼 액션에 정의한 POST 구현 메소드를 연결한다.
Button {
	Task {
    	await vaporManager.createUserInfo(with: UserInfo(name: "국영", job: "영화배우", age: 22))
    }
} label: {
	Text("Submit")
}

결과

  • 서버 메세지
  • POST 이전의 db
  • POST 이후의 db

todos

  • 이제 UPDATEREMOVE도 해야지..?
  • 배포 어떻게 할 건지 조사


  1. 내 크리스마스 선물이 Vapor POST 구현이었을 줄이야 ㅎ
profile
🦶🏻🦉(발새 아님)

0개의 댓글