diff --git a/docs/basics/client.ko.md b/docs/basics/client.ko.md new file mode 100644 index 000000000..dd13944f6 --- /dev/null +++ b/docs/basics/client.ko.md @@ -0,0 +1,76 @@ +# 클라이언트(Client) + +Vapor의 클라이언트는 외부 리소스에 HTTP 요청을 보낼 수 있습니다. 클라이언트는 [async-http-client](https://github.com/swift-server/async-http-client)을 기반으로 만들어졌으며, [content](content.md) API로 통합되어 있습니다. + +## 개요 + +`Application`을 통해 기본 클라이언트에 접근할 수 있습니다. 또는, 라우터 핸들러 안에서 `Request`를 통해 접근할 수 있습니다. + +```swift +app.client // Client + +app.get("test") { req in + req.client // Client +} +``` + +애플리케이션의 클라이언트는 설정(Configuration)을 하는 동안에 HTTP 요청을 만드는데 유용합니다. 만약 라우터 핸들러 안에서 HTTP 요청을 만든다면, 항상 request의 클라이언트를 사용하세요. + +### 메서드(Methods) + +원하는 URL을 `GET` 메서드에 전달해서 `GET` 요청을 만들어보세요. + +```swift +let response = try await req.client.get("https://httpbin.org/status/200") +``` + +`get`, `post`, 그리고 `delete` 같은 각각의 HTTP 메서드를 위한 메서드들이 있습니다. 클라이언트의 응답은 HTTP 상태 코드, 헤더, 본문을 포함하고, Future 형태로 반환됩니다. + +### 컨텐츠(Content) + +클라이언트의 요청과 응답에서 데이터를 처리하는 데 Vapor의 [content](content.md) API를 사용할 수 있습니다. 컨텐츠를 인코딩하거나, 쿼리 파라미터나 헤더를 요청에 추가하기 위해서는 `beforeSend` 클로저를 사용하세요. + +```swift +let response = try await req.client.post("https://httpbin.org/status/200") { req in + // Encode query string to the request URL. + try req.query.encode(["q": "test"]) + + // Encode JSON to the request body. + try req.content.encode(["hello": "world"]) + + // Add auth header to the request + let auth = BasicAuthorization(username: "something", password: "somethingelse") + req.headers.basicAuthorization = auth +} +// Handle the response. +``` + +비슷한 방식으로, `Content`를 사용해서 응답 본문을 디코딩 할 수 있습니다. + +```swift +let response = try await req.client.get("https://httpbin.org/json") +let json = try response.content.decode(MyJSONResponse.self) +``` + +만약 future을 사용한다면, `flatMapThrowing`을 사용할 수 있습니다. + +```swift +return req.client.get("https://httpbin.org/json").flatMapThrowing { res in + try res.content.decode(MyJSONResponse.self) +}.flatMap { json in + // Use JSON here +} +``` + +## 설정(Configuration) + +애플리케이션을 통해 내부 HTTP 클라이언트를 설정할 수 있습니다. + +```swift +// Disable automatic redirect following. +app.http.client.configuration.redirectConfiguration = .disallow +``` + +기본 클라이언트는 반드시 처음 _사용하기 전에_ 먼저 설정을 해야 합니다. + + diff --git a/docs/basics/controllers.ko.md b/docs/basics/controllers.ko.md new file mode 100644 index 000000000..2fc7afbd1 --- /dev/null +++ b/docs/basics/controllers.ko.md @@ -0,0 +1,70 @@ +# 컨트롤러(Controllers) + +컨트롤러는 코드를 그룹화하는데 좋은 방법입니다. 컨트롤러는 요청(request)을 받고 응답(response)을 반환하는 메서드들의 모음입니다. + +컨트롤러가 위치하기에 좋은 곳은 [Controllers](../getting-started/folder-structure.md#controllers) 폴더입니다. + +## 개요 + +예시 컨트롤러를 살펴보겠습니다. + +```swift +import Vapor + +struct TodosController: RouteCollection { + func boot(routes: RoutesBuilder) throws { + let todos = routes.grouped("todos") + todos.get(use: index) + todos.post(use: create) + + todos.group(":id") { todo in + todo.get(use: show) + todo.put(use: update) + todo.delete(use: delete) + } + } + + func index(req: Request) async throws -> [Todo] { + try await Todo.query(on: req.db).all() + } + + func create(req: Request) async throws -> Todo { + let todo = try req.content.decode(Todo.self) + try await todo.save(on: req.db) + return todo + } + + func show(req: Request) async throws -> Todo { + guard let todo = try await Todo.find(req.parameters.get("id"), on: req.db) else { + throw Abort(.notFound) + } + return todo + } + + func update(req: Request) async throws -> Todo { + guard let todo = try await Todo.find(req.parameters.get("id"), on: req.db) else { + throw Abort(.notFound) + } + let updatedTodo = try req.content.decode(Todo.self) + todo.title = updatedTodo.title + try await todo.save(on: req.db) + return todo + } + + func delete(req: Request) async throws -> HTTPStatus { + guard let todo = try await Todo.find(req.parameters.get("id"), on: req.db) else { + throw Abort(.notFound) + } + try await todo.delete(on: req.db) + return .ok + } +} +``` + +컨트롤러 메서드는 `Request`을 받고 `ResponseEncodable` 을 준수하는 응답을 반환해야 합니다. 동기 또는 비동기 방식 모두 가능합니다. + +마지막으로 `routes.swift`에서 컨트롤러를 등록해야 합니다. + +```swift +try app.register(collection: TodosController()) +``` diff --git a/docs/basics/environment.ko.md b/docs/basics/environment.ko.md new file mode 100644 index 000000000..c26d5ea28 --- /dev/null +++ b/docs/basics/environment.ko.md @@ -0,0 +1,144 @@ +# 환경설정(Environment) + +Vapor의 환경설정 API는 앱을 동적으로 설정할 수 있도록 합니다. 앱은 기본적으로 `development` 환경을 사용합니다. `production`이나 `staging` 같은 유용한 다른 환경을 정의할 수 있고, 각 케이스마다 다르게 설정할 수 있습니다. 필요에 따라 프로세스의 환경이나 `.env` 파일의 변수들을 사용할 수 있습니다. + +`app.environment`를 사용해서 현재 환경설정에 접근할 수 있습니다. `configure(_:)` 메서드 안에서 이 프로퍼티와 스위치를 사용하면 환경에 따라 다르게 설정할 수 있습니다. + +```swift +switch app.environment { +case .production: + app.databases.use(....) +default: + app.databases.use(...) +} +``` + +## 환경설정 변경하기 + +기본으로 앱은 `development` 모드로 실행됩니다. 앱을 부팅할 때, `--env` (`-e`) 플래그를 전달해서 환경설정을 변경할 수 있습니다. + +```swift +swift run App serve --env production +``` + +Vapor는 다음과 같은 환경이 있습니다. + +|name|short|description| +|-|-|-| +|production|prod|사용자들에게 제공하기 위한 환경입니다| +|development|dev|로컬 개발을 위한 환경입니다.| +|testing|test|유닛 테스트를 위한 환경입니다.| + +!!! info + 특별한 설정이 없다면 `production` 환경은 기본으로 `notice` 레벨로 로그를 기록합니다. 다른 환경들은 기본으로 `info` 레벨입니다. + +`--env` (`-e`) 플래그에 전체 또는 축약 이름을 전달해서 설정할 수 있습니다. + +```swift +swift run App serve -e prod +``` + +## 프로세스 변수(Process Variables) + +`Environment`는 프로세스의 환경 변수들에 접근할 수 있도록 간단한 문자열 기반의 API를 제공합니다. + +```swift +let foo = Environment.get("FOO") +print(foo) // String? +``` + +`get` 이외에도, `Environment`는 `process`를 통한 동적 멤버 조회 API도 제공합니다. + +```swift +let foo = Environment.process.FOO +print(foo) // String? +``` + +터미널에서 앱을 실행하는 경우에는 `export`를 사용해서 환경 변수를 설정할 수 있습니다. + +```sh +export FOO=BAR +swift run App serve +``` + +Xcode에서 앱을 실행하는 경우에는 `App` Scheme을 수정해서 환경 변수를 설정할 수 있습니다 + +## .env (dotenv) 파일 + +Dotenv 파일은 환경 변수를 자동으로 저장하기 위해서 Key-Value 형태의 리스트를 사용합니다. 이 파일은 수동으로 환경 변수를 설정할 필요가 없어서 쉽게 사용할 수 있습니다. + +Vapor는 현재 작업 디렉토리에서 dotenv 파일을 검색합니다. 만약 Xcode를 사용한다면 `App` Scheme에서 작업 디렉토리를 지정해 줄 필요가 있습니다. + +프로젝트 루트 폴더에 위치한 env 파일에 다음과 같은 내용이 있다고 가정하겠습니다. + +```sh +FOO=BAR +``` + +어플리케이션이 부팅될 때, 다른 프로세스 환경 변수처럼 파일의 컨텐츠에 접근할 수 있습니다. + +```swift +let foo = Environment.get("FOO") +print(foo) // String? +``` + +!!! info + 이미 프로세스 환경설정에 존재하는 변수들은 `.env` 파일에 명시되었더라도 재설정되지 않습니다. + +Vapor는 `.env` 파일에 외에도 현재 환경을 위한 dotenv 파일을 로드하려고 합니다. 예를 들어 `development` 환경에서는 Vapor는 `.env.development` 파일을 로드할 것입니다. 특정 환경의 `.env` 파일 안의 값들은 일반 `.env` 파일보다 우선시 됩니다. + +기본 값으로 구성된 템플릿으로서 `.env` 파일을 프로젝트에 포함하는 것은 일반적인 방식입니다. 특정 환경 파일은 `.gitignore`에서 다음 패턴을 사용해서 업로드되지 않도록 합니다. + +```gitignore +.env.* +``` + +새로운 컴퓨터에 프로젝트를 클론(clone) 할 때, `.env` 템플릿 파일은 복사되고 올바른 값들을 입력할 수 있습니다. + +```sh +cp .env .env.development +vim .env.development +``` + +!!! warning + 패스워드 같은 민감한 정보의 dotenv 파일들은 버전 관리에 절대로 커밋(commit) 되지 않도록 하세요. + +만약 dotfile를 로드하는데 어려움이 있다면, 더 많은 정보를 위해서 `--log debug` 플래그를 사용해서 디버그 로깅을 시도해 보세요. + +## 커스텀 환경설정 + +`Environment`을 확장해서 커스텀 환경설정 이름을 정의할 수 있습니다. + +```swift +extension Environment { + static var staging: Environment { + .custom(name: "staging") + } +} +``` + +어플리케이션의 환경설정은 보통 `entrypoint.swift`에서 `Environment.detect()`를 사용해서 설정합니다. + +```swift +@main +enum Entrypoint { + static func main() async throws { + var env = try Environment.detect() + try LoggingSystem.bootstrap(from: &env) + + let app = Application(env) + defer { app.shutdown() } + + try await configure(app) + try await app.runFromAsyncMainEntrypoint() + } +} +``` + +`detect` 메서드는 프로세스의 명령 줄 인자와 `--env` 플래그를 자동으로 사용합니다. 커스텀 `Environment` 구조체를 초기화하는 작업으로 이를 재설정할 수 있습니다. + +```swift +let env = Environment(name: "testing", arguments: ["vapor"]) +``` + +arguments 배열에는 최소한 하나의 실행 가능한 이름을 나타내는 인자를 포함해야 합니다. 명령 줄에 인자를 제공하는 것 같은 시뮬레이션을 하기 위해서, 추가적인 인자들을 제공할 수 있습니다. 특히 테스트를 위해서 사용할 때 유용합니다. diff --git a/docs/basics/errors.ko.md b/docs/basics/errors.ko.md new file mode 100644 index 000000000..888f44817 --- /dev/null +++ b/docs/basics/errors.ko.md @@ -0,0 +1,153 @@ +# 에러(Errors) + +Vapor는 에러 핸들링을 위한 Swift’s의 `Error` 프로토콜을 기반으로 구현되었습니다. 라우트 핸들러는 error을 던지거나(`throw`) 실패한 `EventLoopFuture`를 반환할 수 있습니다. Swift Error를 반환 또는 던지는 것은 `500` 상태 코드를 발생시키고, 에러가 로그로 기록됩니다. `AbortError`와 `DebuggableError`는 각각 응답 객체와 로깅을 수정하는 데 사용될 수 있습니다. `ErrorMiddleware` 에러들의 처리를 담당합니다. 이 미들웨어는 기본으로 application에 추가되어있고, 원한다면 커스텀 로직으로 교체할 수 있습니다. + +## 중단(Abort) + +Vapor는 `Abort`라는 이름의 기본 에러 구조체를 제공합니다. 이 구조체는 `AbortError`와 `DebuggableError` 모두를 준수합니다. HTTP 상태와 선택적인(optional) 실패 원인를 인자로 사용하여 초기화할 수 있습니다. + +```swift +// 404 error, default "Not Found" reason used. +throw Abort(.notFound) + +// 401 error, custom reason used. +throw Abort(.unauthorized, reason: "Invalid Credentials") +``` + +에러를 던지는 것이 지원되지 않고, 반드시 `EventLoopFuture`를 반환해야 하는 과거의 비동기 방식(`flatMap` 클로저처럼)에서는 실패한 future를 반환할 수 있습니다. + +```swift +guard let user = user else { + req.eventLoop.makeFailedFuture(Abort(.notFound)) +} +return user.save() +``` + +Vapor는 옵셔널 값의 Future을 추출(Unwrapping) 하는 것을 위해 `unwrap(or:)` 메서드를 제공합니다. + +```swift +User.find(id, on: db) + .unwrap(or: Abort(.notFound)) + .flatMap +{ user in + // Non-optional User supplied to closure. +} +``` + +만약 `User.find`가 `nil`을 반환한다면 future는 제공한 error와 함께 실패할 것입니다. nil이 아니라면 `flatMap`은 non-optional 값을 제공받을 것입니다. 만약 `async`/`await`를 사용한다면 optional을 평소처럼 처리할 수 있습니다. + +```swift +guard let user = try await User.find(id, on: db) { + throw Abort(.notFound) +} +``` + + +## 중단 에러(Abort Error) + +기본으로 라우트 클로저에 의해 던져지거나 반환되는 Swift `Error`는 `500 Internal Server Error` 응답을 발생시킵니다. 디버그 모드에서 빌드 될 때, `ErrorMiddleware`는 에러에 대한 설명을 포함시킵니다. 프로젝트가 릴리즈 모드로 빌드 됐다면, 보안 유지에 필요한 부분은 제거됩니다. + +HTTP 응답 상태 코드나 특정 에러의 원인을 반환하도록 설정하기 위해서는 `AbortError`를 채택하세요. + +```swift +import Vapor + +enum MyError { + case userNotLoggedIn + case invalidEmail(String) +} + +extension MyError: AbortError { + var reason: String { + switch self { + case .userNotLoggedIn: + return "User is not logged in." + case .invalidEmail(let email): + return "Email address is not valid: \(email)." + } + } + + var status: HTTPStatus { + switch self { + case .userNotLoggedIn: + return .unauthorized + case .invalidEmail: + return .badRequest + } + } +} +``` + +## 디버깅 가능한 에러(Debuggable Error) + +`ErrorMiddleware`는 라우트에서 던지는 에러를 로깅하기 위해서 `Logger.report(error:)` 메서드를 사용합니다. 이 메서드는 읽을 수 있는 메시지로 로그를 기록하기 위해 `CustomStringConvertible`와 `LocalizedError` 같은 프로토콜을 준수하는지 체크합니다. + +여러분의 에러에 `DebuggableError`를 채택함으로 에러 로깅을 커스터마이징 할 수 있습니다. 이 프로토콜은 고유 식별자(unique identifier), 소스 위치(source location), 스택 트레이스(stack trace) 같이 도움이 되는 다양한 프로퍼티를 포함하고 있습니다. 대부분의 프로퍼티가 옵셔널이기 때문에 프로토콜을 채택하는 것이 수월합니다. + +`DebuggableError` 를 잘 준수하기 위해서, 여러분의 에러는 필요한 소스 위치와 스택 정보를 저장할 수 있는 구조체인 것이 좋습니다. 아래는 이전에 언급된 `MyError`를 `struct`로 업데이트한 예시입니다. 소스 정보를 저장할 수 있게 되었습니다. + +```swift +import Vapor + +struct MyError: DebuggableError { + enum Value { + case userNotLoggedIn + case invalidEmail(String) + } + + var identifier: String { + switch self.value { + case .userNotLoggedIn: + return "userNotLoggedIn" + case .invalidEmail: + return "invalidEmail" + } + } + + var reason: String { + switch self.value { + case .userNotLoggedIn: + return "User is not logged in." + case .invalidEmail(let email): + return "Email address is not valid: \(email)." + } + } + + var value: Value + var source: ErrorSource? + + init( + _ value: Value, + file: String = #file, + function: String = #function, + line: UInt = #line, + column: UInt = #column + ) { + self.value = value + self.source = .init( + file: file, + function: function, + line: line, + column: column + ) + } +} +``` + +`DebuggableError`는 `possibleCauses`와 `suggestedFixes`같은 다양한 프로퍼티를 가지고 있습니다. 이런 프로퍼티들을 사용한다면 디버깅을 향상시킬 수 있습니다. 더 많은 정보를 위해서 프로토콜을 참고해 보시길 바랍니다. + +## 에러 미들웨어(Error Middleware) + +`ErrorMiddleware`는 기본으로 application에 추가되어 있는 두 개의 미들웨어 중 하나입니다. 이 미들웨어는 라우트 핸들러가 던지거나 반환한 Swift 에러를 HTTP 응답으로 변환시킵니다. 미들웨어가 없다면, 던져진 에러는 응답 없이 연결이 종료되는 결과를 발생시킬 수 있습니다. + +`AbortError`와 `DebuggableError`이 제공하는 것 이상의 에러 처리를 구현하기 위해서 여러분의 에러 핸들링 로직이 있는 `ErrorMiddleware`로 교체할 수 있습니다. 이것을 위해 첫 번째로 `app.middleware`를 수동으로 초기화해서 기본 에러 미들웨어를 제거해야 합니다. 그다음, 여러분의 에러 처리 미들웨어를 어플리케이션에 첫 미들웨어로서 추가하세요. + +```swift +// Clear all default middleware (then, add back route logging) +app.middleware = .init() +app.middleware.use(RouteLoggingMiddleware(logLevel: .info)) +// Add custom error handling middleware first. +app.middleware.use(MyErrorMiddleware()) +``` + +에러 처리 미들웨어는 다른 미들웨어보다 _최상단에_ 위치해야 합니다. 단, `CORSMiddleware`는 이 원칙의 예외입니다. diff --git a/docs/basics/logging.ko.md b/docs/basics/logging.ko.md new file mode 100644 index 000000000..2d3184b4b --- /dev/null +++ b/docs/basics/logging.ko.md @@ -0,0 +1,109 @@ +# 로깅(Logging) + +Vapor의 로깅 API은 [SwiftLog](https://github.com/apple/swift-log)을 기반으로 구성되었습니다. 따라서, Vapor는 모든 SwiftLog의 [backend implementations](https://github.com/apple/swift-log#backends)와 호환됩니다. + +## 로거(Logger) + +`Logger`의 인스턴스는 로그 메시지를 출력하는 데 사용됩니다. Vapor는 logger에 접근할 수 있는 몇 가지 방법을 제공합니다. + +### 요청(Request) + +각각 들어오는 `Request`은 고유의 로거를 가지고 있습니다. 해당 요청과 관련된 모든 로그를 위해서 로거를 사용할 수 있습니다. + +```swift +app.get("hello") { req -> String in + req.logger.info("Hello, logs!") + return "Hello, world!" +} +``` + +요청 로거는 들어오는 요청을 식별하기 위한 유니크한 UUID를 가지고 있습니다. 로그를 쉽게 추적할 수 있습니다. + +``` +[ INFO ] Hello, logs! [request-id: C637065A-8CB0-4502-91DC-9B8615C5D315] (App/routes.swift:10) +``` + +!!! info + 로거 메타데이터는 디버그 레벨 또는 그 이하에서만 출력됩니다. + +### Application + +앱이 부팅되고 설정되는 동안에는 `Application`의 로거를 사용해서 로그 메시지를 출력할 수 있습니다. + +```swift +app.logger.info("Setting up migrations...") +app.migrations.use(...) +``` + +### 커스텀 로거(Custom Logger) + +`Application`이나 `Request`에 접근할 수 없는 상황에서는 새로운 `Logger`를 생성할 수 있습니다. + +```swift +let logger = Logger(label: "dev.logger.my") +logger.info(...) +``` + +커스텀 로거도 설정된 데로 로그를 출력하지만, 요청 UUID 같은 중요한 메타데이터는 포함되지 않습니다. 가능한 request나 application의 고유 로거를 사용하세요. + +## 레벨(Level) + +SwiftLog는 여러 단계의 로깅 레벨을 지원합니다. + +|name|description| +|-|-| +|trace|프로그램의 실행을 추적하기 위한 정보를 포함하는 메시지에 적합합니다.| +|debug|프로그램을 디버깅하기 위한 정보를 포함하는 메시지에 적합합니다.| +|info|정보를 제공하는 메시지에 적합합니다.| +|notice|에러가 발생한 상태는 아니지만, 특별한 작업이 필요할 수 있는 상태에 적합합니다.| +|warning|에러가 발생한 상태는 아니지만, notice보다 심각한 상태에 적합합니다.| +|error|에러가 발생한 상태에 적합합니다.| +|critical|즉각적인 주의 조치가 필요한 치명적인 에러 상황에 적합합니다.| + +`critical` 메시지가 기록될 때, 로깅 백엔드 시스템은 디버깅을 위해서 시스템 상태를 기록하는 무거운 작업(Stack Traces를 기록하는 것 같은)을 자유롭게 수행할 수 있습니다. + +Vapor는 기본적으로 `info` 레벨의 로깅을 사용합니다. `production` 환경에서는 향상된 성능을 위해 `notice` 레벨이 사용됩니다. + +### 로그 레벨 변경 + +환경 모드에 상관없이, 생성되는 로그의 양을 늘리거나 줄이기 위해서 로깅 레벨을 재설정할 수 있습니다. + +첫 번째 방법은 어플리케이션을 부팅할 때, `--log` 옵셔널 플래그를 전달하는 것입니다. + +```sh +swift run App serve --log debug +``` + +두 번째 방법은 환경 변수로 `LOG_LEVEL`을 설정하는 것입니다. + +```sh +export LOG_LEVEL=debug +swift run App serve +``` + +두 가지 방법 모두 Xcode에서 `App` scheme을 수정해서 설정할 수 있습니다. + +## 설정(Configuration) + +SwiftLog는 프로세스당 한번 `LoggingSystem`을 초기화(bootstrapping) 해서 설정됩니다. Vapor 프로젝트는 일반적으로 `entrypoint.swift`에서 이 작업을 수행합니다. + +```swift +var env = try Environment.detect() +try LoggingSystem.bootstrap(from: &env) +``` + +`bootstrap(from:)` 메서드를 사용하면 명령행(command-line) 인자와 환경 변수를 기반으로 기본 로그 핸들러를 설정할 수 있습니다. 기본 로그 핸들러는 터미널에서 ANSI 색상이 적용된 로그를 제공합니다. + +### 커스텀 핸들러(Custom Handler) + +Vapor의 기본 로그 핸들러를 사용자의 핸들러로 재설정 할 수 있습니다. + +```swift +import Logging + +LoggingSystem.bootstrap { label in + StreamLogHandler.standardOutput(label: label) +} +``` + +SwiftLog가 지원하는 모든 로그 백엔드는 Vapor에서 사용할 수 있습니다. 그러나, 로그 레벨을 명령행 인자와 환경 변수로 설정하는 것은 오직 Vapor의 기본 로그 핸들러에서만 적용됩니다. diff --git a/docs/basics/routing.ko.md b/docs/basics/routing.ko.md new file mode 100644 index 000000000..b1b266468 --- /dev/null +++ b/docs/basics/routing.ko.md @@ -0,0 +1,436 @@ +# 라우팅(Routing) + +라우팅은 유입되는 요청(Incomming Request)에 적합한 요청 핸들러(Request Handler)를 찾는 과정입니다. Vapor 라우팅의 핵심에는 [RoutingKit](https://github.com/vapor/routing-kit)의 고성능, 트라이 노드(trie-node) 라우터가 있습니다. + +## 개요 + +Vapor에서 라우팅이 어떻게 동작하는지 이해하기 위해서는 먼저 HTTP 요청에 대한 몇 가지 기본 사항을 이해해야 합니다. 다음의 요청 예시를 참고해 주세요. + +```http +GET /hello/vapor HTTP/1.1 +host: vapor.codes +content-length: 0 +``` + +이것은 `/hello/vapor` URL로 보내는 간단한 `GET` 형태의 HTTP 요청입니다. 브라우저 주소창에서 다음의 URL을 입력했을 때, 브라우저가 보내는 HTTP 요청과 같은 종류입니다. + +``` +http://vapor.codes/hello/vapor +``` + +### HTTP 메서드 + +요청의 첫 번째 파트는 HTTP 메서드입니다. 가장 보편적인 HTTP 메서드는 `GET`입니다. 그러나 여러분이 자주 사용하는 몇 개의 메서드가 더 있습니다. 이 HTTP 메서드들은 [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) 개념과 관계가 있습니다. + +|Method|CRUD| +|-|-| +|`GET`|읽기| +|`POST`|생성| +|`PUT`|교체(덮어쓰기)| +|`PATCH`|부분 수정| +|`DELETE`|삭제| + +### 요청 경로(Request Path) + +HTTP 메서드 바로 뒤에는 요청의 URI가 있습니다. URI는 `/`로 시작하는 경로와 `?` 뒤에 따라오는 선택적인(Optional) 쿼리 스트링으로 구성됩니다. Vapor는 요청을 이 HTTP 메서드와 Path를 사용해서 라우팅합니다. + +URI 다음에는 HTTP 버전이 표시됩니다. 그 뒤에는 헤더와 본문(Body)이 올 수 있습니다. `GET` 요청에는 본문(Body)이 없습니다. + +### 라우터 메서드(Router Methods) + +다음 요청을 Vapor가 어떻게 처리하는지 살펴보겠습니다. + +```swift +app.get("hello", "vapor") { req in + return "Hello, vapor!" +} +``` + +일반적인 모든 HTTP 메서드는 `Application`의 메서드로 사용 가능합니다. 이 메서드들은 `/`로 구분되는 하나 또는 그 이상의 문자열 인자의 경로를 받습니다. + +참고로 `on` 뒤에 메서드를 명시하는 방식으로 사용할 수도 있습니다. + +```swift +app.on(.GET, "hello", "vapor") { ... } +``` + +경로가 등록되면, 예시의 HTTP 요청은 다음 HTTP 응답(response)을 반환할 것입니다. + +```http +HTTP/1.1 200 OK +content-length: 13 +content-type: text/plain; charset=utf-8 + +Hello, vapor! +``` + +### 경로 파라미터(Route Parameters) + +HTTP 메서드와 경로를 기반으로 요청을 성공적으로 라우팅했습니다. 이제는 동적 경로를 만들어보겠습니다. 이전에는 “vapor”라는 이름이 경로와 응답 모두에 고정되어 있었습니다. 이것을 동적으로 바꿀 수 있습니다. `/hello/`으로 변경해서, 입력되는 name에 따라 응답을 받을 수 있습니다. + +```swift +app.get("hello", ":name") { req -> String in + let name = req.parameters.get("name")! + return "Hello, \(name)!" +} +``` + +경로 컴포넌트 앞에 `:`을 붙이면, 라우터에게 해당 컴포넌트가 동적 컴포넌트 경로임을 나타냅니다. 이 자리에 오는 어떤 문자열이든 이 라우트와 매칭됩니다. `req.parameters`를 사용해서 해당 문자열의 값에 접근할 수 있습니다. + +만약 예시의 요청을 다시 실행한다면 여전히 “Hello, vapor!”라는 요청을 받을 것입니다. 그러나 이제는 `/hello/`뒤에 어떤 이름을 추가할 수 있고, 그 이름이 포함된 응답을 볼 수 있을 것입니다. `/hello/swift`로 요청을 보내보세요. + +```http +GET /hello/swift HTTP/1.1 +content-length: 0 +``` +```http +HTTP/1.1 200 OK +content-length: 13 +content-type: text/plain; charset=utf-8 + +Hello, swift! +``` + +이제 기본적인 것들을 살펴보았습니다. 각 섹션을 통해서 파라미터, 그룹 등 더 많은 것을 알아보세요. + +## 라우트(Routes) + +라우트는 주어진 HTTP 메서드와 URI 경로에 대한 요청 핸들러를 지정합니다. 또한, 추가적인 메타데이터를 저장할 수 있습니다. + +### 메서드(Methods) + +다양한 HTTP 메서드 헬퍼를 사용해서 `Application`에 라우트를 직접 등록할 수 있습니다. + +```swift +// responds to GET /foo/bar/baz +app.get("foo", "bar", "baz") { req in + ... +} +``` + +라우트 핸들러는 `ResponseEncodable`를 준수하는 모든 타입을 반환할 수 있습니다. 여기에는 `Content`, `async` 클로저, 그리고 미래의 결괏값이 `ResponseEncodable`을 준수하는 `EventLoopFuture`가 포함됩니다. + +`in` 앞에 `-> T`를 사용해서 라우트의 반환 타입을 지정할 수 있습니다. 반환 타입을 컴파일러가 결정할 수 없는 상황에서 유용하게 사용할 수 있습니다. + +```swift +app.get("foo") { req -> String in + return "bar" +} +``` + +지원하는 라우트 헬퍼 메서드는 다음과 같습니다. + +- `get` +- `post` +- `patch` +- `put` +- `delete` + +HTTP 메서드 헬퍼 이외에 `on` 함수도 있습니다. on 함수는 HTTP 메서드를 인자로 전달할 수 있습니다. + +```swift +// responds to OPTIONS /foo/bar/baz +app.on(.OPTIONS, "foo", "bar", "baz") { req in + ... +} +``` + +### 경로(Path Component) + +각 라우트 등록 메서드는 `PathComponent` 리스트를 가변 인자 형태로 받습니다. 이 타입은 문자열 리터럴로 표현 가능하고, 네 가지 케이스가 있습니다. + +- Constant (`foo`) +- Parameter (`:foo`) +- Anything (`*`) +- Catchall (`**`) + +#### 상수(Constant) + +상수는 정적 경로 컴포넌트입니다. 해당 위치에 문자열이 정확히 일치하는 요청만 허용됩니다. + +```swift +// responds to GET /foo/bar/baz +app.get("foo", "bar", "baz") { req in + ... +} +``` + +#### 파라미터(Parameter) + +동적 경로 컴포넌트입니다. 해당 자리의 어떤 문자열이든 허용합니다. `:`를 접두사를 사용하여 파라미터 경로 컴포넌트를 명시합니다. `:`뒤의 문자열은 파라미터의 이름으로 사용됩니다. 해당 이름을 사용해서 요청의 파라미터 값을 가져올 수 있습니다. + +```swift +// responds to GET /foo/bar/baz +// responds to GET /foo/qux/baz +// ... +app.get("foo", ":bar", "baz") { req in + ... +} +``` + +#### Anything + +Anything 컴포넌트는 파라미터 컴포넌트와 비슷합니다. 하지만, 값을 버린다는 점에서 차이가 있습니다. Anything 컴포넌트는 `*`로 명시할 수 있습니다. + +```swift +// responds to GET /foo/bar/baz +// responds to GET /foo/qux/baz +// ... +app.get("foo", "*", "baz") { req in + ... +} +``` + +#### Catchall + +Catchall은 하나 또는 그 이상의 컴포넌트와 매치되는 동적 경로 컴포넌트입니다. `**`을 사용해서 명시할 수 있습니다. 해당 위치 또는, 그 이후 위치에 오는 모든 문자열이 이 요청에 매치됩니다. + +```swift +// responds to GET /foo/bar +// responds to GET /foo/bar/baz +// ... +app.get("foo", "**") { req in + ... +} +``` + +### Parameters + +(접두사 `:`와 함께) 파라미터 경로 컴포넌트를 사용할 때, 해당 위치의 URI 값이 `req.parameters`에 저장됩니다. 경로 컴포넌트의 이름을 사용해서 값에 접근할 수 있습니다. + +```swift +// responds to GET /hello/foo +// responds to GET /hello/bar +// ... +app.get("hello", ":name") { req -> String in + let name = req.parameters.get("name")! + return "Hello, \(name)!" +} +``` + +!!! tip + 라우트 경로에 :name이 포함되어 있다면 `req.parameters.get`에는 `nil`이 절대로 반환되지 않을 것입니다. 하지만, 만약 미들웨어나 여러 라우트들에서 공통적으로 사용하는 코드가 있다면, 라우트 파라미터에 접근할 때 `nil`이 반환될 가능성이 있습니다. 이를 고려한 작업이 필요합니다. + +!!! tip + 예를 들어 `/hello/?name=foo` 같은 URL에서 쿼리 파라미터를 가져오려면, Vapor의 Content API를 사용해야 합니다. URL 쿼리 스트링 안에서 URL 인코딩 데이터를 처리할 수 있습니다. 더 자세한 정보를 위해서 [`Content` reference](content.md)를 살펴보세요. + +`req.parameters.get`은 `LosslessStringConvertible` 타입을 준수하는 타입으로 자동 캐스팅합니다. + +```swift +// responds to GET /number/42 +// responds to GET /number/1337 +// ... +app.get("number", ":x") { req -> String in + guard let int = req.parameters.get("x", as: Int.self) else { + throw Abort(.badRequest) + } + return "\(int) is a great number" +} +``` + +Catchall (`**`)로 매치된 URI 값은 `[String]`으로 `req.parameters`에 저장됩니다. `req.parameters.getCatchall`을 사용해서 이 컴포넌트들에 접근할 수 있습니다. + +```swift +// responds to GET /hello/foo +// responds to GET /hello/foo/bar +// ... +app.get("hello", "**") { req -> String in + let name = req.parameters.getCatchall().joined(separator: " ") + return "Hello, \(name)!" +} +``` + +### 본문 스트리밍(Body Streaming) + +`on` 메서드를 사용해서 라우트를 등록할 때, 요청 본문(Body)을 어떻게 처리할지 지정할 수 있습니다. 요청 본문들은 핸들러를 요청하기 전에 기본적으로 메모리에 수집됩니다. 애플리케이션에 들어오는 요청이 비동기적으로 읽히더라도, 동기적으로 요청 콘텐츠 디코딩을 수행할 수 있게 해주는데 유용합니다. + +Vapor는 기본적으로 스트리밍 본문을 16KB로 제한합니다. `app.routes`를 사용해서 설정할 수 있습니다. + +```swift +// 스트리밍 본문 제한을 500kb로 증가시킵니다. +app.routes.defaultMaxBodySize = "500kb" +``` + +만약 수집된 스트리밍 본문이 설정된 제한을 초과하면 `413 Payload Too Large` 에러가 반환됩니다. + +각각의 라우트마다 요청 본문 수집 전략을 설정하려면 `body` 파라미터를 사용하세요. + +```swift +// 라우트가 실행되기 전, 스트리밍 본문을 최대 1mb로 수집합니다. +app.on(.POST, "listings", body: .collect(maxSize: "1mb")) { req in + // Handle request. +} +``` + +라우트에 `collect` 메서드로 `maxSize`를 전달하면 애플리케이션의 기본값보다 우선되어 적용됩니다. application의 기본값을 사용하려면 `maxSize` 인자를 생략하세요. + +파일 업로드 같은 대용량 요청의 경우, 요청 본문을 버퍼에 수집하는 것은 잠재적으로 시스템 메모리에 부담을 줄 수 있습니다. 요청 본문이 수집되는 것을 막기 위해서는 `stream` 전략을 사용하세요. + +```swift +// 요청 본문을 버퍼로 수집하지 않습니다. +app.on(.POST, "upload", body: .stream) { req in + ... +} +``` + +요청 본문이 스트리밍 될 때, `req.body.data`는 `nil`입니다. 경로로 보내지는 각 조각 데이터를 처리하기 위해서는 `req.body.drain`를 사용해야 합니다. + +### 대소문자를 구분하지 않는 라우팅(Case Insensitive Routing) + +대소문자를 구분하고 유지하는 것은 라우팅의 기본 동작입니다. `Constant` 경로 컴포넌트는 라우팅의 목적에 따라 대소문자를 구분하지 않으면서 원래의 대소문자 형태를 유지하도록 처리할 수 있습니다. 이 동작을 사용하기 위해서는 application의 시작 전에 다음과 같이 설정하세요. + +```swift +app.routes.caseInsensitive = true +``` + +원래의 요청은 변경되지 않습니다. 라우트 핸들러는 수정되지 않은 요청 경로 컴포넌트를 수신할 것입니다. + +### Viewing Routes + +`app.routes`를 사용하거나 `Routes` 서비스를 생성해서 Application의 라우트에 접근할 수 있습니다. + +```swift +print(app.routes.all) // [Route] +``` + +Vapor는 `routes`라는 명령어를 제공합니다. 이 명령어는 사용 가능한 모든 라우트들을 ASCII 형식의 테이블로 출력합니다. + +```sh +$ swift run App routes ++--------+----------------+ +| GET | / | ++--------+----------------+ +| GET | /hello | ++--------+----------------+ +| GET | /todos | ++--------+----------------+ +| POST | /todos | ++--------+----------------+ +| DELETE | /todos/:todoID | ++--------+----------------+ +``` + +### 메타데이터(Metadata) + +모든 라우트 등록 메서드는 생성된 `Route`를 반환합니다. 이를 통해 라우트의 `userInfo` Dictionary에 메타데이터를 추가할 수 있습니다. 설명을 추가하는 것처럼 기본적으로 사용할 수 있는 몇 가지 메서드들이 있습니다. + +```swift +app.get("hello", ":name") { req in + ... +}.description("says hello") +``` + +## 라우트 그룹(Route Groups) + +라우트를 그룹화해서 경로 접두사나 특정 미들웨어가 있는 라우트 집합을 생성할 수 있습니다. 그룹화는 빌더와 클로저 기반의 문법을 제공합니다. + +모든 그룹화 메서드는 `RouteBuilder`를 반환합니다. `RouteBuilder`는 다른 라우트 빌딩 메서드와 함께 제한 없이 혼합, 매치, 중첩시킬 수 있습니다. + +### 경로 접두사(Path Prefix) + +라우트 그룹에 경로 접두사를 사용해서 하나 또는 그 이상의 경로 컴포넌트를 라우트의 그룹 앞에 추가할 수 있습니다. + +```swift +let users = app.grouped("users") +// GET /users +users.get { req in + ... +} +// POST /users +users.post { req in + ... +} +// GET /users/:id +users.get(":id") { req in + let id = req.parameters.get("id")! + ... +} +``` + +`get` 이나 `post` 같은 메서드에 전달할 수 있는 경로 컴포넌트는 `grouped`에도 전달할 수 있습니다. 클로저 기반의 문법으로 사용할 수도 있습니다. + +```swift +app.group("users") { users in + // GET /users + users.get { req in + ... + } + // POST /users + users.post { req in + ... + } + // GET /users/:id + users.get(":id") { req in + let id = req.parameters.get("id")! + ... + } +} +``` + +경로 접두사 라우트 그룹을 중첩해서 CRUD API를 간결히 정의할 수 있습니다. + +```swift +app.group("users") { users in + // GET /users + users.get { ... } + // POST /users + users.post { ... } + + users.group(":id") { user in + // GET /users/:id + user.get { ... } + // PATCH /users/:id + user.patch { ... } + // PUT /users/:id + user.put { ... } + } +} +``` + +### 미들웨어(Middleware) + +경로 컴포넌트를 접두사로 붙이는 것 외에도, 미들웨어를 라우트 그룹에 추가할 수도 있습니다. + +```swift +app.get("fast-thing") { req in + ... +} +app.group(RateLimitMiddleware(requestsPerMinute: 5)) { rateLimited in + rateLimited.get("slow-thing") { req in + ... + } +} +``` + +서로 다른 인증 미들웨어를 사용해서 라우트들의 일부 하위 부분들을 보호할 수 있습니다. + +```swift +app.post("login") { ... } +let auth = app.grouped(AuthMiddleware()) +auth.get("dashboard") { ... } +auth.get("logout") { ... } +``` + +## 리다이렉션(Redirections) + +리다이렉트는 다양한 시나리오에서 유용합니다. SEO를 위해서 옛날 주소를 새로운 주소로 이동시키거나, 인증이 되지 않은 사용자를 로그인 페이지로 이동시키거나, 새로운 API 버전에서 하위 호환성을 지원하기 위해 사용할 수 있습니다. + +리다이렉트를 요청하기 위해서는 아래와 같이 사용하세요. + +```swift +req.redirect(to: "/some/new/path") +``` + +리다이렉트의 유형을 지정할 수도 있습니다. (SEO가 적절하게 업데이트 되도록) 페이지를 영구적으로 리다이렉트 할 수 있습니다. + +```swift +req.redirect(to: "/some/new/path", redirectType: .permanent) +``` + +`Redirect`의 차이는 다음과 같습니다. + +* `.permanent` - **301 Permanent** 리다이렉트를 반환합니다. +* `.normal` - **303 see other** 리다이렉트를 반환합니다. Vapor의 기본값은 303입니다. 클라이언트에게 **GET** 요청으로 리다이렉트를 지시합니다. +* `.temporary` - **307 Temporary** 리다이렉트를 반환합니다. 클라이언트에게 원래의 HTTP 요청을 유지하도록 합니다. + +> 적절한 리다이렉션 상태 코드를 선택하기 위해서는 [전체 리스트](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#3xx_redirection)를 참고하세요. diff --git a/docs/contributing/contributing.ko.md b/docs/contributing/contributing.ko.md new file mode 100644 index 000000000..0f84f0415 --- /dev/null +++ b/docs/contributing/contributing.ko.md @@ -0,0 +1,57 @@ +# Vapor에 기여하기 + +Vapor는 커뮤니티 중심의 프로젝트입니다. 그리고, 커뮤니티 멤버들의 기여는 Vapor 개발에서 아주 중요한 부분입니다. 이 가이드는 Vapor에 기여하는 과정을 이해하고, Vapor에서 여러분이 첫 커밋을 할 수 있도록 도울 것입니다. + +여러분의 기여는 어떤 형태든 큰 도움이 될 것입니다. 오타 수정과 같은 작은 기여도 Vapor를 사용하는 사람들에게 큰 차이를 만들어낼 수 있습니다. + +## 행동 강령(Code of Conduct) + +Vapor는 Swift의 행동 강령을 채택하고 있습니다. Swift 행동 강령은 [https://www.swift.org/code-of-conduct/](https://www.swift.org/code-of-conduct/)에서 확인할 수 있습니다. 모든 기여자분들은 이 행동 강령을 준수해 주시길 바랍니다. + +## 무엇을 작업해야 할까요? + +오픈 소스에 처음 발을 내디딜 때, 어떤 것으로 작업을 시작할지 결정하는 것은 큰 난관이 될 수 있습니다. 보통 본인이 발견한 문제나 원하는 기능이 좋은 작업이 될 수 있습니다. 하지만, Vapor는 여러분이 기여하는 것을 돕기 위해, 유용한 것들을 준비해 놓았습니다. + +### 보안 문제 + +만약 보안 문제를 발견해서 그것을 알리거나 해결하는 데 도움을 주고 싶다면, **절대로 Issue를 생성하거나 Pull Request를 생성하지 마세요.** 수정 사항이 준비될 때까지 취약점이 노출되지 않도록 보안 문제에 대해서는 별도의 프로세스를 운영하고 있습니다. security@vapor.codes로 이메일을 보내거나 [여기](https://github.com/vapor/.github/blob/main/SECURITY.md)에서 상세 내용을 확인할 수 있습니다. + +### 작은 문제들 + +작은 이슈, 버그 또는 오타를 발견했다면, 부담 갖지 마시고 수정을 위한 Pull Request를 생성해 주세요. 만약 특정 저장소의 Open Issue를 해결하는 PR이라면, 사이드바에서 해당 Issue를 링크해 주세요. PR이 Merge 될 때, 해당 Issue는 자동으로 close 됩니다. + +![GitHub Link Issue](../images/github-link-issue.png) + +### 새로운 기능 + +새로운 기능이나 많은 양의 코드를 변경하는 버그 수정을 제안하고 싶다면, 먼저 Issue를 Open 하거나, Discord의 `#development` 채널에 글을 게시해 주세요. 적용이 필요한 특정 맥락(Context)이 있을 수 있거나, 가이드라인을 전달드릴 수도 있기 때문입니다. 채널을 통해서 해당 변경 사항에 관해 논의할 수 있습니다. Vapor의 계획과 맞지 않은 기능을 만드는 데 여러분의 시간을 낭비하는 것을 원하지 않습니다! + +### Vapor의 보드들(Vapor's Boards) + +여러분이 기여를 하고 싶지만 무엇을 해야 할지 아이디어가 떠오르지 않더라도 괜찮습니다! Vapor는 도움을 줄 수 있는 몇 가지 보드를 가지고 있습니다. Vapor에는 활발히 진행 중인 약 40개의 저장소가 있습니다. 이 저장소들을 모두 살펴보는 것은 현실적으로 어렵기 때문에, 관련 정보들을 한 곳에서 보여주는 보드를 활용하고 있습니다. + +첫 번째 보드는 [good first issue board](https://github.com/orgs/vapor/projects/14)입니다. Vapor's GitHub org의 Issue 중에는 `good first issue` 태그가 붙은 것들이 있습니다. 이 Issue들은 코드에 대한 많은 경험을 필요로 하지 않기 때문에 Vapor의 입문자들이 작업하기에 적합합니다. + +두 번째는 [help wanted board](https://github.com/orgs/vapor/projects/13)입니다. 이 보드는 `help wanted` 라벨이 붙어있는 Issue들을 보여줍니다. 이 Issue들은 수정되면 좋지만 코어(Core) 팀이 현재 우선순위가 높은 다른 곳에 집중하고 있는 상황입니다. 보통 `good first issue`보다는 지식이 조금 더 필요합니다. 하지만 재미있는 프로젝트가 될 것입니다. + +### 번역 + +마지막으로, 아주 가치 있는 기여는 문서입니다. 현재의 문서에는 여러 가지 언어로 된 번역본들이 있습니다. 하지만 아직 모든 페이지가 번역되어 있진 않습니다. 우리는 지원하고 싶은 많은 언어가 있습니다. 여러분이 새로운 언어로 기여하거나 업데이트하는데 관심이 있다면, [docs README](https://github.com/vapor/docs#translating)를 확인하거나 디스코드(Discord)에서 `#documentation` 채널을 찾아와 주세요. + +## 기여 과정(Contributing Process) + +오픈 소스 프로젝트에 아직 참여해 본 경험이 없다면, 실제 기여 과정은 조금 혼란스러울 수도 있습니다. 하지만 사실 꽤 간단합니다. + +첫째로, Vapor 또는 작업하고자 하는 저장소를 fork 하세요. GitHub와 GitHub UI에서 fork 할 수 있고, GitHub에 이것을 위한 [훌륭한 문서](https://docs.github.com/en/get-started/quickstart/fork-a-repo)가 준비되어 있습니다. + +fork된 저장소에서 평소처럼 Commit과 Push로 수정할 수 있습니다. 수정사항을 제출할 준비가 되었다면, Vapor 저장소로 PR을 생성할 수 있습니다. 역시나 GitHub에 [훌륭한 문서](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork)가 준비되어 있습니다. + +## Pull Request 제출하기 + +Pull Request를 제출할 때, 다음 사항들을 확인해 주세요. + +* 기존의 모든 테스트가 통과해야 합니다. +* 새로운 동작이 추가되거나 버그를 수정했다면, 이에 대한 새로운 테스트가 추가되어야 합니다. +* 새로운 Public API들은 문서화가 필요합니다. 우리는 API 문서화에 DocC를 사용하고 있습니다. + +Vapor는 수많은 작업과 일들을 줄이기 위해서 자동화를 사용하고 있습니다. Pull Request의 경우에는 [Vapor Bot](https://github.com/VaporBot)을 사용해서 자동으로 Pull Request을 Merge 하고 Releases를 생성합니다. Pull Request의 제목과 내용은 Release Notes 생성에 사용됩니다. 그러므로, 해당 내용이 명확하고 Release Note에 포함되어야 하는 내용이 적절하게 포함되어 있는지 확인해 주세요. [Vapor 기여 가이드라인](https://github.com/vapor/vapor/blob/main/.github/contributing.md#release-title)에서 더 자세한 내용을 확인할 수 있습니다. \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 4df2f495b..2922f60e1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -353,6 +353,7 @@ plugins: Content: 컨텐츠 Contributing: 기여하기 Contributing Guide: 기여 가이드 + Controllers: 컨트롤러 Crypto: 암호화 Custom Tags: 사용자 정의 태그 Deploy: 배포