트랙
/
Swift
Swift
/
연습 문제
/
시 모임의 첫 번째 규칙
시 모임의 첫 번째 규칙

시 모임의 첫 번째 규칙

학습 연습 문제

소개

가져오기

Swift는 표준 라이브러리에서 풍부한 기능을 제공하지만, 외부 라이브러리와 프레임워크를 가져오면 훨씬 다양한 기능을 사용할 수 있어요. 이런 모듈은 Swift 프로젝트에서 직접 제공되기도 하고(Swift Argument Parser처럼), Apple 플랫폼 프레임워크에서 오기도 하며(Foundation과 Network Framework처럼), 서드파티 오픈 소스 라이브러리에서 올 수도 있어요(SwiftyBeaver처럼).

Foundation처럼 Swift에서 가장 널리 쓰이는 라이브러리 중 하나인 핵심 모듈은 Swift 툴체인에 함께 포함되어 있어요. 서드파티 라이브러리는 프로젝트 설정에 추가해야 가져올 수 있어요 (예를 들어 Swift Package Manager를 사용해서요).

모듈을 가져올 때는 import 키워드 뒤에 모듈 이름을 써요. 예를 들어 Foundation은 프로그램에 다음을 추가해서 가져올 수 있어요.

import Foundation

모듈을 가져오면 그 모듈의 타입과 메서드, 함수를 코드에서 사용할 수 있어요. 예를 들어 Foundation을 가져오면 String에 components(separatedBy:) 같은 유용한 유틸리티 메서드가 추가돼요.

관례적으로 import 문은 Swift 파일 맨 위에 두어서, 코드를 읽는 사람이 의존성을 바로 확인할 수 있게 해요.

문자열 인덱싱

String의 개별 문자는 배열에서 쓰는 것과 같은 서브스크립트 방식으로 접근할 수 있어요. 하지만 문자열에 쓰이는 인덱스는 정수가 아니어서 정수처럼 직접 다룰 수 없어요. 대신 비어 있지 않은 문자열에서 첫 번째 문자의 위치를 가리키는 startIndex처럼 이미 알고 있는 다른 인덱스를 기준으로, String과 NSString 라이브러리에서 제공하는 메서드를 사용해 계산해야 해요 (Foundation 모듈을 가져오면 Swift의 문자열에서 모든 NSString 속성과 메서드에 접근할 수 있어요).

예를 들어 다음 문자열이 있다고 해봐요.

let csv = "apple,pear,peach,orange,cherry,lime,goosberry"

"orange"의 "g"를 얻으려고 csv[21]이라고 쓸 수는 없어요. 대신 String.Index 타입의 값을 계산해서 그 인덱스를 넘겨야 해요. 이 인덱스들은 그 자체로 사람이 읽고 쓰도록 만든 값이 아니에요. 사람이 그 안에 무엇이 들어 있는지 알 필요가 없는, 이른바 _불투명 인덱스_예요.

let index = csv.index(csv.startIndex, offsetBy: 21)
csv[index]
// "g"
print(index)
// prints Index(_rawBits: 1376513)

그런데 오프셋이 유효한 인덱스가 아니면, 그러니까 startIndex보다 앞이거나 endIndex보다 뒤의 인덱스를 가리키면 오류가 발생해서 프로그램이 중단돼요. 이 문제를 막으려면 제한 인덱스를 지정할 수 있어요. 그러면 옵셔널 인덱스가 반환되고, 유효하지 않은 인덱스에는 nil이 반환돼요.

let tooFar = csv.index(csv.startIndex, offsetBy: 200)
// error: Execution was interrupted, reason: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0).
let tooFarButSafe = csv.index(csv.startIndex, offsetBy: 200, limitedBy: csv.endIndex)
// nil

또한 인덱스는 서로 다른 문자열끼리 공유할 수 없어요. 예를 들어 위에서 계산한 "orange"의 "g"에 대한 index로 fam = "This is my family: 👨‍👩‍👦‍👦, this is our dog: 🐶, this is our cat: 🐱!" 문자열을 인덱싱하면, 즉 fam[index]라고 쓰면 프로그램이 중단돼요.

이런 것에는 여러 이유가 있지만, 기본적으로는 모두 "유니코드는 까다롭다"로 귀결돼요.

일반적으로 개별 문자에 임의 접근이 필요하다면 Array<Character> 같은 다른 자료 구조를 쓰는 게 좋아요.

유용한 메서드 몇 가지

Swift에는 문자열의 첫 문자와 마지막 문자를 가져오는 first와 last 메서드가 있어요. 이 메서드들은 옵셔널 문자를 반환해서, 문자열이 비어 있으면 nil을 반환해요.

