활동 요약

  • STEP 2 PR Feedback 접수 및 대응
    • 들여쓰기 (Indentation)와 라인 길이 초과로 인한 줄바꿈 (Line-Wrapping) 어떤 방식을 적용하면 좋을까?
    • TableView에서 indexPath를 획득하는 방법
      • Sender 전달인자
      • indexPathForSelectedRow 프로퍼티
    • namespace를 활용한 상수의 등록과 이용
      • Enum, struct, static let
    • UI 요소 프로퍼티의 이름짓기
    • os_log(_:log:_:)

활동 상세

Step 2 PR Feedback 접수 및 대응

어제 보낸 PR의 피드백을 정말 빠르게도 오늘 받을 수 있었어요. 코드의 Indentation, UILabelUITextView 간 선택, indexPath의 획득 방법 등 여러 가지에 대한 피드백을 접수했습니다. 피드백을 접수하고 관련 내용을 학습하여 어떻게 새로운 결과물에 반영하였는지 작성하겠습니다.

들여쓰기 (Indentation)와 라인 길이 초과로 인한 줄바꿈 (Line-Wrapping) 어떤 방식을 적용하면 좋을까?

결론
스타일은 결국 취향이다. 팀에서 정하는 규칙이 있다면 따르되 취향과 장단점에 따라 본인이 취사 선택하자.

팀원과 프로젝트를 진행하게 되면 각자가 지켜야할 팀의 규율인 그라운드 룰을 수립하고 지키는 것이 굉장히 중요합니다. 이번 주에는 개인 프로젝트를 진행하여 기존부터 적용하고 싶었던 내용들을 자유롭게 시도해볼 수 있었는데요, 그 중 제가 평소부터 관심을 가지고 있었던 것이 들여쓰기와 라인 래핑 (Line-Wrapping)이었습니다.

순전히 개인의 취향이라고는 하지만, 저는 언어 사용자들이 보편적으로 사용하고 있는 스타일을 따르기를 원했습니다. 그래서 각종 Swift 스타일 가이드를 찾아본 결과 아래와 같은 스타일 가이드들을 찾을 수 있었습니다.

들여쓰기 (Indentation)와 들여쓰기를 하는 방법 (spacing)

먼저 들여쓰기부터 살펴보시겠습니다. Xcode에서는 기본적으로 들여쓰기가 4 칸으로 설정되어 있죠.

세 스타일 모두 들여쓰기 시 칸 수는 2 칸, 공백을 추가하는 방법은 스페이스로 사용하자 약속하고 있습니다. 코드 길이가 길어 화면을 벗어나게 되어 시각적으로 줄바꿈이 되는 경우와 코드 길이가 길어 실제로 리턴 (엔터) 입력에 의한 줄바꿈 (line wrapping)을 삽입할 때도 현재 있던 위치에서 + 2 칸만큼 공백을 추가하도록 주문하고 있습니다.

Source: Google Swift Style Guide

Source: Raywenderlich Swift Style Guide

Source: StyleShare Swift Style Guide

비교

이미지를 통해 모습을 비교해볼까요?

  1. Indent width: 4 spaces

  2. Indent width: 2 spaces

큰 차이가 나지 않을 수 있지만 아래와 같은 장단점이 있을 수 있겠네요.

  • 들여쓰기 2 칸 적용 시
    • 장점: 하나의 라인 안에 많은 코드를 작성할 수 있다.
    • 단점: 코드 깊이 (depth) 판단이 4 칸에 비해 어려워 가독성이 떨어질 수 있다.

++ 야곰의 의견 - 습관은 무서우므로 들여쓰기를 많이 하지 않도록 4 칸을 사용하는 것도 좋다.

라인 래핑 (Line-Wrapping)

혹시 새로운 메서드를 작성하거나 메서드를 호출할 때 무심코 라인 길이가 굉장히 길어진 경험이 있으신가요? 저는 아래 이미지와 같은 경험이 있습니다.

에..? 19 번 라인이 한 줄이라구..?

57 번 라인의 상태가..?

이에 대한 통상적인 스타일에 대해서도 각종 스타일 가이드에서 다루고 있습니다. Line-Wrapping이라는 이름이죠.

