Naver Tech Concert Day-2

03. 내가 사랑했던 개발자들 : 더 나은 협업을 위한 디자이너, 기획자 이해하기

  • 발표자 : 유두선 (NAVER / 파파고 UX)

  • 동영상 : https://tv.naver.com/v/4635562/list/272653

  • 슬라이드 : https://www.slideshare.net/NaverEngineering/23-121486418

  • 세션설명 : 디자이너와 기획자는 어떤 일과 생각들을 하는지 소개합니다.

  • 목차 :

    1. 뭐하는 사람들인가
      - UI / 인터렉션 / BX 디자이너의 일
      - UX / 기획자의 일
    
    2. 그들의 생각
      - 디자이너 : 왜 1px을 옮겨달라고 할까
      - 기획자 : UX적으로(??) 해결해달라구요?!
    
    3. 어떻게 일하면 좋을까
      - 상세 설계 공유방식에 대한 고민
      - 커뮤니케이션
    
    4. 혹시 디자인에 관심이 있으시다면
    
    5. 내가 사랑했던 개발자들
    

1. 뭐하는 사람들인가

(* 주관적인 견해임)

개발자 : FE, BE, Markup, JAVA, Swift, React, ...

디자이너 : UI, UX, BX, 영상, 인터렉션, Graphic, 원화, 배경, 3D, ...

UI(User Interface) 디자이너

사용자 인터페이스를 디자인하는 사람

  • 툴 : 스케치, 제플린, 포토샵
  • 결과물 : UI 디자인 및 가이드

인터렉션(Interaction) 디자이너

화면 간 트랜지션 또는 서비스 UI에 필요한 애니메이션/인터렉션 디자인하는 사람

  • 툴 : 에펙, 프리미어, 프레이머, 포토샵, 스케치
  • 결과물 : 인터렉션 가이드, 애니메이션 json

BX 디자이너

브랜드 아이덴티티 및 전략 디자인.

브랜드 커뮤니케이션(주로 마케팅) 관련된 디자인하는 사람.

  • 툴 : 일러스트, 포토샵, 인디자인
  • 결과물 : BI, 브랜드 커뮤니케이션 관련 온오프라인 디자인

Graphic 디자이너

서비스 아이콘이나 일러스트와 같은 그래픽 요소를 그리는 사람

  • 툴 : 포토샵, 일러스트
  • 결과물 : 아이콘, 일러스트 등 다양한 그래픽 요소

UX 디자이너 / 기획자

리서치 : FGI, UT, 페르소나, Contextual inquiry 등등

설계 : IA, 상세 설계서

운영 : 고객문의대응, 서비스 운영, 제휴...

2. 그들의 생각

디자이너들이 1px을 옮겨달라고 하는 이유

  • 사용성(가독성)
    • 각도, 행 간격 : 시선의 흐름 디자인하여 가독성/사용성에 영향을 줌
  • 완성도 : 작은 디테일이 쌓여 완성됨

UX적으로 UX에서 해결해주세요?

UX는 문제를 해결해주는 간달프의 마법이 아님.

  • What : 문제 정의 (우리가 풀어야 하는 문제는 무엇인가)
  • How : 해결방법 찾기 (그 문제를 어떻게 풀 것인가)
    • UX 디자이너가 결국 해결책을 가지고 제시해야 하나 기술적인 부분 등에 대해서는 혼자 풀 문제가 아님.

3. 어떻게 일하면 좋을까?

상세 설계 공유방식에 대한 고민

PPT

처음 만들때는 어떻게든 만들지만...

  • 수정하는게 너무 큰 일
  • 의견 공유/취합 어려움
  • 급할때 1쪽 설계 난무 (문서 파편화)

스케치

그래서 스케치로 도입해서 사용해 봄

  • 장점
    • 심볼로 해서 이미지 수정은 좀 쉬워짐
    • 제플린으로 코멘트도 달 수 있음.
  • 단점
    • 모두가 스케치가 익숙하지는 않음
    • 텍스트 검색 불편

GitHub

그래서 스케치는 포기하고 GitHub을 사용함.

issue 간의 링크가 편해서 각각의 객체화가 가능함.

업데이트 방식은 스펙 v1.1 -> 의견 -> 스펙v1.2 식에서 스크롤이 길어져 merge 형식으로 바꿈

  • 장점
    • 문서 파편화 해결
    • 검색 용이해짐
    • 히스토리 파악 용이
    • 수정 편함
    • 누구든 쉽게 사용
  • 단점
    • 마크다운 가독성

3. 어떻게 일하면 좋을까

커뮤니케이션

기획자에게 안되는 이유에 대해서 개발자가 설명해주면 다음에 참고하게 됨.

기획자의 일이 시각적인 부분이 많아 의견을 자유롭게 내기 쉬움.

의견 제시는 좋지만 해결 방법에 대해서는 직접 제시보다는 맡기기.

좋은 관계가 제일임.

4. 혹시 디자인에 관심이 있으시다면..

눈이 손보다 중요함.

디자인은 만족하면 끝남.