let str = "Hello, world!"
print(str.first) // Prints Optional("H")
print(str.last)  // Prints Optional("!")

prefix와 suffix 메서드로 문자열의 일부를 얻을 수도 있어요. 이 메서드들은 정수를 인자로 받아서 문자열의 앞이나 뒤 n개 문자를 담은 부분 문자열을 반환해요.

let str = "Hello, world!"
print(str.prefix(5)) // Prints "Hello"
print(str.suffix(6)) // Prints "world!"

문자열 메서드

텍스트를 다루는 일은 프로그래밍에서 흔한 작업이에요. Swift는 Foundation 프레임워크와 함께 문자열을 살펴보고, 변환하고, 조작하는 데 편리한 메서드와 속성을 많이 제공해요.

components(separatedBy:)로 문자열 나누기

구분자를 기준으로 문자열을 부분 문자열의 배열로 나누려면 components(separatedBy:)를 사용해요 (Foundation을 가져와야 해요):

import Foundation

let sentence = "Hello, world!"
let words = sentence.components(separatedBy: " ")
print(words) // Prints ["Hello,", "world!"]

hasPrefix(_:)와 hasSuffix(_:)로 접두사와 접미사 확인하기

문자열이 특정 문자 시퀀스로 시작하거나 끝나는지 확인하려면 hasPrefix(_:)와 hasSuffix(_:)를 사용해요:

let greeting = "Hello, world!"
print(greeting.hasPrefix("Hello"))  // Prints true
print(greeting.hasSuffix("world!")) // Prints true

대소문자 바꾸기

다음 표준 메서드와 속성으로 문자열의 대소문자를 바꿀 수 있어요:

  • lowercased(): 문자열의 소문자 복사본을 반환해요.
  • uppercased(): 문자열의 대문자 복사본을 반환해요.
  • capitalized: 각 단어의 첫 글자를 대문자로 바꾼 복사본을 반환해요 (Foundation에서 제공해요).
let greeting = "hello, world!"
print(greeting.lowercased())  // Prints "hello, world!"
print(greeting.uppercased())  // Prints "HELLO, WORLD!"
print(greeting.capitalized)   // Prints "Hello, World!"

replacingOccurrences(of:with:)로 부분 문자열 바꾸기

어떤 부분 문자열이 나오는 곳을 모두 다른 문자열로 바꾸려면 replacingOccurrences(of:with:)를 사용해요 (Foundation에서 제공해요):

let greeting = "Hello, world!"
let updated = greeting.replacingOccurrences(of: "world", with: "Swift")
print(updated) // Prints "Hello, Swift!"

joined(separator:)로 문자열 합치기

joined(separator:) 메서드는 [String] 같은 문자열 컬렉션에 속해요. 구분자를 사용해서 원소들을 하나의 문자열로 합쳐요:

let words = ["Hello", "world"]
let sentence = words.joined(separator: ", ")
print(sentence) // Prints "Hello, world"

지침

동네에 새로운 시 동아리가 문을 열었고, 가 볼까 생각 중이에요. 예전에 몇 번 사건이 있었던 터라, 이 동아리에는 아주 특별한 출입 규칙이 있어서 들어가기 전에 먼저 익혀 두어야 해요.

시 동아리에는 문이 두 개 있는데, 둘 다 문지기가 지키고 있어요. 들어가려면 그날의 비밀번호를 알아내야 해요.

1. 문자열을 개별 줄로 나누기

가장 먼저 할 일은 시를 개별 줄로 나누는 거예요. 문지기가 시를 읊어 주면, 그 시를 줄 단위로 나눠야 해요.

문자열을 받아서 줄 바꿈 문자를 구분자로 사용해 문자열의 배열로 나누는 splitOnNewlines 함수를 구현해요. 이 함수는 문자열의 배열을 반환해야 하는데, 각 문자열은 시의 한 줄이에요.

splitOnNewlines("Hello.\nHow are you?\n\nI'm doing fine.")
// returns ["Hello." ,"How are you?", "", "I'm doing fine"].

2. 정문

  1. 문지기가 시를 읊어요.
  2. 시를 개별 줄로 나눠야 해요.
  3. 각 줄의 첫 글자를 가져와야 해요.
    • 줄이 비어 있으면 대신 _를 사용해요.
  4. 글자들을 모아 이어 붙이면 하나의 단어가 돼요.

