[데브인턴십 1기] 안드로이드 앱 개발 인턴십 후기

설채은

업데이트:

안드로이드 개발 인턴 (feat. StoryPlay)

대학교 2학년 때 프로젝트에서 안드로이드 앱 개발을 담당했었는데, 이 때부터 안드로이드 개발자가 되고 싶다는 생각을 했습니다. 안드로이드 앱 개발은 노트북에 Android Studio가 깔려있고, 애뮬레이터 혹은 개발자 모드가 켜진 안드로이드 휴대폰만 있다면, ‘Hello World!’를 화면에 띄우는 앱은 간단하게 만들 수 있을 정도로, 접근성이 좋습니다.

android

물론, 개발을 하기 위해서는 개발 언어도 알아야 합니다. 안드로이드 앱을 개발할 때는 주로 Java 혹은 Kotlin을 사용하게 되며, Layout 이나 Property의 경우는 XML 문서를 사용하고 있습니다. 저는 처음 안드로이드 앱 개발을 시작할 때, Java언어에 대해 친숙한 상태였기에 자연스럽게 Java언어로 개발을 시작했습니다. 그러나 최근, 2017년에 구글은 Kotlin을 안드로이드 앱 개발 공식 언어로 채택했고, 그 이후 2019년에 구글은 Kotlin First를 선언했습니다.

그러나 Kotlin이 Java보다 더 간결하기 때문에, Java를 안다면 Kotlin을 익히는 것은 어려운 일은 아닙니다. 저 또한 Kotlin 보다는 Java를 사용해오던 개발자였으나, 스토리플레이가 Kotlin으로 개발되어 있었기에 자연스럽게 Kotlin을 사용하게 되었고, 지금도 Kotlin을 몰랐다고 해서 개발하는 데 어려움은 없습니다.


StoryPlay?

스토리 플레이는 인터렉티브 채팅형 소설 게임으로, Activity는 SplashActivity와 MainActivity 두개로 구성되어 있고, MainActivity 안에서 모두 Navigation을 사용합니다. MVVM(Model-View-ViewModel)의 패턴을 사용하고 있으며, LiveData, DataBinding 등을 사용하고 있습니다. 또한 API Query는 GraphQL을 사용하고 있습니다.


그동안 개발한 Feature

스토리 상세 페이지 UI 변경

list_ui_1 list_ui_2 list_ui_3 list_ui_4

스토리 상세 페이지 UI의 경우, 원래 기존에는 … 버튼을 클릭하면 하단 Sheet에 버튼 2개가 있는데, 이 버튼을 분리하여 함수를 실행하면 끝이었습니다.

when (item.state) {
       Episode.State.READING -> {
            //읽는중
       }
       Episode.State.ACTIVE -> {
            //읽기 가능 상태
       }
       Episode.State.DONE -> {
            //읽기 완료
       }
       Episode.State.NEED_PURCHASE -> {
            //구매해야함
       }
}

에피소드의 상태에 따라서, 에피소드의 보여지는 UI가 달라지게 합니다.

imgEpisodeStat.setOnClickListener {
    event.onClickStat(item)
}

imgEpisodeRetry.setOnClickListener {
    event.onClickRestart(item)
}

원래 화면에서도 onClickStat과 onClickRestart 함수는 구현되어 있었어서, 해당 버튼에 OnClickListener만 설정해주면 됩니다.


채팅 이미지 메세지 / 배너 이미지 추가

chat_banner_image chat_image

  • 계획
    1. 서버에서 이미지에 대한 정보 (이미지, width, height)를 받아온다
    2. 받아온 대사에 대한 정보를 가공하는 코드들을 이미지를 받아올 수 있도록 수정한다
    3. 받아온 이미지를 채팅처럼 띄울 수 있게 Chat(Yours/Mine)Holder, chat_(yours/mine)_item.xml 을 수정한다
    4. 테스트

채팅이 스크립트부터 시작해서 ChatFragment와 ChatViewModel로 전달되는 과정 chat_model