위 이미지와 같이 아주 긴 메서드를 정의하고자 할 때는 어떤 방식으로 라인 래핑을 적용하면 좋을까요? 아래 이미지들을 보시면, 주황색 블럭 내부의 요소들은 라인 래핑을 적용하지 않고, 파란색 블럭 내부의 요소들은 판단에 따라 라인 래핑을 적용하도록 하고 있습니다.

적용한 모습을 보면 아래와 같이 되는 것이죠.

이와 마찬가지로 여러가지 요소들의 라인 래핑 가이드를 제공하고 있습니다.

적용

그럼 스타일 가이드에 따라 위에서 제시한 내용들에 따라 라인 래핑을 적용해 보겠습니다.

어떠신가요? 매개변수 하나당 하나의 라인을 차지하니 가독성이 개선되었다고 느끼시나요? 앞으로도 가독성 높은 코드 작성을 위해 노력해보자구요~!

TableView에서 상세 뷰로 이동하기 위해 indexPath를 획득하는 방법

부제: prepare(for:sender:)sender는 무엇일까?

TableView에서 Cell을 선택하면 상세 페이지로 이동하게끔 구성해야 하는 경우가 있습니다. 그럼 선택된 Cell이 무엇인지 알기 위해 indexPath.row 값을 찾아야 하는데요, 이번에는 해당 상황에서 어떻게 indexPath를 얻을 수 있는지 알아보겠습니다.

이전 포스팅에서 다음 화면으로 정보를 넘겨주는데 UIViewControllerprepare(for:sender:) 메서드를 사용할 수 있다는 것을 알아봤습니다. 동일하게 해당 메서드를 활용해볼게요.

부제: prepare(for:sender:)sender는 무엇일까?

TableView에서 Cell을 선택하면 상세 페이지로 이동하게끔 구성해야 하는 경우가 있습니다. 그럼 선택된 Cell이 무엇인지 알기 위해 indexPath.row 값을 찾아야 하는데요, 이번에는 해당 상황에서 어떻게 indexPath를 얻을 수 있는지 알아보겠습니다.

이전 포스팅에서 다음 화면으로 정보를 넘겨주는데 UIViewControllerprepare(for:sender:) 메서드를 사용할 수 있다는 것을 알아봤습니다. 동일하게 해당 메서드를 활용해볼게요.

1. tableView.indexPathForSelectedRow 프로퍼티를 사용한다.

TableView가 아니라 소문자로 시작하는 tableView일까요? 그 이유는 @IBOutlet 키워드와 함께 생성한 UITableView의 프로퍼티를 나타내기 때문입니다. 그래서 프로퍼티를 생성하실 때 설정하신 이름으로 적어주시면 됩니다. 그러면 prepare(for:sender:) 메서드를 사용할 때 아래와 같이 indexPath를 얻을 수 있죠.

// MARK: - View controller: segue
extension ArtworksTableViewController {
  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    let indexPath = tableView.indexPathForSelectedRow
    
        if segue.identifier == "showDetail" {
      let followingViewController = segue.destination as? ArtworkDetailViewController
      guard let rowOfIndexPath: Int = indexPath?.row else {
        os_log(.fault, log: .ui, "indexPath가 nil입니다.")
        return
      }

      followingViewController?.artwork = artworks[rowOfIndexPath]
    }
  }
}

그럼 두 번째 방법을 살펴볼까요?

2. prepare(for:sender:) 메서드의 sender를 활용한다.

앞서 말씀드린 prepare(for:sender:) 메서드에서 sender는 무엇을 의미할까요? 앞서 작성한 코드에서 LLDB를 통해 살펴본 sender의 정체를 이미지로 가져왔습니다!

결과적으로 sender는 해당 메서드를 실행(이 메서드의 경우 segue)시키는 주체를 나타내는 것으로 확인됩니다. 기본적으로 매개변수 타입 지정에 의해 Any? 타입이며 이 경우에는 Any? 속에 UITableViewCell 타입이네요. Any? -> UITableViewCell로 다운캐스팅하면 indexPath를 얻을 수 있으니 아래와 같이 코드를 작성할 수 있겠습니다.