참고할 사이트들

볼 때 생각할 점

  • 왜 좋은지 생각해보기
  • 왜 이렇게 디자인했는지 생각해보기
  • 따라 만들기

5. 내가 사랑했던 개발자들

개발자들과 친해지면서 개발과 관련된 부분들도 일부 볼 수 있게 됨.

잘 지냈으면 함.

Q & A

Q : 디자이너들이 아이폰식 디자인을 안드로이드에 적용하려는 이유는?

A : 디자이너들이 주로 아이폰을 많이 쓰다보니 익숙한 부분을 따라감. 머티리얼 디자인 등을 공부하지만 더 노력해야 할 부분임.

Q : 디자이너에게 Github 가 쉽지 않을텐데 도입 방법은?

A : 처음에 코드 메뉴 등은 어려워 이슈만 사용함. 거기에서 커뮤케이션 툴처럼 사용하면서 시작함.

Q : 타켓 유저가 원하는 디자인 취향은?

A : 리서치에 의해 타켓 유저의 취향이 정해졌다면 그대로 가야 함. 특정 결정권자의 의견보다 디자이너의 의견이 맞다고 봄.

Q : UX 전문가의 경우 어플리케이션의 사용성 및 요소 배치에 특히 집중하는 케이스를 본 적이 있는데 이런 부분을 개선하는 것이 UX 전문가의 주된 롤인지?

A : 당연히 중요한 문제이고 기획자도 중요하게 생각하는 롤임.

Q : 기획/디자이너/개발이 나뉘어져 있는 조직에서 서로 융화되기 위해 노력할 만한 것들은?

A : 같이 일하는 사람들과 밥과 커피로 친해졌음.

Q : 같이 일하기 싫은 개발자는?

A : 그냥 나쁜 사람. (ex. 화내는 사람들?)

Q : UX 리서치를 사업적인 지표와 직접적으로 연결시켜 해석할 수 있는 부분이 있을까?

A : 사용자의 만족도나 사용성 평가를 통해 도달시간/체류시간 등을 통해 지표로 연결할 수 있음.

Q : 현재 스토어에 있는 앱 중에 디자인적으로 좋은 앱 하나만 추천해준다면?

A : 파파고 (웃음). 상도 받았음.

Q : 수동적인 사람들을 설득할 방법은?

A : 인간적으로 친해지기 전까지 높게 벽을 치는 사람도 있기 때문에 일단 친해지는 게 우선될 수도 있지만, 결국 안되는 경우도 있었음.

Q : 디자이너가 PPT나 메일로 보고 공유해주는 형태에서 깃헙을 사용하면 좀더 효율적일지?

A : 스케치나 제플린을 사용하면 됨. 깃헙으로 커뮤니케이션을 하면 편했음.

Q : 이상적인 팀 구성에서 UX 디자이너의 업무 및 권한 수준은?

A : 팀마다 상황이 다르지만 UX가 포괄하는 의미가 너무 많음. 상황에 맞춰서 하면 됨.

Q : UX 디자이너들이 일에 관여하는 사람(데이터, PM, 엔지니어)이 많아 힘들어할 경우 최종 협의를 거쳐서 산출물을 내는데까지 비용이 너무 큰데 노하우가 있는지?

A : 의사결정 단계가 너무 많으면 힘들긴 하며, 개인이 조정하긴 어렵고 조직의 변화가 필요할 듯.

Q : 벤치마킹, 리서치 하다보면 창작이 아닌 카피의 경계가 무너지기도 할텐데 그럴때 윤리의식은 어떻게 관리하는지?

A : 사람마다 생각하는 정도가 있기에 디자이너 스스로 본인이 지켜나가야 함.

'IT > 행사' 카테고리의 다른 글

[행사] Naver Tech Concert Day-2 05  (0) 2019.01.14
[행사] Naver Tech Concert Day-2 04  (0) 2019.01.14
[행사] Naver Tech Concert Day-2 02  (0) 2019.01.14
[행사] Naver Tech Concert Day-2 01  (0) 2019.01.14
[행사] Microsoft Azure Everywhere  (0) 2019.01.11

Naver Tech Concert Day-2

02. Efficient and Testable MVVM pattern

  • 발표자 : 김범준 (레이니스트 / 안드로이드 개발)

  • 동영상 : https://tv.naver.com/v/4635548/list/272653

  • 슬라이드 : https://www.slideshare.net/NaverEngineering/22efficient-and-testable-mvvm-pattern

  • 세션설명 : Koin으로 DI를 하고 AAC, Rx를 조합한 MVVM 패턴에 대하여 이야기 하고자 합니다. 어째서 효율적인지 Testable한지를 함께 고민 해 보고 더 나은 구조를 향한 이야기를 나누어 보았으면 합니다.

  • 목차 :

    1. 발표 동기
    
    2. AAC를 소개(나온 배경등)
    
    3. MVVM을 소개(livedata,ViewModel과 엮으며)
    
    4. Rx를 소개 (KickStarter를 참고하여)
    
    5. Koin을 소개(DI를 간단하게 설명하며)
    
    6. 위의 모든 것을 조합 한 예제 설명
    
    7. 효율적인 부분 고민이 되는 지점등 같이 이야기해볼만 한 부분들을 화두로 던져 소통
    
    8. 테스트코드 또한 위와같이 진행
    
    9. 마무리.
    

