brunch

You can make anything
by writing

C.S.Lewis

by Tilltue Aug 31. 2017

iOS 어썸블로그 앱 제작기

어썸블로그,iOS,Swift,TDD,ReactorKit, DI,Quick



친구가 개발중이던 '어썸블로그' 서비스의 아이폰 버전 앱을 개발하게 되었다.


이 앱은 오픈소스 프로젝트이고, 소스링크는 아래와 같다.

iOS github


안드로이드 github


https://goo.gl/AaJNpE

아이폰 앱스토어 링크입니다!


https://play.google.com/store/apps/details?id=org.petabytes.awesomeblogs&hl=ko

안드로이드 앱스토어 링크입니다!



# 어떻게 만들까?

오픈소스 작업이기도 하고, 다른 사람들에게나, 나에게 도움이 될 수 있는 코드가 되기 위해서는 어떻게 만들어야할까? 를 고민했었다. 그동안 공부했던 것들을 적용해보면서 후기를 더 풍성하게 만들어보자는 생각으로, 아키텍쳐 바라보기 글에서 알아봤던 ReactorKit을 사용해보고, TDD로 개발해보기로 결정!

아이폰 앱 개발 제안을 받은 그날 repo 생성! 자 이제 시작하자!

부제: 좌충우돌 TDD 초짜의 도전기



# 'TDD' 어떻게 해야하지?

TDD를 해본적이 없었기 때문에 TDD에 대한 공부를 먼저해야했다.

아래는 참고 자료 링크와 각 링크의 주요내용으로 생각되는 부분을 정리했다.


TDD #1 기본 공부

https://academy.realm.io/posts/testing-in-swift/

- Swift 에서 테스트 작성에 대한 글

올바른 테스트의 예
- 테스트는 즉시 실행과 결과를 얻어야 한다.
- 실행할때마다 같은 결과를 얻을수 있어야 한다.
- 결과에 대한 프로그래머의 해석이 필요하면 안된다. 로그는 성공과 실패로 나눌것.
- 테스트는 실제 구현부보다 먼저 작성해야 한다.

작업을 진행하면서 테스트를 작성 할때마다 위의 글을 계속 읽고 또 읽었다


TDD #2 XCTest : arrange, act, assert

https://www.raywenderlich.com/150073/ios-unit-testing-and-ui-testing-tutorial

- ios 유닛테스트, UI테스트 튜토리얼


//예시

class BananaTests: XCTestCase {

  func testPeel() {

    // Arrange: Create the banana we'll be peeling.

    let banana = Banana()

    // Act: Peel the banana.

    banana.peel()

    // Assert: Verify that the banana is now edible.

    XCTAssertTrue(banana.isEdible)

  }

}


* 테스트 이름은 분명하게

testPeel_makesTheBananaEdible


테스트의 대상 : peel (껍질을 벗김)

기대되는 결과 : makes the banana edible (바나나를 먹을수 있다.)


Arrange 단계를 짧게 진행하기 위해 setup 메서드로 초기화를 옮겨주자

setup은 모든 테스트 메서드를 실행하기 전에 수행된다.


참고: 유닛 테스트 setup teardown 라이프 사이클 주의사항.

http://qualitycoding.org/teardown/


#1. 코드를 테스트 하지 말고 동작을 확인하라.

테스트 코드는 동작을 검증해야 한다는 이야기인데, 예를 들어 데이터 베이스에 내 바나나를 저장하는 동작을 테스트한다고 할때, 저장은 이후 로드해서 사용하기 위한 기대를 가지므로, 원하는 값이 잘 저장되어 다시 로드되었는지 이 과정을 테스트 해야 한다.


참고: BDD 관련 위키피디아 설명 링크

https://en.wikipedia.org/wiki/Behavior-driven_development


#2. 테스트에서 실패 로그는 명확하게 보여주자.

아래에서 언급된 Nimble을 사용하면 로그를 직접 입력할 필요가 없다.


http://anthonysciamanna.com/2015/04/18/ping-pong-pair-programming.html

- PING PONG PAIR PROGRAMMING 에 대한 글-

같이 스터디 할수 있는 마음이 맞는 사람이 있다면 해보면 좋을 법하다.

