Swift에서의 한글, 특수문자 엔드포인트를 적절히 URL Encoding하기
https url에 엔드포인트로 한국어를 사용했을 때, 습관적으로 이렇게 코딩하곤 합니다.
import Foundation
var url = URL(string: "https://sesang06.tistory.com/")!
/// https://sesang06.tistory.com/
let koreanEndPoint = "헬로 월드!"
url.appendPathComponent(koreanEndPoint)
/// https://sesang06.tistory.com/%ED%97%AC%EB%A1%9C%20%EC%9B%94%EB%93%9C!
얼핏 보면 한글이 제대로 퍼센트 이스케이핑 되어있는 것처럼 보이지만, ''헬로 월드!'' 의 느낌표에 주목해 봅시다.
느낌표가 이스케이핑되지 않고 !로 그래도 나온 것을 확인해볼 수 있습니다.
Foundation 에서의 appendPathComponent 함수는 다음과 같이 정의되어 있지만, 별다른 알고리즘에 대한 도움을 얻기는 힘들었습니다.
/// Appends a path component to the URL.
///
/// - parameter pathComponent: The path component to add.
/// - parameter isDirectory: Use `true` if the resulting path is a directory.
public mutating func appendPathComponent(_ pathComponent: String, isDirectory: Bool) {
self = appendingPathComponent(pathComponent, isDirectory: isDirectory)
}
/// Appends a path component to the URL.
///
/// - note: This function performs a file system operation to determine if the path component is a directory. If so, it will append a trailing `/`. If you know in advance that the path component is a directory or not, then use `func appendingPathComponent(_:isDirectory:)`.
/// - parameter pathComponent: The path component to add.
public mutating func appendPathComponent(_ pathComponent: String) {
self = appendingPathComponent(pathComponent)
플레이그라운드를 열어서 써본 결과 다음과 같은 결과를 얻을 수 있었죠.
import Foundation
var url = URL(string: "https://sesang06.tistory.com/")!
let notReservedString = "-_.!~*'()@&=+$,:/"
url.appendPathComponent(notReservedString)
///https://sesang06.tistory.com/-_.!~*'()@&=+$,:/
var anotherUrl = URL(string: "https://sesang06.tistory.com/")!
let reservedString = ";?"
anotherUrl.appendPathComponent(reservedString)
///https://sesang06.tistory.com/%3B%3F
;와 ? 같은 일부 문자만 이스케이핑이 정상적으로 처리되고, 나머지 아스키 특문들은 제대로 이스케이핑이 되지 않습니다.
하지만 퍼센트 이스케이핑에서는 느낌표를 포함한 몇 개의 문자를 반드시 이스케이핑할 것을 요구하고 있죠.
규약[편집]
퍼센트 인코딩 규약은 RFC 3986에 정의되어 있다. 이 RFC에 따르면 URL에서 중요하게 사용되는 예약(reserved) 문자가 있고, 또한 인코딩이 필요하지 않은 비예약(unreserved) 문자가 존재한다.
예약 문자는 다음과 같다. 이들 중 일부는 URI에서 중요한 문법적 의미를 가지고 있기 때문에, 그 의미로 사용할 것이 아니라면 반드시 인코딩을 해야 한다.
! | * | ' | ( | ) | ; | : | @ | & | = | + | $ | , | / | ? | # | [ | ] |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
비예약 문자는 다음과 같다. 이들 문자는 퍼센트 인코딩을 할 필요가 없고, 인코딩을 안 하는 것을 권장한다.
A | B | C | D | E | F | G | H | I | J | K | L | M | N | O | P | Q | R | S | T | U | V | W | X | Y | Z |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
a | b | c | d | e | f | g | h | i | j | k | l | m | n | o | p | q | r | s | t | u | v | w | x | y | z |
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | - | _ | . | ~ |
즉, endPoint 가 일부 특문을 포함한 경우, appendPathComponent 를 사용하면 의도치 않은 결과를 얻을 수 있습니다.
그렇다면... addingPercentEncoding(withAllowedCharacters: CharacterSet.urlPathAllowed)
으로 인코딩하는 것은 어떨까 하고 시도해보았습니다.
url의 정의에서 경로를 이스케이핑하는거니, 정의에도 퍽 맞아 보였고요.
import Foundation
let baseString = "https//sesang06.tistory.com/"
let koreanEndPoint = "헬로 월드!"
let escapedKoreanEndPoint = koreanEndPoint.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlPathAllowed)
///"%ED%97%AC%EB%A1%9C%20%EC%9B%94%EB%93%9C!"
하지만 이 함수도 !를 적절히 이스케이핑하지 못하는 것을 확인할 수 있었습니다.
이스케이핑하지 않아도 되는 건 오직 알파벳 대, 소문자, 숫자와 "-_.~" 특수문자 뿐이었는데요.
결국 불가피하게 이런 코드를 작성할 수밖에 없었습니다.
import Foundation
let baseString = "https//sesang06.tistory.com/"
let koreanEndPoint = "헬로 월드!"
let escapingCharacterSet: CharacterSet = {
var cs = CharacterSet.alphanumerics
cs.insert(charactersIn: "-_.~")
return cs
}()
let escapedKoreanEndPoint = koreanEndPoint.addingPercentEncoding(withAllowedCharacters: escapingCharacterSet)!
///"%ED%97%AC%EB%A1%9C%20%EC%9B%94%EB%93%9C%21"
let url = URL(string: "\(baseString)\(escapedKoreanEndPoint)")
///https//sesang06.tistory.com/%ED%97%AC%EB%A1%9C%20%EC%9B%94%EB%93%9C%21
저의 경우 이 이슈로 인해 https://github.com/devxoul/MoyaSugar
라이브러리를 활용한 코드를 뜯어고칠 수밖에 없었습니다.
위 라이브러리는 Route와 url을 아래와 같은 식으로 정의합니다.
var route: Route {
return .get("/me")
}
// override default url building behavior
var url: URL {
switch self {
case .url(let urlString):
return URL(string: urlString)!
default:
return self.defaultURL
}
}
그런데 라이브러리 내부에서 route 를 baseURL과 합칠 때 다음과 같은 수를 씁니다.
public extension SugarTargetType {
public var url: URL {
return self.defaultURL
}
var defaultURL: URL {
return self.path.isEmpty ? self.baseURL : self.baseURL.appendingPathComponent(self.path)
} ///문제가 되는 부분
public var path: String {
return self.route.path
}
public var method: Moya.Method {
return self.route.method
}
public var task: Task {
guard let parameters = self.parameters else { return .requestPlain }
return .requestParameters(parameters: parameters.values, encoding: parameters.encoding)
}
}
appendingPathComponent 함수를 썼으므로, Route 에 아스키 코드 외의 , 혹은 @, ! 등의 특문을 포함한 엔드포인트를 정의할 경우 오류가 납니다.
그러자고 퍼센트 인코딩을 거기다가 해버릴 경우에는, 한글이 이중으로 이스케이핑 되버립니다.
따라서 다음과 같이 하드코딩하는 것이 최선이었죠.
var route: Route {
return .get("") ///여기선 엔드포인트를 정의하지 못하고..
}
var url: URL {
switch self {
case .url(let urlString, let koreanEndPoint):
let escapingString = properlyEscapingFunc(koreanEndPoint)
return URL(string: "\(urlString)\(escapingString)")!
///여기서 구태여 정의한다.
default:
return self.defaultURL
}
}
따라서 위 라이브러리를 사용할 땐, 엔드포인트의 값이 적절한지 반드시 확인이 필요해 보입니다.