Android 코드 아키텍쳐

  • MVC
  • MVP
  • MVVM
    • with Clean Architecture
    • with RFP(rxJava2)
    • with AAC, Koin
    • with Spek
  • MVI
  • VIPER
  • etc...

MVVM

with CleanArchitecture

with RFP (rxJava2)

with AAC, Koin

with Spek

with에 적힌 라이브러리, 프레임워크는 간단히 설명하기로 함.

why MVVM?

MVP로 충분히 잘 구현해도 서비스 운영을 하면서 아래의 문제들이 생김.

요구사항, 비즈니스 로직 늘어남 -> 코드량, 복잡성 높아짐 -> 유지보수성, 테스트 용이성 하락

what MVVM?

(MVVM에 대한 개인적인 견해이므로 다른 의견이 있을 수도 있음.)

  • ModelViewViewModel로 구성되어 있는 패턴
  • ViewModel은 View의 추상화
  • View와 ViewModel은 n:m 의 관계
  • View는 ViewModel에 bindable함

ViewModel은 View의 추상화

  • ViewModel은 View의 상태와 행동이 추상화 된 것
  • ViewModel은 View의 input과 output이 명시되어 있는 인터페이스 (같은 input 엔 항상 같은 output 이라 테스트에 용이함)
  • output은 View의 상태와 Rooute로 나뉨

View와 ViewModel은 n:m 의 관계

  • 하나의 View가 여러 ViewModel에 조합 가능
  • 하나의 ViewModel이 여러 View에 적용 가능
  • 재 사용성이 용이함.

View는 ViewModel에 bindable함

  • 사용자 행동에 의해 입력 받았을 때 (View -> ViewModel)
  • 사용자 행동에 따른 View의 상태를 변경시켜야 할 때 (ViewModel -> View)
  • 모든 로직은 binding되는 시점에 결정됨.

장점

View에 대한 의존성이 제거되어 효과적으로 역할과 책임을 나눌 수 있음.

ViewModel 단독으로 테스트가 가능함으로 테스트 용이성 증가.

binding 되는 시점에 input 대비 output을 산출하는 로직이 정해지기 때문에 개발자가 상태 관리해야 하는 위험 줄여줌.

Databinding을 통해 보일러 플레이트 코드 줄일 수 있음.

How MVVM?

발표자가 생각한 android에서 MVVM

LiveData는 라이프싸이클 처리 등의 문제로 사용.

ViewModel 윗단 Model단부터는 Clean Architecher가 도입함.

Rx로 비동기 처리함.

Activity/Fragment/something은 View의 상태 변화를 제외한 Router역할+ViewModel과 View를 binding하는 역할 수행함.

추가적으로

  • Clean Architecture 지향
  • Koin을 사용하여 IOC(Inversion Of Control) 구현
  • Spek을 사용하여 행동 주도 결과 테스트 작성

예제 코드 : https://github.com/omjoonkim/GitHubBrowserApp

  • Github 이름을 입력하여 repository 리스트를 보여주는 테스트 앱

package 구조

Clean Architecture 스럽게

  • app
  • data
  • domain
  • remote

원래 UI와 Presentation 레이어가 나뉘어서 의존성을 주입해야 하지만 AAC의 ViewModel을 사용함으로 Android Framework에 의존성이 생길 수 밖에 없어서 App은 두 레이어에 걸치게 됨.

의존성을 바깥에서 주입한다는 것은 (의존성의 역전)

  • Domain은 Data가 어떤 코드인지 모름, Data는 Remote가 어떤 코드인지 모름
  • Domain은 Presentation이 어떤 코드인지 모름, Presentation은 UI가 어떤 코드인지 모름

Koin

Koin?

  • 제어의 역전을 구현할 수 있게 도와주는 Library. (DI가 아님)

  • Kotlin으로 구현되어 있음.

  • 간편한 사용 방법. (AAC도 지원)

  • 제어의 역전을 Service Locator 방식으로 구현함. (Runtime에서 에러 확인 가능)

    (Dagger같은 경우는 컴파일 시점에 에러 확인 가능)

app_module.kt

val myModule: Module = module {
    viewModel { (id: String) -> MainViewModel(id, get(), get()) }
    viewModel { SearchViewModel(get()) }

    //app
    single { Logger() }
    single { AppSchedulerProvider() as SchedulersProvider }

    //domain
    single { GetUserData(get(), get()) }

    //data
    single { GithubBrowserDataSource(get()) as GitHubBrowserRepository }

    //remote
    single { GithubBrowserRemoteImpl(get(), get(), get()) as GithubBrowserRemote }
    single { RepoEntityMapper() }
    single { UserEntityMapper() }
    single {
        GithubBrowserServiceFactory.makeGithubBrowserService(
            BuildConfig.DEBUG,
            "https://api.github.com"
        )
    }
}

