본문 바로가기
카테고리 없음

텍스트 칼라 출력 기능 만들기1

by 경자꿈사 2024. 9. 9.

지난 '데이터 정렬해서 출력하기' 글에서 rainbow gem을 사용하여 텍스트에 색깔을 쉽게 입힐 수 있었는데 오늘은 rainbow gem이 제공하는 기능 중 아주 일부를 간단하게 만들어 볼까 한다.
다시 감을 잡기 위해 지나번 마지막 예제를 다시 한번 보자.

위의 그림을 보면 문자열 "Ruby" 를 Rainbow 메서드에 인수로 주고 그 결과에 대해 yellow 메서드를 호출했더니 원본 문자열 "Ruby" 가 "\e[33mRuby\e[0m" 이렇게 변경된 게 보이고
동일한 방식으로 "Ruby" 를 "Java" 로만 변경해서 만든 문자열 "\e[33mJava\e[0m" 을 puts 로 출력했더니 원하는대로 노란색 글씨로 화면에 'Java' 가 출력되었다.
그렇다면 문자열을 감싸고 있는(\e[33m 와 \e[0m) ANSI 이스케이프 코드를 색깔별로 알아내는 것부터 시작하자.
물론 구글링을 하면 쉽게 찾을 수 있겠지만 우리는 모든 색깔을 표현하는 게 목적이 아니라 rainbow gem 이 제공하는 메서드와 유사한 방식으로 코드를 작성하여 색깔을 표현하는 게 목적이기 때문에 몇 가지 색깔에 대한 ANSI 이스케이프 코드만 알면 되므로 irb 에서 직접 확인해 보자.


위의 그림처럼 빨간색, 노란색, 파락색, 초록색 등 네 가지 색깔에 대한 메서드를 통해 ANSI 이스케이프 코드를 쉽게 확인해 볼 수 있는데, 자세히 보면 ANSI 이스케이프 코드에 패턴이 있는 것을 알 수 있다.
"Ruby" 문자열을 감싸는 코드 중 오른쪽의 코드는 색깔과 무관하게 '\e[0m' 임을 알 수 있고 왼쪽의 코드는 숫자만 달라지는 게 보인다. 그래서 전체 문자열 중에서 숫자만 35로 변경해서 출력해 보면 보라색으로 출력되는 걸 볼 수 있다.
이제 rainbow gem 처럼 색깔을 메서드 이름으로 하는 나만의 색깔 출력 유틸리티 프로그램을 만들어 보자. 색깔 지원은 빨간색, 노란색, 파락색, 초록색 네 가지만 하자.
우선 아래 코드처럼 간단히 만들어 볼 수 있는데, rainbow gem과 같이 MyColor("Ruby").red 처럼 코드를 작성할 수 있게 MyColor 모듈을 하나 만들고 그 안에 MyColor 모듈과 동일한 이름의 메서드를 하나 만들었다.
D:/blog/ruby/my_color 폴더 아래 my_color.rb 파일로 아래 코드를 저장했는데 아래 테스트 그림처럼 이 파일을 로드해서 MyColor 모듈을 include로 믹스인 하면 MyColor 모듈에 정의해 놓은 MyColor 메서드를 호출할 수 있다.
(이에 대한 좀 더 자세한 설명을 원하면 '데이터 정렬해서 출력하기3' 글을 보길 바란다.)
MyColor 메서드는 문자열을 인수로 받아 단지 ColorableText 객체를 생성해 반환해 주기만 하고, 실제 MyColor 모듈 안에서 정의한 ColorableText 클래스가 색깔별 메서드를 제공해 주는 역할을 하고 있다.

module MyColor
  def MyColor(str)
    ColorableText.new(str)
  end

  class ColorableText
    def initialize(str)
      @str = str
    end

    def red
      "\e[31m#{@str}\e[0m"
    end

    def yellow
      "\e[33m#{@str}\e[0m"
    end

    def blue
      "\e[34m#{@str}\e[0m"
    end

    def green
      "\e[32m#{@str}\e[0m"
    end
  end
end

아래 그림처럼 my_color.rb 파일이 있는 위치에서 irb 를 실행해서 테스트해 보자.
이제 우리가 만든 MyColor 모듈을 사용해서도 rainbow gem과 유사한 형식으로 코드를 작성해서 색깔을 표시할 수 있게 되었다.

다음으로 배경 색깔도 지원하도록 MyColor 모듈을 수정해 볼 텐데, 우선 배경 색깔 관련 ANSI 이스케이프 코드를 확인해 보자.
아래처럼 bg 메서드에 빨간색, 노란색, 파락색, 초록색 등 네 가지 색깔을 배경색을 지정하여 ANSI 이스케이프 코드를 보면 텍스트 색깔 관련 ANSI 이스케이프 코드와 숫자만 다를뿐 동일한 형식임을 알 수 있다.

그리고 지금 중요한 건 아니지만 같은 색깔의 텍스트와 배경에 대한 ANSI 이스케이프 코드를 보면 숫자만 10만큼 차이가 날뿐 동일함을 알 수 있다.

아래 코드처럼 배경색을 지정하는 메서드 bg를 ColorableText 클래스에 추가한 후 테스트해 보면 텍스트의 배경색이 원하는대로 잘 나오는 것을 볼 수 있다.

module MyColor
  def MyColor(str)
    ColorableText.new(str)
  end

  class ColorableText
    def initialize(str)
      @str = str
    end

    ...생략

    def bg(color)
      color = color.to_s

      if color == "red"
        "\e[41m#{@str}\e[0m"
      elsif color == "yellow"
        "\e[43m#{@str}\e[0m"
      elsif color == "blue"
        "\e[44m#{@str}\e[0m"
      elsif color == "green"
        "\e[42m#{@str}\e[0m"
      else
        raise "지원하지 않는 배경색입니다" 
      end
    end
  end
end

그러면 이제 텍스트 색깔과 배경색을 함께 지정하려면 어떻게 해야 할까? 우선 rainbow gem 에서 ANSI 이스케이프 코드를 확인해 보자.
아래 그림을 보면 텍스트 색깔이든 배경색이든 나중에 지정한 것에 대한 ANSI 이스케이프 코드가 원본 문자열에 가장 가까이 붙어 있는 걸 볼 수 있다.
그리고 텍스트 색깔과 배경색을 몇 번을 지정하든 상관없이 원본 문자열 우측에는 ANSI 이스케이프 코드가 한 번만 온다는 것도 알 수 있다.

rainbow gem 처럼 텍스트 색깔과 배경색을 메서드 체이닝 방식으로 지정할 수 있도록 하려면 우리가 작성한 ColorableText 클래스의 메서드들은 모두 ColorableText 객체를 반환하도록 수정해야 한다.
아래 코드를 보면 우선 initialize 메서드에 ansi_code 파라미터를 추가하여 red, yellow, blue, green, bg 등의 메서드를 통해 ColorableText 객체를 새로 생성할 때 원본 문자열과 적용할 ANSI 이스케이프 코드를 각각 구분하여 인수로 넘기도록 했다. 왜냐하면 기존 코드처럼 ColorableText 객체를 생성할 때 문자열 인수 하나만 받는다면, 만약 그 문자열에 이미 ANSI 이스케이프 코드가 포함되어 있을 경우 또 다른 ANSI 이스케이프 코드를 삽입해 넣기가 번거롭기 때문이다.
프라이빗 메서드인 concat_ansi_code 가 현재 객체가 가지고 있는 ANSI 이스케이프 코드에 새로운 ANSI 이스케이프 코드를 연결해서 돌려주는 역할을 한다.
루비에서는 if 문도 하나의 표현식이고 모든 표현식은 결과적으로 하나의 값으로 평가되기 때문에 bg 메서드에서처럼 if 문의 결괏값을 바로 변수에 할당하는 것도 가능하다.
그리고 to_s 메서드를 ANSI 이스케이프 코드가 적용된 문자열을 생성하여 돌려주도록 재정의 하였다.

module MyColor
  def MyColor(str)
    ColorableText.new(str)
  end

  class ColorableText
    def initialize(str, ansi_code = "")
      @str = str
      @ansi_code = ansi_code
    end

    def red
      ColorableText.new(@str, concat_ansi_code("\e[31m"))
    end

    def yellow
      ColorableText.new(@str, concat_ansi_code("\e[33m"))
    end

    def blue
      ColorableText.new(@str, concat_ansi_code("\e[34m"))
    end

    def green
      ColorableText.new(@str, concat_ansi_code("\e[32m"))
    end
    
    def bg(color)
      color = color.to_s

      ansi_code = if color == "red"
        "\e[41m"
      elsif color == "yellow"
        "\e[43m"
      elsif color == "blue"
        "\e[44m"
      elsif color == "green"
        "\e[42m"
      else
        raise "지원하지 않는 배경색입니다" 
      end
      
      ColorableText.new(@str, concat_ansi_code(ansi_code))
    end
    
    def to_s
      "#{@ansi_code}#{@str}\e[0m"
    end
    
    def concat_ansi_code(ansi_code)
      "#{@ansi_code}#{ansi_code}"
    end
    
    private :concat_ansi_code
  end
end

이제 아래 그림처럼 텍스트 색깔과 배경색을 지원하는 색깔 범위 내에서 마음껏 조합해서 사용할 수 있게 되었다.

 

See you again~~