기존의 스토리플레이에서는 대사를 상대방 대사(Yours) 와 주인공 대사(Mine)으로 분류한 뒤, 그 안에서 말하는 사람의 대사 속성이 속마음인지, 혹은 그냥 일반 대사인지에 따라서 다르게 보여주고 있었습니다. 채팅 이미지 또한 상대방이 보내거나 주인공이 보낼 수 있다고 생각하여 Chat(Yours/Mine) 으로 분류하는 것이 맞다고 생각했습니다. 그래서 Chat.Mine, Chat.Yours 에 이미지를 불러오는 데 필요한 정보인 width, height를 추가하여 Mine/Yours에서 이미지를 불러올 수 있도록 하였습니다.

when (item.type) {
     Chat.Type.MONOLOGUE -> {
          //속마음
     }
     Chat.Type.NORMAL -> {
          //대사
     }
     Chat.Type.IMAGE -> {
          val params = imgMeChat.layoutParams as ConstraintLayout.LayoutParams
          imgMeChat.layoutParams = params.apply {

          dimensionRatio = "${item.width}:${item.height}"
          
          }

          Glide.with(imgMeChat)
                .load(item.text)
                .transition(DrawableTransitionOptions.withCrossFade())
                .fitCenter()
                .into(imgMeChat)
     }
     Chat.Type.SELECT -> {
          //선택
     }

처음에는 이미지를 불러오는 데는 성공을 했었습니다. 그러나 이미지의 사이즈를 조절하는 데 어려움을 좀 겪었습니다. 헬로우봇과 동일하게 해야한다고 하셔서, 방법을 찾아서 너비의 40%만큼 이미지가 차지하게 했는데, 이상하게도 화면의 40% 보다 더 작아 보였습니다. 알고보니, 이미지가 속한 chat_(mine/yours)_item.xml 에서 해당 Layout 자체에 Padding을 적용시키고 있었기 때문에 문제가 발생한 것이었습니다. 그래서 이 padding을 없애고, 해당 너비만큼 Layout에 padding을 설정하지 않고 각각 view들에 margin을 적용하여 이미지의 너비가 화면의 40%가 되도록 설정하였습니다.

배너 이미지의 경우는 말하는 사람과 상관 없었으므로, 새로 ChatBannerImageHolder를 만들어서 새로운 메세지 타입을 추가했습니다.

class ChatBannerImageHolder (
    containerView: View,
    holderEvent: HolderEvent
) :
    AutoBindViewHolder<Chat.BannerImage, ChatBannerImageHolder.Event>(containerView, holderEvent) {

    private val binding = ChatBannerimageItemBinding.bind(containerView)

    override fun bind(item: Chat.BannerImage) {
        renderUi(item)
    }

    private fun renderUi(item: Chat.BannerImage) = with(binding) {
        val params = imgBanner.layoutParams as ConstraintLayout.LayoutParams
        imgBanner.layoutParams = params.apply {
            dimensionRatio = "${item.width}:${item.height}"
        }

        Glide.with(imgBanner)
            .load(item.text)
            .transition(DrawableTransitionOptions.withCrossFade())
            .fitCenter()
            .into(imgBanner)
    }

    interface Event : HolderEvent {
        fun onClickChatRootView()
    }

    companion object {

        val CREATOR: (ViewGroup, HolderEvent) -> ChatBannerImageHolder = { 
        }

        val DIFF = object : DiffUtil.ItemCallback<Chat.BannerImage>() {
        }
    }
} 

진동/효과음 추가
  • 계획
    • 진동 : sourceLine, statementType, pattern, background
    • 효과음 : sourceLine, statementType, link, background
  1. 서버로부터 진동(Vibration), 효과음(SoundEffect)과 해당하는 속성들을 받아온다.
  2. 새로 ChatEffectHolder와 chat_effect_item.xml을 생성, ChatEvent에 ChatEffectHolderEvent 추가
  3. 진동과 효과음 모두 Effect로 분류하여 타입 추가
  4. 다른 chat이나 description과 똑같이 설정 후 event 추가 (ChatFragment & ChatViewModel)
  5. 진동 : 휴대폰에서 VIBRATE 권한 필요, Vibrator로 재생
  6. 효과음 : SoundPool과 MediaPlayer 두 종류 중에서 MediaPlayer 선택. SoundPool은 스토리플레이의 효과음을 관리하는 데 적절하지 않다고 생각. 한 가지를 한 번 쭉 재생하되, 다른 효과음 재생이 시작되면 기존에 재생되던 효과음은 멈춰야 함
  7. 테스트

배너 이미지와 마찬가지로, ChatEffectHolder를 만들어서 새로운 메세지 타입을 추가 하였습니다.

private fun vibrate() {
    val vibrator = context?.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
    if(Build.VERSION.SDK_INT >= 26) {
        vibrator.vibrate(VibrationEffect.createOneShot(200,VibrationEffect.DEFAULT_AMPLITUDE))
    } else{
        vibrator.vibrate(200)
    }
}
private fun soundEffect(text: String){
       mediaPlayer.apply{
       if(this.isPlaying()){
            mediaPlayer.reset()
       }
       try {
            setDataSource(text)
            setAudioAttributes(
                AudioAttributes.Builder()
                    .setUsage(AudioAttributes.USAGE_MEDIA)
                    .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
                    .build()
             )
             prepare()
             start()
        }catch (e : IllegalStateException){

        }

    }
    mediaPlayer.setOnCompletionListener() {
        mediaPlayer.reset()
    }
}

효과음을 처음에 적용 시켰을 때, 효과음을 쭉 재생하되 다른 효과음을 만나면 정지하고 새로운 효과음이 재생되어야 하는데 둘 다 동시에 재생이 되는 현상이 발생했었습니다. 제가 처음에 적용시켰던 코드가 soundEffect 함수에서 mediaPlayer 객체를 새로 계속 생성하고 있어서 동시에 재생이 되고 있다는 것을 깨닫고, mediaPlayer의 생명주기를 참고하면서 어떻게 할까 고민하다가 ChatFragment에서 하나의 mediaPlayer를 선언해서, 새로운 효과음을 만났을 때 이 mediaPlayer가 재생중이면 mediaPlayer를 reset 시키고 새로운 효과음을 재생시키도록 했습니다.


Facebook SDK Issue

인턴을 시작한 지 3주 쯤 되었을 때, 올라가 있는 스토리플레이 앱에서 인앱 결제에 대한 이벤트가 Facebook Analytics에서 뜨지 않는 문제가 발생했습니다. 원래는 결제가 발생하면 Facebook SDK가 알아서 결제 이벤트를 로깅해줘야 하는데, 그러지 못하고 있었습니다.

일단 원래의 스토리플레이는 제가 개발한 것이 아니었기에 해당 문제가 발생했을 때는 적잖이 당황했습니다. 그리고 어떻게 해결을 해야할 지 구글링을 시작했죠. 그러나 나오는 해결책은 없었고, 공식 문서에도 해결 방법은 없었습니다. 결론은 Facebook SDK가 구글 Billing Service의 새로운 버전과 호환되지 않는 Facebook SDK 자체의 문제였습니다. 당연히 라이브러리의 문제일 것이라는 생각은 하지 못했고, 어디에서 잘못된 것인지 계속 찾았습니다. 아마 제 짧은 인턴 생활 중에서 제일 어려운 문제였던 것 같습니다.

페이스북 팀 과의 미팅을 통해 Facebook SDK 자체의 문제라는 결론이 나왔었고, 그래서 수동으로 앱에 코드를 심어서 배포를 했습니다.

 val logger : AppEventsLogger = AppEventsLogger.newLogger(activity)
 logger.logPurchase(pendingPrice.toBigDecimal(), Currency.getInstance(current)) //구매 이벤트 발생시 가격과 함께 이벤트 발생

구매 외에도 마케팅에 도움이 될만한 결제 시도, 구독, 컨텐츠 조회에 대한 이벤트로 같이 심어놓았습니다.


BigQuery 조회

파이어베이스와 빅쿼리를 연결한 후, 빅쿼리에 저장 되어있는 데이터가 필요한데 혹시 데이터 조회가 가능한 지 저에게 요청이 들어왔습니다. BigQuery 사용은 해본 적이 없어서 할 수 있을지 반신반의하긴 했지만, 찾아보니 빅쿼리에선 SQL을 사용하여 데이터를 조회하고 있었고, 마침 저는 수업에서 데이터베이스에 대해서 배울 때 SQL 언어를 같이 공부했었던터라 할 수 있다고 생각되어서 일단 해보겠다고 했습니다. 그러나 빅쿼리에 데이터가 어떤 방식으로 저장되어 있고, 어떤 형식으로 되어있는지 몰라서 조금 헤맸지만, 구글링을 통해서 필요한 데이터의 조회는 성공했습니다.

SELECT 
    (SELECT value.string_value FROM UNNEST(event_params) WHERE key='last_message') AS lastmessage,
    COUNT(*) as cnt
FROM `*`
WHERE(
_TABLE_SUFFIX BETWEEN FORMAT_DATE('%Y%m%d',DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY))
AND
FORMAT_DATE('%Y%m%d',DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY))
AND
event_name = 'first_touch_back_button'
)
GROUP BY lastmessage