App.kt

class App : Application() {
    override fun onCreate() {
        super.onCreate()
        startKoin(
            this,
            listOf(myModule)
        )
    }
}

검색화면의 경우

  • SearchView의 Input : name, clickSearchButton
  • SearchView의 Output (STATE) : enableSearchButton
  • SearchView의 Output (ROUTER) : goResultActivity

SearchView

    <data>
        <import type="android.view.View"/>
        <variable
            name="viewModel"
            type="com.omjoonkim.app.githubBrowserApp.viewmodel.SearchViewModel" />
    </data>
...
        <EditText
            android:id="@+id/editText"
            android:layout_width="match_parent"
            android:layout_height="48dp"
            android:onTextChanged='@{(s,start,end,before) -> viewModel.input.name(s.toString ?? "")}'
            />
        <Button
            android:id="@+id/button_search"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:enabled="@{viewModel.output.state().enableSearchButton}"
            android:onClick="@{(v) -> viewModel.input.clickSearchButton()}"
            android:text="search"
            />

SearchViewModel

class SearchViewModel(logger: Logger) : BaseViewModel() {    
    private val name = PublishSubject.create<String>()
    private val clickSearchButton = PublishSubject.create<Parameter>()
    val input = object : SearchViewModelInPuts {
        override fun name(name: String) =
            this@SearchViewModel.name.onNext(name)
        override fun clickSearchButton() =
            this@SearchViewModel.clickSearchButton.onNext(Parameter.CLICK)
    }
    private val state = MutableLiveData<SearchViewState>()
    private val goResultActivity = MutableLiveData<String>()
    val output = object : SearchViewModelOutPuts {
        override fun state() = state
        override fun goResultActivity() = goResultActivity
    }

    init {
        compositeDisposable.addAll(
            name.map { SearchViewState(it.isNotEmpty()) }
                .subscribe(state::setValue, logger::d),
            name.takeWhen(clickSearchButton) { _, t2 -> t2 }
                .subscribe(goResultActivity::setValue, logger::d)
        )
    }    
}

interface SearchViewModelInPuts : Input {
    fun name(name: String)
    fun clickSearchButton()
}
interface SearchViewModelOutPuts : Output {
    fun state(): LiveData<SearchViewState>
    fun goResultActivity(): LiveData<String>
}
data class SearchViewState(
    val enableSearchButton: Boolean
)
import androidx.lifecycle.ViewModel
...
abstract class BaseViewModel : ViewModel(){
    protected val compositeDisposable : CompositeDisposable = CompositeDisposable()
    override fun onCleared() {
        super.onCleared()
        compositeDisposable.clear()
    }
}

SearchActivity

class SearchActivity : BaseActivity() {
    private val keyboardController by lazy { getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = DataBindingUtil.setContentView<ActivitySearchBinding>(this, R.layout.activity_search)
        binding.setLifecycleOwner(this)
        actionbarInit(binding.toolbar, isEnableNavi = false)

        val viewModel = getViewModel<SearchViewModel>()
        binding.viewModel = viewModel

        viewModel.output.goResultActivity()
            .observe {                keyboardController.hideSoftInputFromWindow(binding.editText.windowToken, 0)
                startActivity(
                    Intent(
                        Intent.ACTION_VIEW,
                        Uri.parse("githubbrowser://repos/$it")
                    )
                )
            }
    }
}

결과화면의 경우

  • ResultView의 Input : clickHomeButton, clickUser
  • ResultView의 Output(STATE) : title, showLoading
  • ResultView의 Output(ROUTER) : refreshListData, finish, showErrorToast, goProfileActivity

MainViewModel

class MainViewModel(
    searchedUserName: String,
    private val getUserData: GetUserData,
    logger: Logger
) : BaseViewModel() {
    private val clickUser = PublishSubject.create<User>()
    private val clickHomeButton = PublishSubject.create<Parameter>()
    val input: MainViewModelInputs = object : MainViewModelInputs {
        override fun clickUser(user: User) = clickUser.onNext(user)
        override fun clickHomeButton() = clickHomeButton.onNext(Parameter.CLICK)
    }

    private val state = MutableLiveData<MainViewState>()
    private val refreshListData = MutableLiveData<Pair<User, List<Repo>>>()
    private val showErrorToast = MutableLiveData<String>()
    private val goProfileActivity = MutableLiveData<String>()
    private val finish = MutableLiveData<Unit>()
    val output = object : MainViewModelOutPuts {
        override fun state() = state
        override fun refreshListData() = refreshListData
        override fun showErrorToast() = showErrorToast
        override fun goProfileActivity() = goProfileActivity
        override fun finish() = finish
    }

    init {
        val error = PublishSubject.create<Throwable>()
        val userName = Observable.just(searchedUserName).share()
        val requestListData = userName.flatMapMaybe {
            getUserData.get(it).neverError(error)
        }.share()
        compositeDisposable.addAll(
            Observables
                .combineLatest(
                    Observable.merge(
                        requestListData.map { false },
                        error.map { false }
                    ).startWith(true),
                    userName,
                    ::MainViewState
                ).subscribe(state::setValue, logger::d),
            requestListData.subscribe(refreshListData::setValue, logger::d),
            error.map {
                if (it is Error)
                    it.errorText
                else UnExpected.errorText
            }.subscribe(showErrorToast::setValue, logger::d),
            clickUser.map { it.name }.subscribe(goProfileActivity::setValue, logger::d),
            clickHomeButton.subscribe(finish::call, logger::d)
        )
    }
}

