새로운 루비 트릭, 패턴, 메서드를 배우는 가장 확실한 방법 중 하나는 다른 개발자들이 작성한 코드를 읽는 것입니다.
하지만 읽을 가치가 있는 흥미로운 코드는 어디에서 찾을 수 있을까요?
다행히 오픈소스 프로젝트와 코딩 챌린지 사이트가 많습니다. 특히 코딩 챌린지 사이트는 자신의 답안을 제출한 후 다른 개발자들의 풀이를 열람할 수 있어 학습에 큰 도움이 됩니다.
이 글에서 다룰 내용:
저는 여러분이 즐겁게 읽을 만한 7가지 코드 예제를 엄선했습니다. 각 예제마다 해설을 곁들여 최대한 많은 것을 얻어 가실 수 있도록 안내해 드리겠습니다.
두 수의 합 구하기
이 예제에서는 고유한 숫자로 이루어진 배열이 주어졌을 때, 두 숫자를 더했을 때 목표 숫자(target)가 되는 조합이 존재하는지 확인하는 방법을 알아봅니다.
코드:
def sum_eq_n?(arr, n)
return true if arr.empty? && n == 0
arr.product(arr).reject { |a,b| a == b }.any? { |a,b| a + b == n }
end
이 코드가 흥미로운 이유는 product 메서드를 활용했기 때문입니다.
product 메서드를 사용하면 마치 반복문 안에 반복문을 넣은 것처럼 배열 A의 모든 값과 배열 B의 모든 값을 조합할 수 있습니다.
카운팅, 매핑 그리고 찾기
(2, 4, 6, 10)처럼 등차수열에서 빠진 숫자를 찾고 싶다고 가정해 보겠습니다.
여기서는 숫자들 사이의 차이를 계산하는 전략을 사용할 수 있습니다.
[2, 2, 4]
우리의 목표는 이 수열이 어떤 패턴을 따르는지 파악하는 것입니다.
증가하는 수열인가요, 감소하는 수열인가요?
얼마나씩 변하나요?
다음 코드가 수열의 규칙을 밝혀 줍니다:
differences = [2, 2, 4]
differences.max_by { |n| differences.count(n) }
# 2
# 이것이 수열 내 숫자들 사이의 증가폭입니다
수열의 규칙을 알아냈다면, 모든 숫자를 비교해 빠진 숫자를 찾을 수 있습니다.
코드는 다음과 같습니다:
def find_missing(sequence)
consecutive = sequence.each_cons(2)
differences = consecutive.map { |a,b| b - a }
sequence = differences.max_by { |n| differences.count(n) }
missing_between = consecutive.find { |a,b| (b - a) != sequence }
missing_between.first + sequence
end
find_missing([2,4,6,10])
# 8
여기서 우리 대신 힘든 일을 처리해 주는 루비 메서드가 몇 개나 사용되었는지 한번 세어 보세요 🙂
정규표현식 예제
문자열을 다루면서 특정 패턴을 찾고 싶다면 정규표현식이 든든한 동반자가 되어 줄 것입니다.
다소 까다롭게 느껴질 수 있지만, 꾸준히 연습하면 완벽하게 익힐 수 있습니다!
자, 그럼 시작해 보겠습니다:
주어진 문자열이 "모음 → 비모음" 문자가 번갈아 나타나는 패턴을 따르는지 확인하고 싶다고 해봅시다.
예를 들면 이렇습니다:
"ateciyu"
이럴 때 정규표현식과 match? 메서드를 함께 사용하면 됩니다.
코드 예제입니다:
def alternating_characters?(s)
type = [/[aeiou]/, /[^aeiou]/].cycle
if s.start_with?(/[^aeiou]/)
type.next
end
s.chars.all? { |ch| ch.match?(type.next) }
end
alternating_characters?("ateciyu")
# true
주목할 점 몇 가지:
cycle메서드를 사용하면 모음 정규표현식과 비모음 정규표현식 사이를 계속 전환할 수 있습니다.chars메서드로 문자열을 문자 배열로 변환하면all?메서드를 활용할 수 있습니다.
재귀와 스택 예제
재귀(recursion)란 메서드가 스스로를 여러 번 호출하면서 해답에 한 걸음씩 다가가는 기법입니다.
흥미로운 많은 문제들이 재귀로 해결될 수 있습니다.
하지만 재귀에는 호출 깊이 제한이 있기 때문에, 상황에 따라 스택(stack) 자료구조를 대신 활용할 수도 있습니다.
이번 예제를 살펴보겠습니다:
주어진 배열의 "멱집합(Power Set)"을 구하는 문제입니다. 멱집합이란 배열에서 만들 수 있는 모든 부분집합의 집합을 의미합니다.
먼저 재귀를 사용한 예제입니다:
def get_numbers(list, index = 0, taken = []) return [taken] if index == list.size get_numbers(list, index+1, taken) + get_numbers(list, index+1, taken + [list[index]]) end get_numbers([1,2,3])
같은 문제를 스택으로 해결한 코드입니다:
def get_numbers_stack(list)
stack = [[0, []]]
output = []
until stack.empty?
index, taken = stack.pop
next output << taken if index == list.size
stack.unshift [index + 1, taken]
stack.unshift [index + 1, taken + [list[index]]]
end
output
end
핵심 아이디어는 알고리즘의 각 단계에서 숫자를 "취하거나" "취하지 않거나", 둘 중 하나를 선택한다는 것입니다.
양쪽 경우를 모두 분기해 시도함으로써 가능한 모든 조합을 만들어 냅니다.
나무(tree)를 상상해 보세요. 각 잎사귀가 하나의 해답입니다.
눈여겨볼 점 몇 가지:
- 재귀를 사용한 해법이 더 짧습니다.
- 알고리즘이 실제로 "진행"되는 부분(index + 1)은 거의 동일합니다.
- 루비에는
Stack클래스가 없기 때문에, 여기서 사용한 스택은 그저 배열일 뿐입니다.
메서드 체이닝 예제
이것은 제가 가장 좋아하는 예제입니다. 루비가 얼마나 강력한 언어인지 보여 주기 때문입니다.
메서드를 조합하면 한 메서드의 출력 결과를 다른 메서드의 입력으로 전달할 수 있습니다.
마치 공장 생산 라인과 같습니다!
원자재(입력값)로 시작해 메서드를 호출하는 과정을 거치며, 원자재를 원하는 결과물로 서서히 변형시키는 것이죠.
예제를 보겠습니다:
def longest_repetition(string)
max = string
.chars
.chunk(&:itself)
.map(&:last)
.max_by(&:size)
max ? [max[0], max.size] : ["", 0]
end
longest_repetition("aaabb")
# ["a", 3]
문자열이 주어지면 이 코드는 가장 길게 반복된 문자를 찾아 냅니다.
참고할 점:
- 가독성을 극대화하도록 코드가 정렬된 방식
Symbol#to_proc패턴(&:size)의 활용
참고로, 이것을 "데메테르의 법칙(Law of Demeter)"과 혼동하지 마세요.
그 법칙은 다른 객체의 내부까지 손을 뻗는 것에 관한 것입니다.
여기서는 객체를 변형할 뿐입니다.
with_index 예제
컬렉션을 순회하면서 현재 인덱스도 함께 얻고 싶으신가요?
with_index 메서드를 사용하면 됩니다.
예제입니다:
def reverse_alternate(string)
string.gsub(/[^\s]+/).with_index { |w, idx| idx.even? ? w : w.reverse }
end
reverse_alternate("Apples Are Good")
# "Apples erA Good"
주목할 점:
with_index와even?을 조합해 현재 단어를 뒤집어야 하는지 판단합니다.- 블록 없이 호출한
gsub은Enumerator객체를 반환하므로, 다른 메서드와 체이닝할 수 있습니다.
each_with_object 예제
또 하나 흥미로운 메서드는 each_with_object이며, 그 친구격인 with_object도 있습니다.
결과를 담아 둘 객체가 필요할 때 이 두 메서드를 사용할 수 있습니다.
예제입니다:
def clean_string(str)
str
.chars
.each_with_object([]) { |ch, obj| ch == "#" ? obj.pop : obj << ch }
.join
end
clean_string("aaa#b")
이 예제에서는 # 기호를 발견하면 마지막 문자를 삭제하고 싶습니다.
주목할 점:
each_with_object는 인수를 받는데, 이 인수는 결과를 담기 시작할 객체입니다. 이 인수는 블록의 두 번째 매개변수가 됩니다.- 문자열을 문자 배열로 변환한(
chars) 후, 작업이 끝나면 다시 문자열로 되돌립니다(join). - 삼항 연산자로 분기를 처리해 코드를 더 짧게 유지했습니다.
마무리
이 글에서는 여러분이 더 나은 루비 코드를 작성하는 데 도움이 되도록, 해설과 설명을 곁들인 흥미로운 코드 예제들을 살펴보았습니다.
더 많은 사람들이 이 글을 즐길 수 있도록 공유하는 것도 잊지 마세요.
읽어 주셔서 감사합니다!