red-green-refactor 방식을 따라, 우선 실패하는 테스트 코드를 작성하고 가장 간단한 방법으로 그것을 컴파일합니다. 그런 다음, 컴파일이 성공하도록 코드를 간단히 수정하고 리팩토링을 수행합니다.

- 개발자 A가 실패한 테스트 작성
- 개발자 B는 테스트가 통과하도록 충분한 구현 코드만 작성하도록합니다.
- 개발자 B는 이제 다음 테스트를 작성합니다.
- 개발자 A는 해당 테스트를 통과하도록 구현 코드만 작성합니다.
- 개발자 A와 개발자 B가 모두 현재 작업중인 장치에 대한 테스트가 더 이상 없음을 동의 할 때까지 계속하십시오.
- 모든 테스트가 통과하는 동안 이 둘의 개발자 만 코드를 리팩토링 할 수 있습니다.


TDD #3 Quick

https://github.com/Quick/Quick/blob/master/Documentation/en-us/MoreResources.md

Testing 을 도와줄 프레임워크를 하나 사용하기로 했고, Quick 을 선택했다.

테스트 코드를 이해하기 쉽게 도와주고, 실패에 대한 메시지를 보기 쉽게 제공한다는 데에 강점이 있다.

쓰지않아도 무방하다. 편하게 해주는것 뿐이며, 이것도 사용법을 익혀야 한다.


- Quick framework README

https://github.com/Quick/Quick/blob/master/Documentation/en-us/README.md


#실패 로그를 명확하게 보여주자.

Nimble 은 여러가지 종류의 assertion 을 제공한다. XCTAssert 를 사용할때와 다르게 에러메시지를 직접 입력할 필요가 없다.

Nimble 사용 document 는 아래의 링크.

https://github.com/Quick/Nimble


* 참고
Podfile 에서 test target 일때
quick , nimble 을 추가하도록 한다.
소스 링크: https://github.com/tilltue/awesome-blogs-ios/blob/master/Podfile


pod install 하면 ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES 관련 경고가 발생하는데
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES build 옵션에서 이 항목을 제거하면 된다.
관련 링크 : https://github.com/CocoaPods/CocoaPods/issues/5981


TDD #4 RxTest

rxswift 를 사용한다면 rxtest 도 도움이 된다.


rxtest 사용 예

https://github.com/ReactiveX/RxSwift/blob/master/Documentation/UnitTests.md

http://adamborek.com/rxtests-rxactionsheet/




# 개발 시작!

(이제 정말로.. 시작하는 거야)


순서는 다음과 같이 하기로 했다.

1. 실패한 테스트 작성

2. 구현

3. 테스트 통과

4. 리펙토링

5. 필요에 따라 1~4 반복


개발 진행 #1 피드 Reactor 테스트 작성 고민 

reactor 의 mutation 에서 load feed 동작이 network api 동작이다 보니. mock 데이터를 사용해야할 필요성을 느꼇다. 테스트의 목적은 network api test 가 아닌 reactor 의 검증이기 때문이다.

Moya provider 에서 immidiately stub 를 지원해서 stub 데이터를 사용 할수 있지만, reactor 의 mutation 에서 테스트 생성자를 사용하도록 하는 코드를 넣을수는 없는 일.

어떻게 할까 고심하던차에 dribble 예제를 보고 Swinject 을 알아보기로 했다.


개발 진행 #2 Swinject

많은 개발자들이 DI를 잘 알고 사용에 익숙하다던데, 나는 이번에 처음 알았다. (부끄럽다)

Swinject는 Swift 에서 DI를 도와주는 프레임워크이다.

https://github.com/Swinject/Swinject


어썸블로그 코드에서 활용된 부분은 아래링크에서 볼수 있다.

https://github.com/tilltue/awesome-blogs-ios/blob/master/AwesomeBlogs/Source/Base/Service.swift

    func mockRegister() {
        container.register(RxMoyaProvider<AwesomeBlogsRemoteSource>.self){ _ in RxMoyaProvider<AwesomeBlogsRemoteSource>(stubClosure: MoyaProvider.immediatelyStub) }
    }
    func register() {
        container.register(RxMoyaProvider<AwesomeBlogsRemoteSource>.self){ _ in RxMoyaProvider<AwesomeBlogsRemoteSource>() }
    }

요런식으로 register 를 각각 용도에 맞게 하고

container.resolve(RxMoyaProvider<AwesomeBlogsRemoteSource>.self)