interface MainViewModelInputs : Input {
    fun clickUser(user: User)
    fun clickHomeButton()
}
interface MainViewModelOutPuts : Output {
    fun state(): LiveData<MainViewState>
    fun refreshListData(): LiveData<Pair<User, List<Repo>>>
    fun showErrorToast(): LiveData<String>
    fun goProfileActivity(): LiveData<String>
    fun finish(): LiveData<Unit>
}
data class MainViewState(
    val showLoading: Boolean,
    val title: String
)

MainView

    <data>
        <import type="android.view.View"/>
        <variable
            name="viewModel"
            type="com.omjoonkim.app.githubBrowserApp.viewmodel.MainViewModel" />
    </data>
...
            <androidx.appcompat.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:title="@{viewModel.output.state().title}" />
...
        <FrameLayout
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:visibility="@{viewModel.output.state().showLoading ? View.VISIBLE : View.GONE}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/appBar" >
            <ProgressBar
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center" />
        </FrameLayout>

MainActivity

...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
        binding.setLifecycleOwner(this)

        val viewModel = getViewModel<MainViewModel> {
            parametersOf(intent.data.path.substring(1))
        }
        binding.viewModel = viewModel

        actionbarInit(binding.toolbar, onClickHomeButton = {
            viewModel.input.clickHomeButton()
        })

        with(viewModel.output) {
            refreshListData().observe { (user, repos) ->
                binding.recyclerView.adapter = MainListAdapter(
                    user,
                    repos,
                    viewModel.input::clickUser
                )
            }
            showErrorToast().observe { showToast(it) }
            goProfileActivity().observe {
                startActivity(
                    Intent(
                        Intent.ACTION_VIEW,
                        Uri.parse("githubbrowser://repos/$it")
                    )
                )
            }
            finish().observe {
                onBackPressed()
            }
        }
    }
...

Test

사전준비

  • SchedulersProvider 생성

    TestSchedulersProvier

    class TestSchedulerProvider : SchedulersProvider {
        override fun io() = Schedulers.trampoline()
    
        override fun ui() = Schedulers.trampoline()
    }
  • DummyApiService 생성

    TestDummyGithubBrowserService

    class TestDummyGithubBrowserService : GithubBrowserService {
        override fun getUserInfo(userName: String): Single<UserModel> =
            Single.just(
                UserModel("omjoonkim", "")
            )
        override fun getUserRepos(userName: String): Single<List<RepoModel>> =
            Single.just(
                listOf(
                    RepoModel("repo1", "repo1 description", "1"),
                    RepoModel("repo2", "repo2 description", "2"),
                    RepoModel("repo3", "repo3 description", "3")
                )
            )
    }
  • Spek + LiveData를 같이 테스트하기 위한 코드 작성

    (JUnit이 불가해 동작을 위한 코드 필요함)

        beforeEachTest {
            ArchTaskExecutor.getInstance().setDelegate(object : TaskExecutor() {
                override fun executeOnDiskIO(runnable: Runnable) {
                    runnable.run()
                }
                override fun isMainThread(): Boolean {
                    return true
                }
                override fun postToMainThread(runnable: Runnable) {
                    runnable.run()
                }
            })
        }
        afterEachTest {
            ArchTaskExecutor.getInstance().setDelegate(null)
        }

    MainViewModelSpec

    object MainViewModelSpec : KoinSpek({
        beforeEachTest { ... }
        afterEachTest { ... }
    
        lateinit var userName: String
        val viewModel: MainViewModel by inject { parametersOf(userName) }
        val getUserData: GetUserData by inject()
    
        Feature("MainViewModel spec") {
            Scenario("유저가 화면에 들어오면 검색한 유저의 프로필,저장소 데이터가 정상적으로 보여야 한다") {
                Given("검색하려는 유저의 이름은 omjoonkim이다"){
                    userName = "omjoonkim"
                }
                Then("화면에 검색한 유저의 데이터가 정상적으로 나타난다") {
                    assertEquals(
                        getUserData.get(userName).blockingGet(),
                        viewModel.output.refreshListData().value
                    )
                }
            }
            Scenario("유저 프로필을 클릭하면 유저의 프로필 화면으로 이동되어야 한다") {
                When("프로필을 클릭 했을 때") {
                    viewModel.input.clickUser(
                        viewModel.output.refreshListData().value?.first
                            ?: throw IllegalStateException()
                    )
                }
                Then("해당 유저의 프로필 화면으로 이동 된다") {
                    assertEquals(
                        viewModel.output.refreshListData().value?.first?.name
                            ?: throw IllegalStateException(),
                        viewModel.output.goProfileActivity().value
                    )
                }
            }
            Scenario("홈버튼을 클릭하면 화면이 정상적으로 종료되어야 한다.") {
                When("홈버튼을 클릭 했을 때") {
                    viewModel.input.clickHomeButton()
                }
                Then("화면이 정상적으로 종료 된다.") {
                    assertEquals(
                        Unit,
                        viewModel.output.finish().value
                    )
                }
            }
        }
    })

  • DI for Test

    test_modules

    val testModule = module {
        single(override = true) {
            TestSchedulerProvider() as SchedulersProvider
        }
        single(override = true) {
            TestDummyGithubBrowserService() as GithubBrowserService
        }
    }
    
    val test_module = listOf(myModule, testModule)