// MARK: - View controller: segue
extension ArtworksTableViewController {
  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    guard let indexPath = tableView.indexPath(for: sender as! UITableViewCell) else {
      os_log(.fault, log: .ui, OSLogMessage.indexPathIsNil)
      return
    }
  
    if segue.identifier == Identifier.Segue.artworkDetail {
      let followingViewController = segue.destination as? ArtworkDetailViewController
    
      followingViewController?.artwork = artworks[indexPath.row]
    }
  }
}

날 것에 의미를 부여해보자!: NameSpace

코드를 작성하다보면 날 것의 숫자나 텍스트를 적용해야 하는 경우가 있습니다. 예를 들어 JSON 파일로부터 데이터를 불러올 때, 파일의 이름을 작성한다거나, UI 요소에 적용될 접미 접두사 같은 요소들이 있겠죠.

이와 같은 날 것의 코드는 최초 작성자를 포함하여 동료들까지 실수를 일으킬 수 있는 요소가 될 수 있기에 별도의 장소에 함께 작성해두었다가 불러서 사용하는 것이 더 좋은 선택일 수 있습니다. 날 것의 코드에 의미를 부여해줄 수 있기도 하구요.

예시 하나.

으잉? "exposition_universelle_1900"이 뭐람.. 파일 이름인가? 파일 이름 바뀌면 다른 곳에서도 다 바꿔야겠네.. 애초에 파일 이름이 맞는지도 잘 모르겠어.

충분히 이런 생각을 할 수 있다고 생각합니다. 이를 미연에 방지하려면 이러한 날 것의 코드를 모아서 관리하는 Name space를 만들 것을 고려해볼 수 있습니다. 아래 코드를 보실까요?

enum ExpoData {
  static let expoIntroduction: String = "exposition_universelle_1900"
  static let artworks: String = "items"
  static let posterImage: String = "poster"
}

Expo라는 것이 프로젝트임을 알고있다면, 이 열거 타입은 데이터와 관련된 타입이라는 것을 알 수 있겠네요. 소개와 관련된 자료는 expoIntroduction이, 미술품과 관련된 자료는 artworks가 가지고 있는 것도 파악이 가능할 것입니다. 그럼 이미지로 보여드렸던 내용도 아래와 같이 적용할 수 있겠죠.

파일 이름이 변경돼도 이 코드에서 변경할 점은 없겠네요. ExpoData라는 Name space에서 변경해주면 다른 파일에서 적용한 내용들도 함께 변경할 수 있으니까요.

예시 둘.

마찬가지로 UI 요소에 적용되는 날 것의 텍스트도 이러한 방식으로 관리할 수 있습니다. JSON 데이터에서 48130300이라는 숫자만 제공하는 상황에서 아래 이미지와 같이 방문객: , 에 해당하는 내용을 접두, 접미어로 붙여주어야할 때 아래와 같이 적용하시면 됩니다.

어떤가요? 저는 날 것의 텍스트가 코드에서 살아 숨쉬는 것 보다 훨씬 좋다고 생각합니다. 텍스트 뿐만 아니라 정수나 소수 모든 타입을 이러한 형식으로 선언하여 원하는 때마다 반복적으로 꺼내 쓸 수 있습니다!

아래가 궁금하시다면 계속해서 읽어주세요~!

좀 더 깊게 알고 싶어요. 왜 열거 타입에 타입 프로퍼티를 선언해서 사용하나요?

사실 Name space를 정의하는데 열거 타입을 꼭 사용하여야 하거나 타입 프로퍼티를 활용하지 않아도 됩니다. 구조체를 이용하거나 열거 타입의 case - rawValue를 활용해도 되죠. 이를 활용하면 아래와 같이 작성이 가능합니다.

// 구조체로 Name space를 작성하는 경우
struct ExpoData {
  static let expoIntroduction: String = "exposition_universelle_1900"
  static let artworks: String = "items"
  static let posterImage: String = "poster"
}

// 열거 타입의 case - rawValue 형식으로 작성하는 경우
enum ExpoData: String {
  case expoIntroduction = "exposition_universelle_1900"
  case artworks = "items"
  case posterImage = "poster"
}