위의 SQL은 최근 7일 동안 앱 첫 실행에서 실행되는 온보딩 작품에서 어느 대사에서 빠져나갔는지에 대한 데이터를 조회하는 SQL문 입니다. 우선은 필요한 데이터에 대한 조회에 성공은 했지만, BigQuery의 구조를 파악해야겠다고 생각했습니다.


iOS와 Android

앱을 개발하면서, iOS와 Android는 뗄 수 없는 관계 같습니다. 저는 안드로이드 개발자로서 앱을 계속 개발했었는데, 저만 혼자 개발을 진행하는 것이 다는 아니라는 것을 깨달았습니다. 스토리플레이는 iOS와 Android 모두에서 서비스 되어야 하고, 이 두 앱은 똑같아야 하겠죠. 그러면서 iOS 개발자 분과 소통을 하면서 든 생각은 저도 iOS 개발에 대해 아는 것이 좋겠다는 생각이 들었습니다. 안드로이드에서는 하단 배경 이미지가 잘리는데, iOS에서는 잘리지 않도록 구현이 되어 있었습니다. 이 때 iOS에서는 어떻게 구현이 되어있는 지 궁금하다는 생각이 들면서 iOS 앱 개발에 대해서도 아는 것이 결과적으로 개발함에 있어서 저에게도 도움이 되고, 생산성도 조금 높일 수 있지 않을까 하는 생각이 들었습니다.