Spek을 이용한 테스트 코드 작성

  • Feature
  • Scenario
  • Given
  • When
  • Then

More... + TMI

Dagger2 vs Koin

  • Heavy vs light
  • Dependency Injection vs ServiceLocator
  • CompileTime vs RunTime

Spek과 Koin의 호환성

  • Spek + Koin을 사용하려면 추가적으로 작업해야 하는 코드들 있음.

    (Spek 구동방식과 Koin을 사용하는 방식이 서로 충돌되기 때문)

개선의 여지 + 아쉬운 점

  • Databinding이 kotlin에 100% 호환되지 않음 (람다 함수를 xml에서 값으로 지정해줄 수 없음)
  • Router
  • Presentation module 분리

Q & A

Q : MVVM 적용시 피곤한 점은?

A : 기존 뱅크샐러드의 경우 MVP 베이스임. 그래서 MVP에 Koin 추가나 코드 수정해야 했음.

​ 팀원 협의에 대한 부분. Rx나 MVVM 러닝커브가 높은 점.

Q : 클린 아키텍쳐면 presentation, domain, entity, data 4가지 레이어 정의를 보면 presentation이 ui에 해당되는데 왜 presentation에 data와 같은 레벨로 하고 ui는 remote로 한건지?

A : ui가 remote라기 보다는 ui와 remote는 서버에서 데이터를 받아오는가장 바깥 레이어로 생각하면 됨.

Q : Koin의 runtime error가 compile time error 대비 장점은?

A : Dagger는 적용하기 어려웠음. 선수지식과 많은 코드가 필요했음. Compile error시 어디서 에러가 나는지 찾아야 했음.

​ Koin을 하면서 코드 스타일로 의존성 주입을 하면서 에러 찾기가 편함.

​ 복잡한 서비스의 경우는 Dagger, 심플한 경우 Koin이 괜찮아 보였음.

Q : TDD가 아닌 BDD를 한 이유는?

A : TDD와 BDD의 가장 큰 차이는 구현 대신 행동 즉 행위를 테스트하는 것임. Functional Programming 관점에서 행동 기반이 맞다고 생각했음.

Q : Model의 수에 따라 Mapper의 수도 지속적으로 증가하는데, Mapper가 특별한 경우가 아닐 땐 1:1로 매핑되고 Koin으로 바인딩 해주는 것들이 보일러 플레이트로 느껴짐. 이런 코드를 좀 더 줄일만한 아이디어는?

A : 진정한 클린 아키텍쳐라면 레이어 별로 테스트가 가능해야 하고, Mapper도 테스트되어야 함. 똑같은 input이 들어갈 경우 똑같은 entity를 내보내주는 부분이 확인 되어야 함. 그것 때문에 인터페이스를 만들고 개선의 여지가 있다고 봄. 모델에 단순히 컨버팅해주는 코드들이 많아 필요 없다거나 확장함수로 처리하는 경우도 있지만 제대로 하겠다면 매퍼를 만드는게 필요하다 봄.

Q : 개발 일정을 단축시킨다거나 안드로이드 인력을 줄였다거나 하는 경우는?

A : input과 output을 먼저 정의하고 ViewModel을 View를 짜고 그 다음에 binding 코드를 짜는 방식이 사고의 흐름에 맞게 개발한다고 봄. 그래서 빠르게 개발한다고 보지만, 선수지식과 경험이 필요한 부분이라 경우에 따라 다를 것으로 봄.

Q : input & output 등을 인터페이스로 구현하고 ViewState 모델링한 부분은 MVVM과 별개로 적용한 것인지?

A : 고민했던 부분. output 자체를 state로 나누는 것과 아예 router명을 명시해서 나누는 것도 고민했음. 개념적으로 같고 코드상의 차이로 보고, 모든 결과에 state라는 것과 그 외에는 router 코드로 판단하기로 함. MVVM과 별개로 봐도 될 것으로 봄.

완벽한 아키텍처, 완벽한 패턴 그리고 정답은 없다고 봄.

각각의 프레임워크, 플랫폼, 서비스 그리고 어떤 사람들이 어떤 컨벤션을 가지고 개발하는지에 따라 달라진다고 봄.