예를 들어, 그들이 가장 좋아하는 작가 중 한 명인 Michael Lockwood는 다음과 같은 아크로스틱 시를 썼는데, 아크로스틱이란 각 문장의 첫 글자를 모으면 단어가 되는 형식을 말해요:

Stands so high
Huge hooves too
Impatiently waits for
Reins and harness
Eager to leave

문지기가 시를 읊으면, 시를 개별 줄로 나누고 각 줄의 첫 글자로 답해요. 즉, ["S", "H", "I", "R", "E"]예요.

그다음 문지기는 답한 글자 배열로 만들어진 단어를 주는데, 그 단어를 첫 글자만 대문자로 바꾼 형태로 만들어야 해요. 마지막으로 반환하는 비밀번호가 "SHIRE"면 들어갈 수 있어요.

시를 나타내는 문자열을 받는 frontDoorPassword 함수를 구현해요. 이 함수는 문지기에게 말할 비밀번호를 반환해야 해요.

frontDoorPassword("Stands so high\nHuge hooves too\nImpatiently waits for\nReins and harness\nEager to leave")
// returns "SHIRE"

3. 뒷문

동아리 안쪽에는 가장 유명한 시인들이 있는데, 꼭 VIP 구역 같아요. 아무나 갈 수 있는 곳이 아니라서, 뒷문 절차는 조금 더 복잡해요.

  1. 문지기가 시를 읊어요.
  2. 시를 줄 단위로 나눠야 해요.
  3. 각 줄의 마지막 글자를 가져와야 해요.
    • 줄 끝에 공백이 남아 있을 때가 있으니, 그 공백을 없애야 해요.
  4. 글자들을 모은 다음, 그 글자로 만들어진 단어 끝에 ", please"를 붙여야 해요.

예를 들어, 앞에서 나온 시는 _텔레스틱_이기도 한데, 텔레스틱이란 각 문장의 마지막 글자를 모으면 단어가 되는 형식을 말해요:

Stands so high
Huge hooves too
Impatiently waits for
Reins and harness
Eager to leave

문지기가 시를 읊으면, 시를 개별 줄로 나누고 줄 끝의 공백을 없앤 다음, 각 줄의 마지막 글자로 답해요. 즉, ["h", "o", "r", "s", "e"]예요.

그다음 문지기는 답한 글자 배열로 만들어진 단어를 주는데, 그 단어를 첫 글자만 대문자로 바꾸고 ", please"를 붙여야 해요. 마지막으로 반환하는 비밀번호가 "horse, please"면 들어갈 수 있어요.

시를 나타내는 문자열을 받는 backDoorPassword 함수를 구현해요. 이 함수는 문지기에게 말할 비밀번호를 반환해야 해요.

backDoorPassword("Stands so high\nHuge hooves too\nImpatiently waits for\nReins and harness\nEager to leave")
// returns "horse, please"

4. 비밀의 방

동아리 안쪽 방에는 또 다른 문이 있는데, 이 문은 최고의 시인들만 들어갈 수 있는 비밀의 방으로 이어져요.

  1. 문지기가 시를 읊어요.
  2. 시를 줄 단위로 나눠야 해요.
  3. i번째 줄의 i번째 글자를 가져와야 해요.
  4. 글자들을 모은 다음, 그 글자로 만들어진 단어 끝에 "!"를 붙여야 해요.
  5. 그 단어를 모두 대문자로 바꿔야 해요.

예를 들어, 앞에서 나온 시를 살짝 바꾼 버전이 이 패턴에 맞아요:

Stands so high
Huge hooves too
Impatiently waits for
Rider with harness
Eager to leave

문지기가 시를 읊으면, 시를 개별 줄로 나누고 줄 끝의 공백을 없앤 다음, 각 줄의 i번째 글자로 답해요. 즉, ["S", "u", "p", "e", "r"]예요.

그다음 문지기는 답한 글자 배열로 만들어진 단어를 주는데, 그 단어를 모두 대문자로 바꾸고 "!"를 붙여야 해요. 마지막으로 반환하는 비밀번호가 SUPER!면 들어갈 수 있어요.

시를 나타내는 문자열을 받는 secretRoomPassword 함수를 구현해요. 이 함수는 문지기에게 말할 비밀번호를 반환해야 해요.

secretRoomPassword("Stands so high\nHuge hooves too\nImpatiently waits for\nRider with harness\nEager to leave")
// returns "SUPER!"
GitHub에서 편집 링크가 새 창이나 탭에서 열려요
Swift Exercism

시 모임의 첫 번째 규칙 문제를 시작해 볼 준비가 됐나요?

Exercism에 가입하고 Swift 트랙을 개념 35개연습 문제 116개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.