띵스플로우에서 일하면서 배우고 좋았던 점들

스토리플레이를 개발하면서, 안드로이드 아키텍쳐를 구성하는 데는 꽤 많은 선택지가 있고, 이 선택지에서 앱에 잘 적용될 수 있는 것들을 고르면 매우 깔끔한 구조의 앱이 만들어진다는 것을 알게되었습니다. 그러기 위해서는 많은 경험과 지식을 쌓아야 하는 것이 매우 중요함을 깨달았습니다. 또한 디자이너, 마케터들, 에디터들, 그리고 다른 개발자와 잘 협업하고 의견을 주고 받는 것이 매우 중요하다는 것도 느꼈습니다. 피쳐 하나를 개발할 때도, 늘 ‘이렇게 하면 에디터들이 스토리를 전개할 때 원하는 느낌을 줄 수 있을까?’ 를 생각하면서 개발했었습니다 그러면서 띵플의 공유정신이 매우 중요하다는 것도 깨닫게 되었습니다.

개발하면서, 늘 파이어베이스와 페이스북 이벤트를 모니터링 하기도 하였고, 빅쿼리 조회도 했습니다. 빅쿼리 조회를 할 때는 학교에서 배웠던 SQL이 이렇게 쓰이는구나 라고 생각하면서 역시 뭐든 알아두면 어디든 쓰게 되어있다는 것을 깨달았습니다

띵스플로우에서 스토리플레이 안드로이드 개발자로 일하면서 다양한 직무의 여러 사람들과 소통하며 협업할 수 있어서 좋았고, 그 과정에서 배워가는 것이 많았습니다. 또한 스토리플레이 앱을 개발하는 것이 재미있기도 했습니다. 제 첫 직장생활이자 개발자로서 일하는 곳이 띵스플로우라서 매우 다행이었고, 행운이었다고 생각합니다. 감사합니다!

lamama_with_picket

띵스플로우 팀은 자기의 일을 좋아하고 잘하는 사람들 입니다. 사용자와 서비스를 중심으로 빠르게 실행하고 학습하며, 다양한 직무의 사람들이 협업을 통해 시너지를 내고 있습니다. 다양한 콘텐츠 혁신을 이루고 있는 띵스플로우 팀에 함께할 분을 찾습니다! 언제든 people@thingsflow.com로 이메일을 주시기 바랍니다!

태그: , ,

카테고리:

업데이트:

댓글남기기