어떻게 할지에 고민보다는 왜 이렇게 하는지에 대한 이유만 명확하게 생각하고 논의하면 될 것으로 봄.

'IT > 행사' 카테고리의 다른 글

[행사] Naver Tech Concert Day-2 04  (0) 2019.01.14
[행사] Naver Tech Concert Day-2 03  (0) 2019.01.14
[행사] Naver Tech Concert Day-2 01  (0) 2019.01.14
[행사] Microsoft Azure Everywhere  (0) 2019.01.11
[행사] Naver Tech Concert Day-1 06  (0) 2019.01.07

Naver Tech Concert Day-2

01. 변화의 시대 : 안드로이드 앱 어떻게 개발할 것인가?

  • 발표자 : 신동길 (NAVER / 네이버앱개발)

  • 동영상 : https://tv.naver.com/v/4635525/list/272653

  • 슬라이드 : https://www.slideshare.net/NaverEngineering/21-121507374

  • 세션설명 : 변화의 시대 : 안드로이드 앱 어떻게 개발할 것인가? 안드로이드는 끊임없는 OS 버전 뿐만아니라 개발 언어, 구조, GUI등 많은 부분에서 다양항 변화가 시도되고 있습니다. 많은 방법론과 라이브러리가 제공되다보니 어떤 전략과 기준으로 개발해야하는지 혼돈스러울 때가 많습니다. 네이버 앱의 개편에 적용한 기술 사례와 방법론을 통해서 효율적인 앱 개발애 대해서 얘기하고자 합니다.

  • 목차 :

    1. 안드로이드 앱의 구조
    
    2. 함수형과 객체형: 함수형과 객체형 개발 어떻게 적용할 것이가?
    
    3. GUI 기반 구조: Activity 구조,Fragment 구조, View 기반의 구조의 장단점은 무엇인가? 어떤 구조를 택할 것인가?
    
    4. 데이터 모델: 데이터 모델이 필요한가? MVC, MVP, MVVM 이 정말 필요한가?
    
    5. 프로세스 와 쓰레드 어떤 모델로 가져갈 것인가? : Service, AsyncTask, Thread, JobManager, WorkManager, Coroutine ,Loader 너무 많은데
    무엇이 정답인가? 멀티 프로세스 모델 어떻게 설계할 것인가?
    
    6. 통계와 설정: 어떤 정보를 모으고 최적할 것인가?
    

네이버 앱을 개편하면서 베타 버전까지 냈는데, 이 때까지 고민한 것들에 대한 이야기임.

(네이버에서도 베타 중에 기능 추가 요청이 들어오는 경우가 있음.)

https://play.google.com/store/apps/details?id=com.navercorp.techcon&hl=en_US

네이버 테크 콘서트 앱은 머티리얼 디자인만 적용하여 7시간 개발한 앱임.

(다른 프레임워크는 전혀 사용하지 않음)

앱번들, 사이닝 등록과정까지 10시간 정도 걸림.

1. 무엇이 변했는가?

Hardware

  • Multi-Core
  • Large Memory
  • Big Display

Platform

  • Dalvik -> JIT/ART
  • Many Strictions

다양한 프레임워크

  • Lottie
  • RxJava
  • Retrofit
  • Glide, Picasso, OkHttp

좋은 앱이란

  1. Big Sized
  2. Mulit-Core Processor
  3. Mulit-Featured
  4. Multi-Media

단말이 바뀌고 좋아지다 보니 보다 많은 기능을 더 빠르게 개발할 것을 요구함.

애니메이션도 많이 붙게 되고, 오히려 느려지는 현상도 있음.

환경에 효율적이어야 함. (네이버 앱도 전면 개편 필요....)

네이버 앱은 웹뷰 사용하지 않고 웹 엔진을 사용함. (웹뷰 문제와 웹뷰의 수정 지연 때문에)

크로스워크 XWalk를 사용하고 직접 수정 및 미지원 기능 등은 기본 웹뷰를 사용함.

웹뷰에서 불가한 low 레벨 통계도 사용함.

자바의 문제점

  1. 상속과 오버라이드 (3단계 이상시)
  2. 객체 내 모두 선언해야 함 (딱딱함)
  3. Listener 표현의 복잡함

즉 Big Size, 복잡한 GUI 환경에는 적합하지 않음.

좀 더 간결한 코딩 필요(많은 기능 필요)

Functional vs Objective Programming

Functional Programming

UI 코드를 간결히 표현할 도구 필요

코어 로직에 대해 간결히 표현할 방식의 필요

많은 함수를 가진 큰 객체 이벤트 처리 효율성 재고 필요

네이버 앱에서는 로직의 계층구조를 플랫하게 변경하기 위해 자바 8을 고려함.

자바 8의 스트림, 데이터의 파이프라이닝을 안드로이드 N부터 지원함.

레트로 람다 등의 도입을 고려했으나 협업을 고려했을 때 쉽지 않음.