실제로 위와 같은 형식으로 사용하기도 하는데요, 저는 개인적인 이유로 case가 없이 타입 프로퍼티만으로 작성된 열거 타입을 활용합니다. 그 이유는 의도치 않은 인스턴스를 생성하지 않을 수 있기 때문이에요.

1. 구조체와 비교

예를 들어, 위 예시와 같이 구조체로 Name space를 작성한 경우 ExpoData()를 통한 인스턴스 생성이 가능합니다. 아무 기능이 없더라도 의도치 않은 것임에는 틀림없죠. 이는 아래와 같이 이니셜라이저에 접근제한을 걸어둠으로써 인스턴스 생성을 방지할 수 있습니다.

// 구조체로 Name space를 작성하는 경우
struct ExpoData {
  static let expoIntroduction: String = "exposition_universelle_1900"
  static let artworks: String = "items"
  static let posterImage: String = "poster"
  
  private init() { }
}

저는 이렇게 이니셜라이저에 접근제한을 설정하는 것 또한 case 없는 열거 타입에 비해 불필요한 코드가 추가되는 것이라 생각해서 선호하지 않습니다.

2. 열거 타입의 case - rawValue로 정의된 경우와 비교

두 번째 예시를 다시 가져와 보겠습니다.

// 열거 타입의 case - rawValue 형식으로 작성하는 경우
enum ExpoData: String {
  case expoIntroduction = "exposition_universelle_1900"
  case artworks = "items"
  case posterImage = "poster"
}

제가 위와 같이 사용하지 않는 이유는 아래와 같습니다.

  • 해당 요소를 호출할 때 말미에 .rawValue를 추가해야 한다는 번거로움이 있습니다. ExpoData.artworks.rawValue처럼요. 저는 이렇듯 불필요하게 추가되는 요소를 선호하지 않습니다.
  • case가 존재하는 열거 타입의 경우 rawValue를 활용해서 이니셜라이징할 수 있으므로 구조체와 마찬가지로 의도치 않은 인스턴스를 생성할 수 있다는 단점이 존재합니다.
  • 열거 타입의 rawValue가 하나의 타입 (위 예시의 경우 String)으로 고정되므로 관련 있는 여러 타입을 하나의 Name Space에서 관리할 수 없습니다.

하지만 이 모든 것은 제 선호일 뿐이고 사용하시는 분들이 자유롭게 취사선택하시면 되겠습니다.

UI 요소 프로퍼티의 이름, 어떻게 지으면 좋을까?

스토리보드에서 UI 요소를 화면에 올리고 코드와 연결하는 작업을 할 때 @IBOutlet 속성의 프로퍼티를 만들죠. 여러분들은 이들의 이름을 어떻게 지으시나요? 저는 코드 리뷰를 받으며 UI 요소의 이름을 짓는 것에 대해 다시 한 번 생각해 보았어요.

통합 로깅 시스템으로 로깅 해보자!: OSLog

여느 때처럼 즐겁게 코드를 작성하던 찰나, 진행하던 프로젝트의 PR에 대한 피드백 (코드 리뷰)을 받았습니다.

이미지에서 피드백해주신 코드는 옵셔널 바인딩 결과에 따라 에러를 debugPrint 해주는 부분이었는데요, 과연 링크를 남겨주신 부분이 무엇일까 클릭해보았더니 Zedd님께서 os_log를 주제로 작성하신 글이었습니다. 요지는 Unified logging System (통합 로깅 시스템)에 메시지를 로깅할 수 있는 방식이라는 것이네요. 제 나름대로 내용을 공부해 가볼게요.


공식문서가 설명하는 OSLog

OSLog (프레임워크)
과거의 데이터를 읽기 위한 통합 로깅 시스템.
OSLog 프레임워크는 사용자 (프로그래머)가 로그를 읽을 수 있게 해준다. 통합 로깅 시스템을 사용하면 Instrument 및 Console과 같은 Apple 툴과 함께 사용할 사용자 지정 디버깅 및 분석 툴을 구축할 수 있게 해준다.

오케이, 링크된 Logging을 살펴봅시다.

Logging
통합 로깅 시스템을 사용하여 디버깅 및 성능 분석을 위해 앱에서 원격 측정 (telemetry)을 캡처한다.