요렇게 가져와서 쓰면된다.

register 는 덮어씌워지므로 테스트 할때 정말 간단하다!


참고 : DI관련 글 모음

https://justhackem.wordpress.com/2016/05/13/dependency-inversion-terms/

강의 (RxSwift + Swinject + MVP)

https://www.lynda.com/iOS-tutorials/iOS-App-Development-Design-Patterns-Mobile-Architecture/585241-2.html?srchtrk=index%3a2%0alinktypeid%3a2%0aq%3adesign+Pattern+iOS%0apage%3a1%0as%3arelevance%0asa%3atrue%0aproducttypeid%3a2



개발 진행 #3 Swinject 을 통한 의존성 주입

dribble 오픈소스는 서비스 레이어를 두고, 이 부분을 protocol 로 추상화 해둔뒤, mock 과 실제 api 를 DI를 통해 테스트 모델을 만드는 mock service 레이어를 만들어 두었다.


나는 조금 다르게 만들었다.

Service : 싱글톤 객체로, Container ( Swinject 의존성 주입을 위한 )를 가지며 필요한 의존성을 주입한다.

(Container 인스턴스 를 공요해야하므로 싱글턴 객체로 생성했다.)

Api.swift 코드 내에서 서비스 레이어에 주입된 의존성을 바탕으로 타겟타입(API)별 RxMoyaProvider 를 생성한다. 이렇게 하면 mock 데이터를 MoyaProvider 내의 SampleData에 구현하면 되며,

테스트 시에는 register 를 통해 mock 데이터에 의존성을 가지도록 할 수 있다.


swinject가 아닌 다른 형태의 DI 를 사용한다면, 꼭 이렇게 사용하지 않아도 된다.

여러가지 고민을 해봤지만 이 편이 이해하기 쉬운 코드일 것 같아 이렇게 구현하게 되었다.

의존성 주입에 대한 개념이 부족한 상태에서 (어려운 것은 아니지만 어떻게 활용해야 하는지 감이 잘 오지 않았다.) 고민을 많이 했다. 

서비스 레이어의 구성을 어떻게 해야 할지, 의존성 주입은 어떻게 해야 할 것인지 등등.


개발 진행 #4 피드 Reactor 테스트 코드 작성


실패한테스트의 작성

commit:

https://github.com/tilltue/awesome-blogs-ios/commit/376a42e6847538df7280d0173e047d974d852a1b 

성공 테스트 작성 및 리펙토링

https://github.com/tilltue/awesome-blogs-ios/commit/e5b791c1af447f92ada75cb3e1dfd9798826c0b6

FeedStateEvent 테스트용 이벤트 객체를 만들어서 불필요한 코드를 줄였다.


