루비가 주는 매력 중 하나는 바로 기능의 깊이입니다. 평소에 익숙하게 쓰던 연산자도 조금만 파고들면 아직 표면적인 부분만 사용하고 있었다는 사실을 깨닫게 됩니다. 겸손해 보이는 스플랫 연산자(*와 **)가 그 좋은 예입니다.
아마 여러분도 스플랫을 "나머지 전부(catch-all)" 인수를 받는 용도로 사용해 보셨을 겁니다. 그리고 대부분의 사람들이 스플랫을 이 용도로만 사용합니다.
def go(x, *args)
puts args.inspect
end
go("a", "b", "c")
새로운 키워드 인수 문법을 사용한다면 다음과 같이 더블 스플랫(double splat)을 사용하면 됩니다:
def go(**params)
puts params.inspect
end
go(x: 100, y: 200)
물론 이것도 유용하지만, 스플랫은 이보다 훨씬 더 많은 용도로 활용할 수 있습니다. 자세히 살펴보겠습니다!
배열로 여러 개의 인수 전달하기
스플랫은 메서드를 정의할 때뿐만 아니라 호출할 때도 사용할 수 있습니다. 스플랫을 사용하면 여러 개의 인수를 기대하는 함수에 배열을 통째로 넘길 수 있습니다. 배열의 첫 번째 요소는 첫 번째 인수로, 두 번째 요소는 두 번째 인수로 순서대로 전달됩니다.
def go(x, y)
end
point = [12, 10]
go(*point)
새 스타일의 키워드 인수에는 더블 스플랫을 사용할 수 있다는 점도 잊지 마세요:
def go(x:, y:)
end
point = { x: 100, y: 200 }
go(**point)
스플랫은 반드시 맨 끝에 있어야 하는 건 아니다
일반적으로 스플랫 인수는 인수 목록의 맨 끝에 두지만, 꼭 그래야 한다는 법칙은 없습니다.
인수 목록 어디에든 스플랫을 배치할 수 있습니다.
def go(x, *args, y)
puts x # => 1
puts y # => 5
puts args.inspect # => [2,3,4]
end
go(1, 2, 3, 4, 5)
배열 분해(Destructuring)
지금까지 살펴본 인수 관련 트릭들은 사실 배열 분해(array destructuring)의 특수한 경우에 불과합니다.
"배열 분해"라는 용어가 낯설다면 간단히 설명하자면, 배열을 개별 항목으로 쪼개는 것을 의미합니다. 다음과 같이 사용합니다:
a, b = [1, 2]
puts a
# 1
puts b
# 2
동작은 잘 하지만, 배열의 모든 항목마다 변수를 하나씩 지정해야 한다는 점이 번거로울 수 있습니다. 스플랫 연산자는 와일드카드처럼 동작하면서 이 문제를 우아하게 해결해 줍니다. 몇 가지 예제를 살펴보겠습니다.
배열에서 첫 번째 항목 꺼내기
원본 배열을 변경하지 않으면서 첫 번째 항목만 꺼내고 싶을 때가 종종 있습니다. 다음 예제가 바로 그 역할을 합니다.
first, *remainder = [1, 2, 3, 4, 5]
first
# => 1
remainder
# => [2, 3, 4, 5]
첫 번째 항목만 필요하고 나머지 배열은 필요 없다면 다음 문법을 사용하세요:
first, * = [1, 2, 3, 4, 5]
first
# => 1
마지막 항목 꺼내기
배열의 시작이 아니라 끝에서 항목을 꺼내려면 스플랫을 앞쪽에 배치하면 됩니다:
*prefix, last = [1, 2, 3, 4, 5]
last
# => 5
prefix
# => [1, 2, 3, 4]
마찬가지로 특정 변수가 필요 없다면 굳이 할당하지 않아도 됩니다.
배열의 처음과 마지막 n개 항목 가져오기
스플랫 연산자를 가운데에 놓으면 배열 양쪽 끝에서 원하는 만큼의 항목을 뽑아낼 수 있습니다.
first, *, last = [1, 2, 3, 4, 5]
first
# => 1
last
# => 5
한계점
배열 분해에서 스플랫 연산자를 사용할 때는 여전히 배열 항목의 위치를 배열의 시작과 끝을 기준으로 지정해야 합니다. 따라서 긴 배열의 중간에서 항목을 추출하는 용도로는 적합하지 않습니다.
또한 해시를 다룰 때 더블 스플랫(**) 연산자로 활용할 만한 멋진 트릭은 찾기 어렵습니다. 아쉽네요!
배열 구성하기
스플랫 연산자는 배열을 분해하는 데만 유용한 것이 아니라, 배열을 만들 때도 유용합니다.
다음 예제에서는 스플랫을 사용해 두 배열을 하나로 합칩니다.
[*[1,2], *[3,4]]
=> [1, 2, 3, 4]
이는 [[1, 2], [3,4]].flatten과 동일한 결과입니다.
여기까지만 봤다면 크게 유용하지 않다고 느낄 수 있습니다. 하지만 스플랫에는 또 다른 독특한 능력이 있습니다. 바로 객체를 지능적으로 배열로 변환(coerce)하는 기능입니다.
# 일반적으로 `*thing`은 `thing`을 배열로 감쌉니다
x = *"hi mom"
# => ["hi mom"]
# ...단, nil인 경우는 예외입니다
x = *nil
# => []
# 배열은 그대로 통과됩니다
x = *[1,2,3]
# => [1, 2, 3]
# 하지만 해시는 배열로 변환됩니다
x = *{a: 1}
# => [[:a, 1]]
덕분에 복잡한 타입 변환 작업 없이도 배열을 손쉽게 만들 수 있는 도구가 생깁니다.
예를 들어, 설정 시스템을 위해 문자열 배열을 수집한다고 상상해 보세요. 일반적으로는 다음과 같이 처리해야 합니다:
- 배열이 존재하는지 확인하고, 없다면 초기화하기
- 문자열 하나가 아니라 문자열 배열이 추가되려 할 때도 지능적으로 대응하기
스플랫 연산자는 이런 처리를 공짜로 해줍니다:
# 설정 해시에는 :ignore 배열이 있을 수도, 없을 수도 있습니다.
config = { }
# 콜백 함수는 배열을 반환할 수도 있고,
# 단일 항목을 반환할 수도 있습니다.
def add_ignores
"scoundrels" # ["scoundrels", "cads", "ne'er-do-wells"] 같은 배열일 수도 있습니다
end
# 마법이 일어나는 부분입니다. 시작점이 무엇이든
# 무시할 항목들의 배열을 얻게 됩니다.
config[:ignore] = [*config[:ignore], *add_ignores()]
마무리
기존의 모든 배열 조작 코드를 스플랫으로 다시 작성해서는 안 된다는 점은 말씀드리지 않아도 알겠죠? 스플랫이 항상 정답은 아니며, 남용하면 코드 가독성이 오히려 떨어질 수 있습니다.
하지만 때로는 배열 스플랫 트릭이 딱 필요한 순간이 옵니다. 그런 순간이 찾아온다면 한번 시도해 보시길 바랍니다. :)