아직 통합 로깅 시스템에 대한 이해는 하지 못했지만 제가 기존에 에러가 일어나면 print를 해서 콘솔창으로 확인하던 방식과 달리 로그를 남길 수 있는 방법인가 보네요. 계속 읽다보니 문서에 중요한 내용이 있었습니다.

통합 로깅 시스템은 시스템의 모든 수준에서 원격 측정을 캡처할 수 있는 포괄적이고 성능이 뛰어난 API를 제공합니다. 이 시스템은 데이터를 텍스트 기반 로그 파일에 쓰지 않고 메모리 및 디스크에 로그 데이터를 중앙 집중식으로 저장합니다. 콘솔 앱, 로그 명령줄 도구 또는 Xcode 디버그 콘솔을 사용하여 로그 메시지를 보거나 OSLog 프레임워크를 사용하여 로그 메시지에 프로그래밍 방식으로 액세스할 수도 있습니다.

그렇군요. 성능이 좋고 모든 수준에서 사용할 수 있다. 모든 수준에 대해 궁금하신 분은 이 문서를 읽어보세요. 아래와 같은 표를 찾으실 수 있으실 것입니다.

수준에 대해 간단하게 보자면 아래와 같습니다.

  • Debug: 개발 중 코드 디버깅 시 사용할 수 있는 유용한 정보
  • Info: 문제 해결 (트러블슈팅) 시 활용할 수 있는, 도움이 되지만 필수적이지 않은 정보
  • Notice (기본값): 문제 해결에 필수적인 정보. Failure를 초래할 수 있는 정보
  • Error: 코드 실행 중 나타난 에러. 활동 객체가 존재하는 경우 관련 프로세스 체인에 대한 정보 캡처
  • Fault: 코드 속의 폴트 (Faults) 및 버그 관련 정보. 활동 객체가 존재하는 경우 관련 프로세스 체인에 대한 정보 캡처

제약사항

또 한가지 중요한 점은 통합 로깅 시스템이 iOS 10, macOS 10.12, tvOS 10.0, watchOS 3.0 이후 버전부터 지원한다는 점입니다.

메서드별 에러 발생에 대한 로깅 방식 확인

준비해둔 에러 상황에서 어떻게 작동하는지 확인해보겠습니다. mac에 기본 탑재된 console 앱으로 확인해볼게요.

  1. os_log(_:)
import OSLog

os_log("artWork is nil.")

  1. os_log(_:log:_:)
os_log(.error, log: .default, "artwork is Nil.")
os_log(.fault, log: .default, "artwork is Nil.")

물론 콘솔에서도 아래와 같이 로깅된 내용을 확인할 수 있습니다.

더 강력하게 OSLog를 활용하는 방법

아래와 같이 OSLog 클래스의 인스턴스를 OSLog 프레임워크에 타입 프로퍼티로 만들어 확장해주시면 원하시는 subsystem과 카테고리로 메시지를 로깅하실 수 있습니다.

import OSLog

extension OSLog {
  private static var subsystem = Bundle.main.bundleIdentifier!
  static let ui = OSLog(subsystem: subsystem, category: "UI")
  static let data = OSLog(subsystem: subsystem, category: "Data")
}

// 메시지 로깅이 필요한 위치에서 아래 메서드를 호출
os_log(.fault, log: .data, OSLogMessage.artworkIsNil)

StaticString 타입의 장벽에 부딪힌 당신에게

메시지 타입으로 StaticString을 요구하여 문자열 보간법과 같은 방법을 직접적으로 적용할 수는 없지만 아래와 같은 방법으로 우회적으로 적용할 수 있습니다. 참고 자료

// 아래는 메시지가 `StaticString` 타입이 아니므로 에러가 발생합니다. 
os_log(.info, log: .network, "New used logged in with name \(user.name)") // 에러 발생!

// 메시지를 미리 선언하여 아래와 같이 우회적으로 사용할 수 있습니다.
let message = "New used logged in with name \(user.name)"
os_log(.info, log: .network, "%@", message)

읽어주셔서 감사합니다!

참고자료

profile
합리적인 해법 찾기를 좋아합니다.

0개의 댓글