결국 코틀린을 고려했지만, 언어를 바꾸는 것이라 고민하던 중 안드로이드 언어로 지정되어 적용하게 됨.

프레임워크를 쓰기 위해서 프레임워크에 대한 공부가 우선되기 때문에 대규모(5명 이상?) 프로젝트일 경우 신중해야 함.

그래서 협업자들에 대한 커뮤니케이션이 필요함. (다 생각이 다르고 설득이 어려움)

2. 앱의 구조

네이버 앱에서 Web Engine이 중심

Activity란?

라이프 싸이클은 왜 존재하는가? (다른 OS는 앱 단위에서 관리함)

Activity는 상태 관리와 UI 관리를 같이 함.

Activity는 각각이 앱이라 봐도 됨. (별도 프로세스, 런처 가능하여 멀티 런처 가능함)

안드로이드는 하나의 패키지 안에 복수개의 앱을 만들 수 있게 되어 있음.

Activity 간에는 Package 말곤 연계 없음

Activity 간에는 다 오픈 되있는 개념.

위가 안드로이드 UI 개념의 특징임.

Fragment는 화면 분할 개념이 시작이었으나 Activity 내에서 네비게이션 기능을 주로 함.

Intent란?

Intent는 RPC 역할. (프로세스 간의 통신)

Parcel로 Serialize한 정보 저장 역할.

Activity를 늘리는 건 좋은 방법이 아니고 비효율적임.

Ui Navigation

  • Activity만 사용
  • Activity 내 Fragment 사용
  • Activity 내 View 단위 사용 (많이 사용)

Activity나 뎁스가 깊어지면 안좋은 구조.

Fragment는 attach/dettach 시에 문제가 많음.

네이버 앱 내 탭의 Fragment는 좌우플리킹시 라이프 싸이클이 있어 제어에 문제가 있음.

라이프 싸이클 사용은 객체 각자 관리해야 하면 결국 상속 구조로 가야 함.

(꼭 프레임워크 제공 라이브러리 등에 의존할 필요 없음)

Event Dispatching (Big SizeComponent)

이벤트에 대해서 각 기능(ex. 툴바, 탭 등등)마다 처리함. (ex. Back 키 등)

각 기능에서 할 경우 최상위에서 상속받아 계층별로 사용하게 됨.

이를 루트 액티비티에서 처리하게 해도 됨.

루트 액티비티에서 이벤트 맵/리스트 관리 후 콜백 등을 처리하면 됨. (예전에는 기피했으나 멀티 코어 세상이라..)

네이버 앱의 경우 웹뷰의 NestedScroll이 필요한 경우 웹뷰의 엔진으로 어떤 것을 쓸지 몰라 추상화가 필요함.

인터페이스 상속과 큰 클래스에서 엔진 두가지를 다 관리하는 방법이 있었는데, 인터페이스 상속을 택함.

멀티프로세스

  1. 메인과 구별되는 기능요소
  2. 별도의 모듈(Dynamic Linked Library) 로드하는 요소 so 사용 등은 메모리 제거 안되므로 프로세스 분리 필요. (외부 라이브러리 등)
  3. 모듈의 크기가 큰 경우
  4. 멀티미디어와 같이 자원을 많이 소요하는 요소
  5. 크래시시 모듈 차단

추가로 UI 쓰레드도 멀티가 될 수 있음.

쓰레드

쓰레드풀(AsyncTask, Coroutine)

쓰레드풀은 멀티미디어 프로세스에서 못써서 쓰레드를 우선 순위 컨트롤해 사용. (쓰레드풀 사용시 성능 저하)

3. Design Architecture 적용

네이버 앱은 MVP (일반 브라우저와 Ai 기능 분리 사용)

객체 크기가 크다보니 MVVM보다 적합함

인앱 브라우저는 MV/Plugin (Processor(Controller)는 플러그인 형태)

Piped Filter Model

Piped Input(Output) Steam 사용.

source, transform, sink 필터 만들어 사용.

4. Multi Package

  1. Apk Extension(.obb)
  2. Another Apk
  3. App Bundle
  4. Instant App

앱 패키징은 앱번들 고려 중

5. 다양한 Framework 어떻게 활용할 것인가?

네이버 앱은 프레임워크를 많이 안씀.

Lottie, OkHttp, Glide, Retrofit 정도 사용 중.

Framework는 주로 native code, 오픈 소스 많지 않음.

대규모 서비스 앱에서 오픈 소스 사용에 신중해야 함.

(네이버 앱은 유저 3천만에 UV 억단위인데 Crash 천단위를 유지하기 위해 오픈 소스에 신중했음)

'IT > 행사' 카테고리의 다른 글

[행사] Naver Tech Concert Day-2 03  (0) 2019.01.14
[행사] Naver Tech Concert Day-2 02  (0) 2019.01.14
[행사] Microsoft Azure Everywhere  (0) 2019.01.11
[행사] Naver Tech Concert Day-1 06  (0) 2019.01.07
[행사] Naver Tech Concert Day-1 05  (0) 2019.01.07

+ Recent posts