Vapor 란 Swift를 사용하여 백엔드, 웹 앱 API 및 HTTP 서버를 작성할 수 있는 웹 프레임워크입니다
Vapor 설치하기
Swift 설치가 되어 있으면, Vapor Toolbox를 설치합니다. 이 CLI(command line interface) 도구는 Vapor를 사용하는 데 필요하지는 않지만, 새 프로젝트를 생성하는 등 유틸리티를 포함하고 있습니다.
Toolbox는 Homebrew를 통해 배포됩니다. Homebrew를 아직 설치하지 않았다면 brew.sh에서 설치합니다.
brew install vapor
Vapor 프로젝트 생성하기
첫 번째 단계는 프로젝트를 만들 경로로 들어가 Vapor 프로젝트를 생성합니다.
vapor new hello -n
명령어가 완료되면 새로 생성된 폴더로 이동합니다
cd hello
Vapor 프로젝트 실행하기
위에서 만든 프로젝트를 열기 위해서 터미널에서 다음 명령어 또는 직접 파일을 클릭하여 열 수 있습니다.
open Package.swift
이때 빌드를 하는데 프로젝트의 Schema는 반드시 My Mac으로 되어 있어야 합니다. 빌드 후.실행된다면 다음과 같이 콘솔에 뜹니다.
아래서 발생하는 WARNING은 custom working directory 를 설정하면 해결됩니다.
실행된 Vapor 서버의 LocalHost 접속해보기
실행한 후 웹 브라우저를 열고, localhost:8080/hello 또는 http://127.0.0.1:8080을 접속했을 때 다음과 같이 뜨면 성공입니다.
Vapor Package의 폴더 구조
Vapor Package의 폴더 구조는 SPM의 폴더 구조를 기반으로 합니다.
Public
이 폴더에는 FileMiddleware 가 활성화된 경우 앱에서 제공되는 공개 파일이 포함됩니다. 일반적으로 이미지, 스타일 시트 및 브라우저 스크립트가 여기에 포함됩니다. 예를 들어, localhost:8080/favicon.ico로의 요청은 Public/favicon.ico 파일의 존재 여부를 확인하고 해당 파일을 반환합니다. 해당 https://vapor.codes/favicon.ico에 접속하면 Vapor 로고 이미지를 확인할 수 있습니다.
만일 Vapor가 공개 파일을 제공하기 위해서는 configure.swift 파일에서 FileMiddleware를 활성화해야 합니다.
// Serves files from `Public/` directory
let fileMiddleware = FileMiddleware(
publicDirectory: app.directory.publicDirectory
)
app.middleware.use(fileMiddleware)
Sources
이 폴더에는 프로젝트의 모든 Swift 소스 파일이 포함됩니다. 최상위 폴더인 App 은 SwiftPM 매니페스트에서 선언된 패키지 보듈을 반영합니다.
App
이 폴더는 애플리케이션 로직이 모두 들어가는 곳입니다.
Controllers
컨트롤러는 애플리케이션 로직을 그룹화하는 좋은 방법입니다. 대부분의 컨트롤러에는 요청을 받아들이고 어떤 형태의 응답을 반환하는 많은 함수가 있습니다.
Migrations
마이그레이션 폴더는 Fluent를 사용하는 경우 데이터베이스 마이그레이션(테이블 생성, 삭제 등)을 위한 곳입니다.
Models
모델 폴더는 Content 구조체나 Fluent Model 을 저장하기 좋은 장소입니다.
configure.swift
이 파일에는 configure(_:) 함수가 포함되어 있습니다. 이 메서드는 entrypoint.swift에 의해 호출되어 새로 생성된 Application을 구성합니다. 여기에서 라우트, 데이터베이스, 프로바이더 등과 같은 서비스를 등록해야 합니다.
entrypoint.swift
이 파일에는 Vapor 애플리케이션을 설정, 구성 및 실행하는 @main 진입점이 포함되어 있습니다.
routes.swift
이 파일에는 routes(_:) 함수가 포함되어 있습니다. 이 메서드는 configure(_:)의 마지막 부분에서 Application에 라우트를 등록하는 데 사용됩니다.
Tests
Sources 폴더의 각 비실행(non-executeable) 모듈은 Tests에 해당하는 폴더를 가질 수 있습니다. 이 폴더에는 패키지를 테스트하기 위해 XCTest 모듈을 기반으로 작성된 코드가 포함됩니다. 테스트는 명령줄에서 swift test를 사용하거나 XCode에서 Command + U 를 눌러 실행할 수 있습니다.
AppTest
이 폴더에는 App 모듈의 코드를 위한 단위 테스트가 포함되어 있습니다.
Package.swift
마지막으로 SPM의 패키지 매니페스트가 있습니다.
Vapor와 DB를 활용하여 회원가입 구현
우선 DB를 Vapor에서 사용하기 위해서는 Fluent 설정은 y로 설정합니다. 이때 원하는 DB 종류를 설정해야 합니다. 이때 leaf란 website를 만들기 위한 템플릿 언어로 n으로 설정합니다.
Fluent
Fluent는 Swift용 ORM(Object-relational mapping) 프레임워크입니다. Vapor 앱과 데이터베이스 사이의 추상화 계층으로, 데이터 베이스를 쉽게 사용할 수 있도록 인터페이스를 제공합니다. 따라서 DB를 직접 작성하지 않아도 Swift로 데이터베이스를 작업할 수 있게됩니다.
DB와 Vapor 서버와 연결되는 과정
다음은 앱에서 vapor 서버에 값을 요청했을 때 서버와 연결된 DB에 접근하는 과정입니다.
Vapor Package 실행
다음은 만든 Vapor Package 를 실행합니다.
유저 모델 만들기
다음은 DB 테이블에 저장할 유저의 값입니다. User 클래스는 Model, Content, Validatable 프로토콜을 준수하는데 의미는 다음과 같습니다.
Model 이란
Model
✓ Model은 Fluent를 사용하기 위해서는 반드시 준수해야되는 프로토콜입니다.
✓ Model은 DB에 고정된 데이터 구조를 나타냅니다.
✓ Model은 Codable 값을 저장할 수 있는 하나 또는 여러개의 field를 가집니다.
✓ 프로퍼티 래퍼들은 field, identifier들을 표시하며 복잡한 mapping을 위해 사용됩니다
✓ Model은 반드시 schema를 필요로 합니다. (schema란 model과 테이블이 일치하는지 확인하기 위해서 사용됩니다)
static let schema: String = "users"
✓ Model은 반드시 unique identifier 가 존재해야 합니다.
- 이 Field는 반드시 @ID 프로퍼티 래퍼를 사용해야합니다.
- 또한 @ID 는 반드시 UUID 타입이여야 하며 모델이 만들어질 때 Fluent는 자동으로 UUID 식별자를 생성합니다.
@ID(key: .id)
var id: UUID?
Content란
Content
✓ Vapor의 content API는 HTTP 메시지를 쉽게 인코딩, 디코딩할 수 있게 해줍니다.
✓ JSON encoding이 기본적으로 사용되지만 URL-Encoded Form과 Multipart도 지원합니다.
Validatable란
Validatable이란 Vapor 에서 제공하는 API로 클라이언트로부터 받는 request를 Content API를 사용하여 decode하기 전에 검열하는 것 입니다. 잘못된 데이터를 막을 수 있으며 이는 유효하지 않은 데이터 같은 경우에 해당됩니다.
// 첫번째 인자: key값, as: 원하는 타입, is: 원하는 형태
// username은 String 타입이여야 하며 비어 있으면 안된다고 설정합니다.
validations.add("username", as: String.self, is: !.empty, customFailureDescription: "Useranme cannot be empty")
import Foundation
import Fluent
import Vapor
final class User: Model, Content, Validatable {
static let schema: String = "users"
@ID(key: .id)
var id: UUID?
@Field(key: "username")
var username: String
@Field(key: "password")
var password: String
init() { }
static func validations(_ validations: inout Validations) {
validations.add("username", as: String.self, is: !.empty, customFailureDescription: "Username cannot be empty.")
validations.add("password", as: String.self, is: !.empty, customFailureDescription: "Password cannot be empty.")
// between 6 and 10 characters
validations.add("password", as: String.self, is: .count(6...10), customFailureDescription: "Password must be between 6 and 10 characters long.")
}
}
DB 테이블 만들기
위에서 만든 모델을 이용하여 데이터를 저장하기 위해서는 데이터베이스에 테이블이 존재해야 합니다. 데이터베이스에 테이블을 만들기 위해서는 AsyncMigration 프로토콜을 준수하는 구조체를 만듭니다.
AsyncMigration을 준수하기 위해서는 prepare 와 revert 를 구현해야합니다. 각각 하는 역할은 prepare는 db 테이블을 만드는 것과 revert는 db 테이블을 삭제하는 것으로 이루어집니다.
struct CreateUsersTableMigration: AsyncMigration {
func prepare(on database: any Database) async throws {
try await database.schema("users")
.id()
.field("username", .string, .required).unique(on: "username")
.field("password", .string, .required)
.create()
}
func revert(on database: any Database) async throws {
try await database.schema("users")
.delete()
}
}
prepare에서는 database에 users 스키마(테이블 이름)으로 id와 username, password 필드를 create() 메서드를 사용해 만듭니다.
func prepare(on database: any Database) async throws {
try await database.schema("users")
.id()
.field("username", .string, .required).unique(on: "username")
.field("password", .string, .required)
.create()
}
위처럼 AsyncMigration 프로토콜을 준수하는 구조체를 만들었다면 실제 데이터베이스에 만드는 작업을 수행합니다.
configure.swift 파일에서 다음과 같이 app.migartion.add 를 추가해줍니다.
import NIOSSL
import Fluent
import FluentPostgresDriver
import Vapor
// configures your application
public func configure(_ app: Application) async throws {
// uncomment to serve files from /Public folder
// app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
app.databases.use(DatabaseConfigurationFactory.postgres(configuration: .init(
hostname: Environment.get("DATABASE_HOST") ?? "localhost",
port: Environment.get("DATABASE_PORT").flatMap(Int.init(_:)) ?? SQLPostgresConfiguration.ianaPortNumber,
username: Environment.get("DATABASE_USERNAME") ?? "vapor_username",
password: Environment.get("DATABASE_PASSWORD") ?? "vapor_password",
database: Environment.get("DATABASE_NAME") ?? "vapor_database",
tls: .prefer(try .init(configuration: .clientDefault)))
), as: .psql)
app.migrations.add(CreateUsersTableMigration())
// register routes
try routes(app)
}
위에서 데이터베이스에 테이블을 생성하는 코드를 작성하였다면 그 코드가 적용될 DB가 존재해야 합니다.
PostgreSQL 사용하여 DB 만들기
다음 링크에서 PostgreSQL 를 설치해줍니다 https://postgresapp.com/downloads.html
이후 3개의 데이터베이스가 존재하는데 그 중 아무거나 선택을 합니다. 선택하게 되면 터미널이 생성되는데 터미널에서 다음 명령어를 통해 데이터베이스를 만들어줍니다.
CREATE DATABASE db이름;
그러면 다음과 같이 PostgreSQL 이름의 서버에 새로운 DB가 생성됩니다.
위에서 나타나는 정보를 아래에 추가해줍니다.
hostname: 호스트 이름
port: 사용할 서버의 포트 번호
username: DB를 사용할 사용자의 이름
password: DB에 접속하기 위한 비밀번호
database: DB의 이름
import NIOSSL
import Fluent
import FluentPostgresDriver
import Vapor
// configures your application
public func configure(_ app: Application) async throws {
// uncomment to serve files from /Public folder
// app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
app.databases.use(.postgres(configuration: SQLPostgresConfiguration(
hostname: Environment.get("localhost") ?? "localhost",
port: Environment.get("DATABASE_PORT").flatMap(Int.init(_:)) ?? SQLPostgresConfiguration.ianaPortNumber,
username: Environment.get("taewonyoon") ?? "taewonyoon",
password: Environment.get("") ?? "vapor_password",
database: Environment.get("loginprojectdb") ?? "loginprojectdb",
tls: .prefer(try .init(configuration: .clientDefault)))
), as: .psql)
app.migrations.add(CreateUsersTableMigration())
// register routes
try routes(app)
}
데이터베이스에 테이블 만들기
이후 터미널에서 swift run App migrate 를 실행시킵니다.
그러면 PostgreSQL 서버 데이터베이스에 테이블이 생성됩니다. 생성된 것을 확인하기 위해 Azure Data Studio를 사용하여 확인합니다.
Azure Data Studio를 사용하기 위해서는 다음과 같이 PostgreSQL 확장프로그램을 설치해야 합니다. 설치하였다면 New Connection을 눌러 설정 후 PostgreSQL 서버와 연결해 DB에 접근합니다.
연결이 된 후 다음과 같이 테이블이 성공적으로 만들어진 것을 확인할 수 있습니다. 값이 들어가 있는 것은 아래에서 설명합니다.
컨트롤러로 회원가입 요청받기
다음으로는 사용자로부터 요청을 받을 컨트롤러를 만듭니다.
import Fluent
import Vapor
struct UserController: RouteCollection {
func boot(routes: Vapor.RoutesBuilder) throws {
let api = routes.grouped("user") // 127.0.0.1:8080/user
api.post("register", use: register) // 127.0.0.1:8080/user/register
}
func register(req: Request) async throws -> RegisterResponseDTO {
// validate the user // validations
try User.validate(content: req) // req에 대해 검사합니다
let user = try req.content.decode(User.self) // 검사에 문제가 없으면 decode합니다.
// 이름이 이미 존재하는 이름인지 확인합니다.
if let _ = try await User.query(on: req.db)
.filter(\.$username == user.username) // 만약 db에 이미있는 username이라면 에러를 반환합니다
.first() {
return RegisterResponseDTO(error: true, reason: "Username is aleary taken")
}
// hash the password // 비밀번호를 그냥 넣지 않고 hash로 함호화하여 저장합니다.
user.password = try await req.password.async.hash(user.password)
// db에 저장하기
try await user.save(on: req.db)
// 여기까지 문제없이 실행되었다면 false를 Return하여 문제가 없음을 알려준다
return RegisterResponseDTO(error: false)
}
}
다음으로 컨트롤러를 routes.swift에 추가해 등록합니다.
import Fluent
import Vapor
func routes(_ app: Application) throws {
app.get { req async in
"It works!"
}
app.get("hello") { req async -> String in
"Hello, world!"
}
try app.register(collection: UserController())
}
vapor 서버 실행
마지막으로 빌드 이후 실행합니다
앱 회원가입 함수
앱에서 회원가입을 하기위한 함수는 다음과 같습니다.
func register(completion: @escaping (RegisterResponseDTO) -> Void) {
let data = ["username":id, "password":password]
AF.request(Constants().registerPath
,method: .post,
parameters: data,
encoder: JSONParameterEncoder.default).responseDecodable(of: RegisterResponseDTO.self){ response in
guard let dto = response.value else { return }
completion(dto)
}
}
앱에서 실행했을 때
이전에 이미 회원가입을 했기 때문에 에러가 발생하는 것까지 확인할 수 있습니다.
'SwiftUI' 카테고리의 다른 글
Publishers and Subscribers (Combine) (0) | 2024.04.11 |
---|---|
Combine을 이용하여 값 변화에 대응하는 뷰 만들기 (0) | 2024.04.10 |
타이머를 이용하여 아두이노 LED 켜기 (0) | 2024.04.09 |
Timer 타이머 (0) | 2024.04.08 |
JSON Data (0) | 2024.04.08 |