Quick 에서 equal 로 rxtest 에서 제공하는 event 스트림 비교 구문을 사용하지 못하기 때문에 RxTest 쪽에 있는 이벤트 비교구문을 가져와서 quick equal로 확장해야 했다. 이걸 하는데 걸린 시간이... :(

RxTest 팟을 추가했고, Quick 의 equal 확장 구문 문서를 봐야 했으며, RxTest 에서 이벤트 비교방법에 대해 문서를 봐야 했다.

RxTest 문서는 없기때문에 ㅜ.ㅜ 찾는데 에로 사항이... 굉장히 쉬운 샘플인데 :(

아래는 찾은 링크이다. 여러분의 시간은 소중하니까요. XD


http://adamborek.com/rxtests-rxactionsheet/

https://github.com/ReactiveX/RxSwift/blob/master/RxTest.podspec


개발진행 #5 사이드 메뉴 Reactor 테스트 작성

필요한 기능

- 메뉴 show / hide

- 선택된 메뉴 밑줄

- 선택한 메뉴의 블로그 피드로 교체.

기능 정의: 메뉴 show / hide 는 action 에 의해 동작한다. show / hide 상태는 저장되지 않는다.

         메뉴 선택에 대한 액션은 밑줄을 보여주며, 블로그 피드를 교체한다.


action : Menu show/hide

- 성공: 메뉴 show / hide 가 반복적으로 된다.

- 실패: 메뉴 show 가 되지 않는다.


action : menu selected

- 성공: 선택한 메뉴에 index가 저장되며 피드가 교체된다. (메뉴는 hide 되어야 한다.)

- 실패: 선택한 메뉴의 index가 반영되지 않고 피드가 교체 되지 않는다.


실패한 테스트 작성 -> 구현 -> 리펙토링.

https://github.com/tilltue/awesome-blogs-ios/blob/master/AwesomeBlogsTests/MainSideMenuReactorSpec.swift


개발진행 #6 cache 테스트 작성

필요한 기능

- 네트워크로 받아온 데이터를 캐싱하는 기능.

- 캐시가 있을때 : 캐시를 읽어서 먼저 이벤트를 전달하고 새로운 데이터를 받아와서 캐싱 한뒤 그 이벤트도 전달하는 기능.

안드로이드 어썸블로그 소스 캐시 구조와 동일하게 구성하도록 함.


- 실패한 테스트 작성

- 구현부 작성.

- 테스트 성공.

- 리펙토링 및 UI 갱신을 위한 처리.

- 테스트 리펙토링.

https://github.com/tilltue/awesome-blogs-ios/blob/master/AwesomeBlogsTests/MainSideMenuReactorSpec.swift


개발진행 #7 의도치 않은 구현 :(

실패한 테스트 작성 -> 구현 -> 테스트 성공 -> 리펙토링 이 흐름을 꼭 지키고 싶었는데,

이 흐름에 익숙치 않아, 초기 테스트 작성과 빈 데이터 모델을 작성하면서 구현을 해버리는 경우가 종종 발생했다.


이렇게 테스트 코드를 작성했고, 다른 개발후기는 생략했다. ( 사실 쓸말이 없... )



# 후기

TDD 를 하면서 (그동안의 개발과) 가장크게 다르다고 느꼈던 부분은

- 실패한 테스트를 작성하면서 어떤 기능이 필요하고 모델이 어떤 값들을 가져야 하는지 더 고민하고 만들게 된다. ( 요구사항 분석을 좀더 철저히 하고, 모델을 만들때 불필요한 값들이 없이 시작하게 되는 느낌?)

- 구현부를 작성하면서 내가 테스트를 작성할때 고려하지 못했던 부분들을 발견했을때 다시 앞쪽의 흐름으로 돌아가 기능(혹은 모델 작성)에 대한 고민을 하게 만들었다.

- 리펙토링 과정은 즐거웠다. 정확한 이유는 설명하기 어렵지만... :)

- 테스트가 성공한 다음에 작업진행하다가 코드를 수정하고, 다시한번 테스트를 돌렸을때 테스트가 실패하면 내가 무언가 잘못한 것임을 알게 된다. 다시 코드를 리펙토링하고 테스트를 통과하게 만들었을때, 왠지모를 안도감을 느꼈다.


Test 코드 작성은 힘들었다. 정말로. (하지만 점점 진행하면서 좀더 수월해지는 것도 느꼇다.)

개인 프로젝트라고 해도, TDD를 통해 얻으려고 하는 가치에 대해 집중하면 좋은 효과를 볼수 있을 것 같다.

나의 경우 몇가지 느낀점을 적어보자면,

DI를 더 많이 공부하고 싶다는 생각이 들었다.

- 더 많이 활용해보고 싶다. 활용하는 예제들을 많이 보고싶다.

- 테스트 코드 작성이 엄청나게 쉬워졌다.

- 서비스 내에서도 활용방안이 많아 보인다.

- 관련내용들은 너무 어렵고 용어도 이해하기 힘들다... 열심히 공부하는 수밖에...


ReactorKit 사용후기

테스트에 용이했다.

다음 개인 프로젝트에서는  kickstarter의 MVVM 구조를 분석해서 따라해보고 싶다.  

Reswift 는 그 다음에 손이 간다면... :)


#마치며... 

이 글을 통해 TDD를 막 시작하시는 분들에게 도움이 되셨으면 좋겠고, TDD 고수들이 댓글로 많이 조언해주셨으면 좋겠다. :) 현업에서도 활용해보고 싶다. 

맘맞는 개발자가 2명정도 만되도 ping pong pair programing 을  하면 재밌을것 같다! 



#부록: 어썸블로그 아이폰 앱개발 인포그래픽



안드로이드 앱 개발기


오픈소스 프로젝트이고, 안드로이드 앱 링크와 github 주소는 아래.


서버 개발기

http://benjaminlog.com/m/304


브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari