<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>darby-_-.log</title>
        <link>https://velog.io/</link>
        <description>성장하는 개발자가 되자</description>
        <lastBuildDate>Mon, 22 Apr 2024 15:53:44 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>darby-_-.log</title>
            <url>https://velog.velcdn.com/images/darby-_-/profile/b02feb6f-ec8f-4a75-9d58-72362e5c86e0/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. darby-_-.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/darby-_-" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Android Toml로 Gradle Version 관리하기]]></title>
            <link>https://velog.io/@darby-_-/Android-Toml%EB%A1%9C-Gradle-Version-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@darby-_-/Android-Toml%EB%A1%9C-Gradle-Version-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 22 Apr 2024 15:53:44 GMT</pubDate>
            <description><![CDATA[<h4 id="잡담">잡담</h4>
<p>기존에는 Gradle에 라이브러리를 추가할때</p>
<pre><code>implementation &#39;com.squareup.retrofit2:retrofit:(version)&#39;</code></pre><p>이런식으로 추가를 하고있었는데 다른 오픈 프로젝트 분석을 하다가 발견을 했다.</p>
<pre><code>implementation(libs.hilt.android)</code></pre><p>이런식으로 Gradle에 추가를 하고 있었는데 libs? 이게 뭐길래 다 저기서 등록하는 걸까? 라는 생각을 갖게 되었고 구글링을 해보니 toml 파일에서 쉽게 관리를 할 수 있다는것을 알게 되었다.</p>
<hr>
<h3 id="toml-사용법">Toml 사용법</h3>
<p><img src="https://velog.velcdn.com/images/darby-_-/post/d2596a65-c0ff-4b44-8e2c-1608c33f9c34/image.png" alt=""></p>
<p>프로젝트단에 gradle에 libs.versions.toml 파일을 추가해주시면됩니다.</p>
<pre><code class="language-kotlin">[versions]
agp = &quot;8.3.2&quot;
kotlin = &quot;1.9.22&quot;
kotlinxCoroutines = &quot;1.7.3&quot;
kotlinxSerializationJson = &quot;1.6.2&quot;
ksp = &quot;1.9.22-1.0.18&quot;
coreKtx = &quot;1.13.0&quot;
retrofit = &quot;2.9.0&quot;
...

[libraries]
...
androidx-appcompat = { group = &quot;androidx.appcompat&quot;, name = &quot;appcompat&quot;, version.ref = &quot;appcompat&quot; }
material = { group = &quot;com.google.android.material&quot;, name = &quot;material&quot;, version.ref = &quot;material&quot; }
hilt-android = { group = &quot;com.google.dagger&quot;, name = &quot;hilt-android&quot;, version.ref = &quot;hilt&quot; }
hilt-compiler = { group = &quot;com.google.dagger&quot;, name = &quot;hilt-android-compiler&quot;, version.ref = &quot;hilt&quot; }
hilt-android-testing = { group = &quot;com.google.dagger&quot;, name = &quot;hilt-android-testing&quot;, version.ref = &quot;hilt&quot; }
kotlinx-coroutines-android = { group = &quot;org.jetbrains.kotlinx&quot;, name = &quot;kotlinx-coroutines-android&quot;, version.ref = &quot;kotlinxCoroutines&quot; }
kotlinx-coroutines-test = { group = &quot;org.jetbrains.kotlinx&quot;, name = &quot;kotlinx-coroutines-test&quot;, version.ref = &quot;kotlinxCoroutines&quot; }
retrofit-core = { group = &quot;com.squareup.retrofit2&quot;, name = &quot;retrofit&quot;, version.ref = &quot;retrofit&quot; }
retrofit-gson = { group = &quot;com.squareup.retrofit2&quot;, name = &quot;converter-gson&quot;, version.ref = &quot;retrofit&quot; }
...


[plugins]
androidApplication = { id = &quot;com.android.application&quot;, version.ref = &quot;agp&quot; }
jetbrainsKotlinAndroid = { id = &quot;org.jetbrains.kotlin.android&quot;, version.ref = &quot;kotlin&quot; }
androidLibrary = { id = &quot;com.android.library&quot;, version.ref = &quot;agp&quot; }
ksp = { id = &quot;com.google.devtools.ksp&quot;, version.ref = &quot;ksp&quot; }
hilt = { id = &quot;com.google.dagger.hilt.android&quot;, version.ref = &quot;hilt&quot; }
kotlin-jvm = { id = &quot;org.jetbrains.kotlin.jvm&quot;, version.ref = &quot;kotlin&quot; }
kotlin-android = { id = &quot;org.jetbrains.kotlin.android&quot;, version.ref = &quot;kotlin&quot; }
</code></pre>
<p>toml은 4개로 구성되어 있는데 저는 우선 3개만 쓰고 있습니다.</p>
<p><strong>[versions]</strong>
해당 라이브러리의 버전을 관리할 수 있습니다.</p>
<pre><code class="language-kotlin">composeOptions {
        kotlinCompilerExtensionVersion = libs.versions.androidxComposeCompiler.get()
    }</code></pre>
<p>이런식으로 composeOptions에서 코틀린컴파일러버전도 따로 관리가 가능해서 좋습니다. 
get()하면 해당 버전을 읽어옵니다.</p>
<p><strong>[libraries]</strong>
실질적으로 라이브러리의 주소가 들어가는 곳 입니다.
위에 적은 retrofit2로 예시를 들어보겠습니다.</p>
<pre><code class="language-kotlin">implementation &#39;com.squareup.retrofit2:retrofit:2.9.0&#39;</code></pre>
<p>group = &quot;com.squareup.retrofit2&quot;
name = &quot;retrofit&quot;
version = retrofit &lt;&lt; 위에서 정의한 버전을 가져다가 쓰면 됩니다.</p>
<pre><code class="language-kotlin">retrofit-core = { group = &quot;com.squareup.retrofit2&quot;, name = &quot;retrofit&quot;, version.ref = &quot;retrofit&quot; }</code></pre>
<p>이런식으로 작성을 하시면 됩니다.</p>
<pre><code class="language-kotlin">api(libs.retrofit.core)</code></pre>
<p>그리고 gradle에서 이렇게 작성하고 Sync Now 해주시면 됩니다.</p>
<p><strong>[plugins]</strong></p>
<pre><code>plugins {
   id(&quot;com.android.application&quot;) version &quot;8.3.2&quot; apply false

}

plugins {
   id(&quot;com.android.application&quot;)

}</code></pre><p>기존에 plugins도 위와 같이 작성이 가능합니다.</p>
<pre><code class="language-kotlin">androidApplication = { id = &quot;com.android.application&quot;, version.ref = &quot;agp&quot; }

-&gt;
alias(libs.plugins.androidApplication) apply false</code></pre>
<p>이런식으로 바꿔서 적용을 할 수 있습니다.</p>
<p><strong>[bundles]</strong></p>
<pre><code>retrofit = [
    &quot;retrofit-core&quot;,
    &quot;retrofit-converter-gson&quot;
]</code></pre><p>번들에서 따로 retrofit만 넣을 수 있습니다.</p>
<pre><code> implementation(libs.bundles.retrofit)</code></pre><p>이렇게 넣으면 retrofit 번들로 꾸려진 건 한번에 적용이 됩니다.</p>
<hr>
<p>후기</p>
<p>개인적인 생각으로는 싱글모듈앱에서는 사용하면 좋고 아니어도 괜찮은 느낌이다. 
하지만 앱의 확장성을 생각한다면 해주는 편이 관리하기가 수월한 것 같다.</p>
<p>최근에 멀티모듈로 구성한 앱을 개발할 때 toml로 사용을 해봤는데 처음 설정만 해놓으면 꾸준히 앱을 관리하는 측면에서 훨씬 수월한것 같다. </p>
<p>앞으로도 toml로 계속 쓰게될 것 같다. </p>
<p>Toml 쉽다! 편하다! </p>
<hr>
<p>참고 
<a href="https://developer.android.com/build/migrate-to-catalogs?hl=ko#kts">https://developer.android.com/build/migrate-to-catalogs?hl=ko#kts</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Android Text에 이미지 넣기]]></title>
            <link>https://velog.io/@darby-_-/Android-Text%EC%97%90-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%84%A3%EA%B8%B0</link>
            <guid>https://velog.io/@darby-_-/Android-Text%EC%97%90-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%84%A3%EA%B8%B0</guid>
            <pubDate>Mon, 18 Mar 2024 17:22:47 GMT</pubDate>
            <description><![CDATA[<p>글을 쓰기 앞서... 과거 회상</p>
<p>나는 기본적으로 내가 보고 사용하는 것에 갑자기 &quot;이건 어떻게 만들었을까?&quot;에 대한 막연한 궁금증을 가질 때가 종종 있다. 이번에 글을 쓰게 된 이유도 그 중 하나이다.</p>
<p>지금은 한국에서 서비스를 하고 있지 않은 앱인 Twitch를 자주 봤었는데 <del>지금은 다 치지직으로 갔다</del> 나는 영상보다 채팅창의 구현이 너무나 궁금했었다.</p>
<p>그 이유는 글자들 사이에 이모티콘이 있고 또는 이미지, Gif가 있었기 때문이다.</p>
<p>그 당시 내가 상상으로 구현을 해본다고 쳤을 때 나였다면 이렇게 구현했을 것 같다라고 생각했었다.</p>
<p><img src="https://velog.velcdn.com/images/darby-_-/post/9f3889c8-f40b-4b6a-88ae-40f9e15a63d6/image.png" alt=""></p>
<p>내가 생각했던 방식은 View가 있고 들어온 String에 따라 특정 태그같은걸로 Text와 Image를 나눠서 동적으로 뷰를 만드는 방식으로 생각을 했었다.. 지금 생각하면 참 어려운 방식으로 만들었다 라고 생각이 된다.. (멍청한 생각)</p>
<div style="display: grid; grid-template-columns: auto auto; gap: 20px;">
  <img src="https://velog.velcdn.com/images/darby-_-/post/348b2848-5323-40bf-ae95-3769293e3ab4/image.jpg" style="width: 100%;">
  <img src="https://velog.velcdn.com/images/darby-_-/post/c470e9fd-cc75-4432-9d81-4f6a16d490e6/image.jpg" style="width: 100%;">
</div>



<h1 id="text">Text</h1>
<pre><code class="language-kotlin">@Composable
fun Text(
    text: AnnotatedString,
    modifier: Modifier = Modifier,
    color: Color = Color.Unspecified,
    fontSize: TextUnit = TextUnit.Unspecified,
    fontStyle: FontStyle? = null,
    fontWeight: FontWeight? = null,
    fontFamily: FontFamily? = null,
    letterSpacing: TextUnit = TextUnit.Unspecified,
    textDecoration: TextDecoration? = null,
    textAlign: TextAlign? = null,
    lineHeight: TextUnit = TextUnit.Unspecified,
    overflow: TextOverflow = TextOverflow.Clip,
    softWrap: Boolean = true,
    maxLines: Int = Int.MAX_VALUE,
    minLines: Int = 1,
    inlineContent: Map&lt;String, InlineTextContent&gt; = mapOf(),
    onTextLayout: (TextLayoutResult) -&gt; Unit = {},
    style: TextStyle = LocalTextStyle.current
)</code></pre>
<p>이전에 내가 생각했던 방식보다 훨~~씬 쉬운 방식으로 해당 채팅화면을 구현을 할 수 있었다.</p>
<p>Text 하나만으로 해당 채팅화면을 구현을 할 수가 있다.</p>
<p>치지직이나 트위치의 경우 </p>
<p>(구독자여부) (아이디) :(치지직의 경우는 이게 없음) Text bla bla (이모티콘) (구독티콘)</p>
<p>이런식으로 텍스트가 있다.</p>
<p>Text에서 그럼 어떤것을 사용해야 하나라고 보면 </p>
<p><strong>text: AnnotatedString,</strong>
<strong>inlineContent: Map&lt;String, InlineTextContent&gt; = mapOf()</strong></p>
<p>이 두 개가 Text안에서도 이미지나 Gif를 그리게 해준다.</p>
<h3 id="annotatedstring은">AnnotatedString은??</h3>
<blockquote>
<ul>
<li>스타일링: AnnotatedString을 사용하여 텍스트의 일부 또는 전체에 서로 다른 서식을 적용할 수 있습니다. 예를 들어, 특정 텍스트를 볼드체로 하거나 색상을 변경하는 등의 서식을 지정할 수 있습니다.</li>
</ul>
</blockquote>
<ul>
<li>링크 및 클릭 이벤트: AnnotatedString을 사용하여 특정 텍스트를 하이퍼링크로 만들고 클릭 이벤트를 처리할 수 있습니다. 이를 통해 사용자가 특정 텍스트를 클릭했을 때 원하는 작업을 수행할 수 있습니다.</li>
<li>커스텀 서식 지정: AnnotatedString은 사용자 정의 서식 태그를 정의하고 이를 사용하여 특정 텍스트 부분에 사용자가 정의한 서식을 적용할 수 있습니다. 이를 통해 특정 텍스트에 사용자 정의 서식을 적용하여 복잡한 텍스트 서식을 구현할 수 있습니다.</li>
</ul>
<p>ChatGPT가 알려준 내용입니다.</p>
<p>코드로 알아보겠습니다 !</p>
<pre><code class="language-kotlin"> val annotatedString = buildAnnotatedString {
        append(&quot;Text에 이미지를 추가하기 !&quot;)
        appendInlineContent(id = &quot;image&quot;)
    }

 val inlineContentMap = mapOf(
      &quot;image&quot; to InlineTextContent(
          Placeholder(40.sp, 40.sp, PlaceholderVerticalAlign.TextCenter)
      ) {
          GlideImage(
              modifier = Modifier.fillMaxSize(),
              model = ContextCompat.getDrawable(context, R.mipmap.doridori),
              contentDescription = &quot;&quot;
          )
      },
  )

  Text(
        modifier = Modifier.height(50.dp),
        text = annotatedString,
        inlineContent = inlineContentMap,
     )</code></pre>
<p>&quot;image&quot; to inlineContentMap image와 일치하는 부분이 있다면 GlideImage를 통해 이미지 작업을 진행시켜줍니다.</p>
<p><img src="https://velog.velcdn.com/images/darby-_-/post/8b18ddc7-0690-4912-9b38-e417639fbcd2/image.gif" alt="" title="생각대로 잘 나왔다"></p>
<p>이번에는 여러개의 이미지를 넣어보겠습니다. 방법은 아까와 똑같습니다.</p>
<pre><code class="language-kotlin">    val annotatedString2 = buildAnnotatedString {
        append(&quot;Text 중앙에 이미지를 삽입해보겠습니다.&quot;)
        appendInlineContent(id = &quot;ryo&quot;)
        append(&quot;GIF를 \n추가하였습니다.&quot;)
        appendInlineContent(id = &quot;image2&quot;)
    }

    val inlineContentMap2 = mapOf(
        &quot;ryo&quot; to InlineTextContent(
            Placeholder(40.sp, 40.sp, PlaceholderVerticalAlign.TextCenter)
        ) {
            GlideImage(
                modifier = Modifier.fillMaxSize(),
                model = ContextCompat.getDrawable(context, R.mipmap.ryo),
                contentDescription = &quot;&quot;
            )
        },

        &quot;image2&quot; to InlineTextContent(
            Placeholder(40.sp, 40.sp, PlaceholderVerticalAlign.TextCenter)
        ){
            GlideImage(
                modifier = Modifier.fillMaxSize(),
                model = ContextCompat.getDrawable(context, R.mipmap.iiyo),
                contentDescription = &quot;&quot;
            )
        }

    )

    Text(
        modifier = Modifier.height(50.dp),
        text = annotatedString2,
        inlineContent = inlineContentMap2
    )
</code></pre>
<p><img src="https://velog.velcdn.com/images/darby-_-/post/58db573e-d36d-4489-8c42-e4f7c03c361a/image.gif" alt=""></p>
<p>Text의 sp의 사이즈보다 이미지가 살짝커서 텍스트가 살짝 잘렸지만 보통이라면 Text사이즈랑 image 사이즈랑 같게해주면 안잘린다.</p>
<p>skydoves님의 트위치 클론 코딩을 보면 맵핑을 하는 부분만 살짝 보겠습니다.</p>
<pre><code class="language-kotlin">
@Composable
fun TwitchMessageItem(
  messageItemState: MessageItemState,
  modifier: Modifier = Modifier
) {
  val badges = messageItemState.message.user.badges
  val badgePath = &quot;${AssetUtils.baseUrl}/badges/&quot;

  val badgesContent = badges.associateWith { badgeName -&gt;
    InlineTextContent(
      Placeholder(20.sp, 20.sp, PlaceholderVerticalAlign.TextCenter)
    ) {
      CoilImage(
        modifier = Modifier
          .fillMaxSize()
          .padding(2.dp),
        imageModel = { &quot;$badgePath/$badgeName&quot; }
      )
    }
  }
  val userSpannable = buildUserSpannableText(messageItemState = messageItemState)
  val (inline, messageText) = messageItemState.message.text.trim().transformText()
  val previewText = userSpannable + messageText

  Text(
    modifier = modifier.padding(vertical = 2.dp),
    text = previewText,
    color = ChatTheme.colors.textHighEmphasis,
    inlineContent = badgesContent + inline
  )
}

private fun buildUserSpannableText(messageItemState: MessageItemState): AnnotatedString {
  val message = messageItemState.message
  val user = messageItemState.message.user
  val badges = user.badges
  val userColor = user.nameColor

  return buildAnnotatedString {
    badges.forEach { badge -&gt;
      appendInlineContent(id = badge)
    }

    if (badges.isNotEmpty()) {
      append(&quot; &quot;)
    }

    val nameStart = this.length

    append(message.user.name)

    val nameEnd = this.length

    addStyle(
      style = SpanStyle(
        color = Color(android.graphics.Color.parseColor(userColor)),
        fontWeight = FontWeight.SemiBold
      ),
      start = nameStart,
      end = nameEnd
    )

    append(&quot;: &quot;)
  }
}</code></pre>
<pre><code class="language-kotlin">
fun String.transformText(): Pair&lt;Map&lt;String, InlineTextContent&gt;, AnnotatedString&gt; {
  val emotes = AssetsProvider.reactions
  val emoteKeys = emotes.keys
  val parts = this.split(&quot; &quot;)
  val addedEmotes = parts.filter { it in emotes }

  val inlineContent = addedEmotes.associateWith { badgeName -&gt;
    InlineTextContent(
      Placeholder(20.sp, 20.sp, PlaceholderVerticalAlign.TextCenter)
    ) {
      CoilImage(
        modifier = Modifier
          .fillMaxSize()
          .padding(2.dp),
        imageModel = { AssetUtils.getEmotePath(emotes[badgeName]) }
      )
    }
  }

  val annotatedString = buildAnnotatedString {
    for (part in parts) {
      if (part in emoteKeys) {
        appendInlineContent(id = part)
      } else {
        append(part)
      }

      if (parts.indexOf(part) != parts.lastIndex) {
        append(&quot; &quot;)
      }
    }
  }

  return inlineContent to annotatedString
}</code></pre>
<p>associateWith를 통해 &lt;K,V&gt;로 Return을 받은 <strong>badgesContent</strong>(구독 또는 아이디 앞에오는 것)와 <strong>inline</strong>(이모티콘류 채팅에 써질 것)을 inlineContent에 담고 Text전문을  </p>
<pre><code>val previewText = userSpannable + messageText</code></pre><p>합친 후에</p>
<pre><code class="language-kotlin">  Text(
    modifier = modifier.padding(vertical = 2.dp),
    text = previewText,
    color = ChatTheme.colors.textHighEmphasis,
    inlineContent = badgesContent + inline
  )</code></pre>
<p>이런식으로 Text를 만들어 내면 됩니다.</p>
<p>이걸 이제 LazyColumn과 같은 Recyclerview의 역할을 하는 곳에 담은 뒤</p>
<p>list된 item들을 뿌려주면 됩니다.</p>
<p>아마 실제 서비스를 한다면 아마도 
<strong>inlineContent</strong>이 부분을 API 통신을 통해 list들을 가져오지 않을까 싶습니다.</p>
<p>맵핑을 어떻게 할 것인지는 개발자 마음입니다. <a href="https://github.com/skydoves">skydoves님</a>처럼 associateWith로 Map&lt;K,V&gt;식으로 깔끔하게 리턴받는 형식이 괜찮은 것 같습니다.</p>
<p><a href="https://seosh817.tistory.com/222">Kotlin Collections API에 대해 한 번 알아보고 원하는 걸 쓰시면 될 것 같습니다!</a></p>
</br>
</br>
</br>
</br>
이렇게 찾다보니 쉬운방법이 있는데 나는 엄청 돌아가는 것 같다. 예전부터 채팅의 구현이 엄청 궁금했었는데 생각보다 쉽게 풀어서 뭔가 허무한것 같기도 하지만 쉬운방법이 있는데 굳이 어려운길로 갈 필요가 없으니까 좋은 경험이었다. 

<p>빨리 찾아볼걸 ~ 근데 뭐라 검색해야 할지도 감이 안와서 잘 못찾았었다.. 혹시라도 더 좋은 방법으로 채팅창 구현을 하셨다면 알려주시면 감사하겠습니다 !! </p>
<hr>
<p>참고
<a href="https://github.com/skydoves/twitch-clone-compose">https://github.com/skydoves/twitch-clone-compose</a> (트위치 클론 코딩 message쪽을 보면서 분석을 했는데 도움이 많이되었습니다.)
<a href="https://velog.io/@heetaeheo/Android-AnnotatedString">https://velog.io/@heetaeheo/Android-AnnotatedString</a>
<a href="https://developer.android.com/reference/kotlin/androidx/compose/ui/text/AnnotatedString">https://developer.android.com/reference/kotlin/androidx/compose/ui/text/AnnotatedString</a>
<a href="https://seosh817.tistory.com/222">https://seosh817.tistory.com/222</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[LiveData -> Flow로 변환해보기]]></title>
            <link>https://velog.io/@darby-_-/LiveData-Flow%EB%A1%9C-%EB%B3%80%ED%99%98%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@darby-_-/LiveData-Flow%EB%A1%9C-%EB%B3%80%ED%99%98%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Tue, 27 Feb 2024 18:15:54 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>기존에 개발했던 메이플스토리 검색앱에서 사용하던 LiveData를 Flow로 변환하면서 알게된 내용입니다. 틀린 부분이 있으면 알려주세요!</p>
</blockquote>
<h4 id="livedata의-문제점">LiveData의 문제점</h4>
<p>처음 MVVM구조를 만들면서 ViewModel을 만들고 그거를 이제 observe하면서 데이터가 바뀐 것을 읽는 형식으로 만들었습니다.</p>
<p>LiveData의 장점</p>
<ul>
<li>LiveData는 수명 주기를 자동으로 인식하고, 활성 상태인 관찰자만 업데이트를 수신합니다. 이를 통해 메모리 누수나 UI 갱신 오류를 방지할 수 있습니다.</li>
<li>데이터가 변경될 때마다 관찰자에게 알림을 제공하여 UI를 업데이트할 수 있습니다. 이는 데이터의 실시간 업데이트를 지원하며, 사용자 경험을 향상시킵니다.</li>
<li>LiveData는 구성 변경(예: 화면 회전)에 대한 데이터 손실을 방지하고 앱 상태를 보존하는 데 도움이 됩니다.</li>
</ul>
<img src="https://velog.velcdn.com/images/darby-_-/post/3714a463-7326-4e37-a2b0-a43c45df53c8/image.png" width="80%" style="display: block; margin-left: auto; margin-right: auto;">

<p>보통 이런식으로 계층구조를 만들어서 사용을 하는데 LiveData 같은 경우 Domain Layer에서 사용하는걸 추천하지 않습니다. 보통은 Flow로 하는걸 추천을 하더라구요. 이유는 Domain Layer는 Java와 Kotlin 코드로만 구성하는 것이 좋으며 LiveData를 사용하게 되면 그 순간부터 Android에 의존적일수 있기에 단위테스트를 따로해야하는 경우가 생길 수 있습니다. </p>
<p><a href="https://developer.android.com/topic/libraries/architecture/livedata?hl=ko#livedata-in-architecture">구글 공식 문서</a>에서도 Flow로 만든 다음 asLiveData()로 변환해서 사용하는걸로 작성이 되어있습니다.</p>
<blockquote>
<p>데이터 영역 클래스에서 LiveData 객체를 작업하고 싶을 수 있지만 <strong>LiveData는 비동기 데이터 스트림을 처리하도록 설계되지 않았습니다.</strong> LiveData 변환과 MediatorLiveData를 사용하여 LiveData 객체 작업을 할 수는 있지만 이 접근 방식에는 단점이 있습니다. 즉, 데이터 스트림을 결합하는 기능이 매우 제한적이고 변환을 통해 만들어진 객체를 포함하여 모든 LiveData 객체가 기본 스레드에서 관찰됩니다.</p>
</blockquote>
<p>간단히 말하면 LiveData는 비동기에서 쓰기에 적절하지 않다. </p>
<p>그래서 저는 제가 개발했던 앱을 LiveData로 되어있던 Domain Layer 계층을 Flow로 바꿀 생각을 하고 있었습니다. 그래서 Coroutine에 대해 알아보고 Flow에 대해 알아보던 중 <strong>StateFlow</strong>에 대해 알게 되었습니다. 그 전에 Flow에 대해 간단하게 알아보고 가겠습니다.</p>
<h2 id="flow가-뭔가요">Flow가 뭔가요??</h2>
<p>Flow는 말 그대로 데이터의 흐름들 속에서 데이터를 처리, 변환 할 수 있으며 비동기 데이터 스트림을 처리할 수 있습니다. </p>
<p>기본적으로 데이터를 생산(emit)하고 소비(collect)하는데 사용됩니다.</p>
<h4 id="특징">특징</h4>
<ul>
<li>비동기성(Asynchronous): Coroutine Flow는 비동기적으로 데이터를 처리하며, 데이터가 생산되는 즉시 소비될 수 있습니다.</li>
<li>확장성(Scalability): 여러 개의 연산자(operator)를 조합하여 데이터를 변환하고 처리할 수 있으며, 비동기적으로 실행됩니다.</li>
<li>취소 가능성(Cancellable): 코루틴의 취소 기능을 활용하여 데이터의 생산과 소비를 취소할 수 있습니다.</li>
<li>배압 제어(Backpressure): Flow의 생산자와 소비자 간의 속도 차이를 조절하여 메모리 부하를 방지하고 안정적인 데이터 흐름을 유지할 수 있습니다.</li>
</ul>
<p>Flow는 기본적으로 Cold Stream 입니다.</p>
<p><a href="https://medium.com/@apfhdznzl/flow%EC%99%80-channel-cold-stream%EA%B3%BC-hot-stream-c42c64cf4996"><strong>Cold Stream이 뭔데?</strong></a></p>
<h4 id="flow-단점">Flow 단점</h4>
<ul>
<li>Flow는 안드로이드 생명주기에 대해 알 수 없습니다.</li>
<li>생명주기에 대해 알 수가 없다 보니 화면 회전, 화면 이동 등의 이벤트로 인해 화면이 재생성될 때마다 새로운 구독자가 등록될 수 있습니다. 이 경우, 이전 구독자가 여전히 활성화되어 있을 수 있으므로 메모리 누수가 발생할 수 있습니다.</li>
<li>Flow는 Cold Stream의 한 형태이기 때문에 새로운 구독자가 등록될 때마다 데이터를 처음부터 다시 생성합니다. 이로 인해 데이터가 불필요하게 캐싱되거나 중복으로 처리되는 문제가 발생할 수 있습니다.</li>
</ul>
<p>이러한 문제점이 있었습니다. 그래서 Flow로 받아서 asLiveData()로 변환해서 LiveData로 사용하고 있었습니다.</p>
<h3 id="stateflow의-등장">StateFlow의 등장</h3>
<p>StateFlow는 SharedFlow 의 한 종류이며 Hot Stream의 성격을 가지고 있습니다.</p>
<p>사용하다보면 LiveData와 비슷하다고 느꼈는데 가장 큰 차이점은 <strong>StateFlow는 기본 값을 가지고 있다</strong>인 것 같습니다.</p>
<h4 id="stateflow-특징">StateFlow 특징</h4>
<ul>
<li>항상 값을 가지고 있고, 오직 한 가지 값을 가집니다. 그렇기 때문에 생성할 때 기본값을 넣어줘야합니다.</li>
<li>StateFlow는 Hot Stream의 한 형태이므로 여러 구독자가 공유된 데이터 스트림을 동시에 구독할 수 있습니다.</li>
<li>StateFlow는 불변성 상태 값을 유지하므로 데이터의 변경을 실시간으로 반영할 수 있습니다.</li>
</ul>
<p>이러한 특징들로 인해 LiveData를 대체할 수 있습니다.</p>
<p>제가 사용했던 코드를 보면서 마무리 하겠습니다.</p>
<pre><code>MainViewModel.kt

@HiltViewModel
class MainViewModel @Inject constructor(private val userRepository: UserRepository) : ViewModel() {
    //기본값을 넣어줘야한다.
    private val _userData = MutableStateFlow(ResultVO())

    val userData: StateFlow&lt;ResultVO&gt; get() = _userData

    private val _errorLiveData = MutableStateFlow(&quot;&quot;)
    val errorLiveData: StateFlow&lt;String&gt; get() = _errorLiveData
    fun clearErrorData() {
        _errorLiveData.value = &quot;&quot;
    }


    fun getUserData(id: String) {
        viewModelScope.launch {
            try {
                //가져온 데이터를 발행(emit)
                _userData.emit(userRepository.getUserData(id))
                clearErrorData()
            } catch (e: Exception) {
                _errorLiveData.emit(e.message.toString())
            }
        }
    }
}</code></pre><pre><code>MainView.kt

@Composable
fun ParallaxEffect(
    navigateBack: () -&gt; Unit,
    id: String?,
) {
    ...

    val viewModel: MainViewModel = hiltViewModel()
    val likeCharacterViewModel: LikeCharacterViewModel = hiltViewModel()
    val recentSearchViewModel: RecentSearchViewModel = hiltViewModel()

    val userData by viewModel.userData.collectAsState()
    val errorData by viewModel.errorLiveData.collectAsState()

    ...

    if (errorData.isNotEmpty()) {
           SearchFailedDialog(
           errorData = errorData.toString(),
           onDismiss = { viewModel.clearErrorData() })
      }

      MainAvatar(userData = userData)

}</code></pre><p>사용방법은 LiveData와 거의 유사합니다.</p>
<hr>
<p>참고</p>
<p><a href="https://developer.android.com/topic/architecture?hl=ko">https://developer.android.com/topic/architecture?hl=ko</a>
<a href="https://medium.com/@apfhdznzl/flow%EC%99%80-channel-cold-stream%EA%B3%BC-hot-stream-c42c64cf4996">https://medium.com/@apfhdznzl/flow%EC%99%80-channel-cold-stream%EA%B3%BC-hot-stream-c42c64cf4996</a>
<a href="https://www.youtube.com/watch?v=6z3QjG7vLkE&amp;ab_channel=%EA%B0%9C%EB%B0%9C%ED%95%98%EB%8A%94%EC%A0%95%EB%8C%80%EB%A6%AC">https://www.youtube.com/watch?v=6z3QjG7vLkE&amp;ab_channel=%EA%B0%9C%EB%B0%9C%ED%95%98%EB%8A%94%EC%A0%95%EB%8C%80%EB%A6%AC</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Flutter 찍먹]]></title>
            <link>https://velog.io/@darby-_-/Flutter-%EC%B0%8D%EB%A8%B9</link>
            <guid>https://velog.io/@darby-_-/Flutter-%EC%B0%8D%EB%A8%B9</guid>
            <pubDate>Thu, 15 Feb 2024 15:26:53 GMT</pubDate>
            <description><![CDATA[<p>최근에 Flutter를 한 번 해봤다..</p>
<p>2018년도? 쯤에 한 번 해봤었는데 이해가 잘 가지 않아서 손을 놨었다.</p>
<p>최근에 코틀린으로 Android를 개발하다가 iOS도 개발해보고 싶은 마음은 있는데,</p>
<p>뭔가 맥북 사기가 좀 그래서 고민만 하던 찰나 Nomad에서 클론코딩을 보면서 따라서 해보고 있는데 이게 생각보다 재밌다.</p>
<p><strong>이론 -&gt; 실전</strong> 보다 <strong>실전 -&gt; 이론</strong>을 더 좋아한다. 
(일단 화면에 돌아가게 만들어놓고 이게 어떻게 돌아가는 방식인지 확인하는 경우가 많다.)</p>
<p>그러다보니 잘 따라가게 된 것같다. </p>
<p>매일 코딩이 과제가 있는건 아니고 종종 있는데 
새로운 언어로 개발을 하다보니 신선한 느낌??을 받았다. </p>
<h2 id="후기">후기</h2>
<hr>
<p>처음은 UI에 대한 과제이고 그다음은 Timer를 이용한 간단한 뽀모도로 앱을 개발하는 것이였다.</p>
<div style="display: flex;">
  <img src="https://velog.velcdn.com/images/darby-_-/post/4d6bcb3f-a255-4bdc-b489-ebb61b8a2709/image.gif"  width="33%" >
  <img src="https://velog.velcdn.com/images/darby-_-/post/d562b2c8-17d1-4214-9257-f5ec3188674b/image.gif"  width="33%" >
</div>


<p>사실 Flutter를 개발하면서 궁금한게 많아서 이것저것 인터넷을 많이 참고했다.. 과제랑 상관이 없는 것들이 대부분이다.</p>
<p>메모리 관리나 Thread는 어떻게 관리하는지 궁금한게 있긴했는데 아직 그 정도의 레벨은 아니라서 lifeCycle 정도만 찾아봤다.</p>
<p>Android에서 lifeCycle하고 조금 다르다는걸 알았다.. </p>
<p>약간 Compose와 비슷한 느낌이 있다.</p>
<p>이번주까지가 일정이라 한 번 다 배워보고 정리를 하는 글을 올릴 것 같다. </p>
<p>간단한 앱을 만들 때는 괜찮은 것 같다.. 굳이 Kotlin 쓰고 swift 써가며 할 필요가 없는 이유가 있긴 한 것 같다. </p>
<p>그래도 난 아직도 Native가 장점이 있다고 생각한다. + a 느낌으로 배워서 하면 좋을 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Coroutine with Kotlin ]]></title>
            <link>https://velog.io/@darby-_-/Coroutine-with-Kotlin</link>
            <guid>https://velog.io/@darby-_-/Coroutine-with-Kotlin</guid>
            <pubDate>Mon, 12 Feb 2024 16:18:55 GMT</pubDate>
            <description><![CDATA[<p>코루틴에 대해 공부한 내용을 적은 글입니다. 틀린 부분이 있을 수도 있습니다.</p>
<h2 id="coroutine이-뭘까">Coroutine이 뭘까?</h2>
<p>Coroutine에 대해 알기전에 routine이 뭔지를 알면 좋습니다.</p>
<h3 id="routine이란">routine이란?</h3>
<blockquote>
<p>컴퓨터 프로그램의 일부로서, 특정한 일을 실행하기 위한 일련의 명령.</p>
</blockquote>
<p>위키피디아에 등재된 내용입니다.</p>
<p>한마디로 루틴은 어떠한 일을 하기 위한 프로그램?? 정도로 이해하고 넘어가면 좋을 것 같습니다.</p>
<p>그리고 코루틴을 사용하기 전에는 우리는 서브루틴이라는 방식으로 개발을 하였습니다.</p>
<pre><code>fun routine() {
    routine1()
    routine2()
}

fun routine1(){
    print(&quot;Hello&quot;)
}

fun routine2(){
    print(&quot;안녕하세요&quot;)
}</code></pre><p align="center" style="color:gray">
  <img src="https://velog.velcdn.com/images/darby-_-/post/c21b9400-27fc-490c-95aa-f42063b9e0aa/image.png" style="padding: 0;margin:0;" width="80%">
</p>

<p>routine 함수는 routine1의 &quot;Hello&quot;를 print하고 다시 메인 루틴이 호출한 장소로 돌아와 다음 루틴인 routine2 &quot;안녕하세요&quot;를 print하고 다시 메인 루틴이 호출한 장소로 돌아와 다음 루틴을 실행한다.</p>
<p><strong>그러면 Coroutine은 무엇일까?</strong></p>
<blockquote>
<p>Coroutine은 <strong>일시 중단과 재개가 가능한 루틴</strong>이며 특정 Thread에 결합되어 있지 않으며 하나의 Thread에서 중단하고 다른 Thread에서 재개가 가능합니다.</p>
</blockquote>
<p align="center" style="color:gray">
   <img src="https://velog.velcdn.com/images/darby-_-/post/c1b92ec9-3766-4e22-94a5-718db399e9c0/image.gif" style="padding: 0;margin:0;" width="80%">
  출처 : https://gist.github.com/chaxiu/d87870528bbfe3e9d7e481e1f6acace1
</p>

<p>위와 같이 Main Thread는 log를 찍어주고 네트워킹 작업같이 오래걸리는 작업은 IO Thread가 하고 있다.</p>
<p><a href="https://youtu.be/x0KY6qtuNHg?t=200">중단(suspend) 어떻게 작동할까 ?</a>
해당영상을 간략히 요약하면
suspension point라는게 있는데 이것은 게임의 check-point의 기능과 비슷합니다.
<strong>suspended 상태에서는 어떠한 자원도 사용하지 않음(Coroutine이 효율적인 이유)</strong></p>
<blockquote>
<p>기존의 Thread에서는 block을 걸면 자원을 사용하지 못하게 점유를 하고 있지만 Coroutine은 그렇지 않다. 또한 서로 다른 Thread에서 suspend와 resume이 가능하다.</p>
</blockquote>
<p>suspent funtion이라고 모두가 중단 가능한 함수는 아니다.</p>
<h2 id="coroutinecontext">CoroutineContext</h2>
<blockquote>
<p>코루틴이 실행될 때 실행 환경을 정의하고 구성합니다. (CoroutineContext = Coroutine 실행환경)</p>
</blockquote>
<ul>
<li>Job, CoroutineName, CoroutineDisPatcher, CoroutineExceptionHandler 등이 있다.</li>
<li>모든 CoroutineScope와 Suspend function을 가지고 있음</li>
<li>CoroutineContext가 포함하는 모든 element는 unique key를 가진다. (중복허용 X)</li>
</ul>
<p>** 모든 suspend function에서는 CoroutineContext에 접근이 가능하다. **</p>
<ol>
<li>suspend function은 컴파일 시 Continuation을 파라미터로 추가해줍니다.</li>
<li>Continuation은 CoroutineContext를 가집니다.</li>
<li>모든 suspend function은 current coroutine의 Context에 접근이 가능합니다.</li>
</ol>
<h2 id="coroutinescope">CoroutineScope</h2>
<ul>
<li>child는 parent를 상속합니다.</li>
<li>parent는 모든 children이 끝날 때 까지 기다립니다.</li>
<li>parent가 취소되면, children도 취소됩니다.</li>
<li>child에서 에러가 발생하면 parent에게도 전파가 됩니다.</li>
<li>launch(), async()는 CoroutineScope의 확장 함수입니다.</li>
<li>runBlocking()은 오직 root coroutione으로 사용이 됩니다. (CoroutineScope의 확장 함수 X)</li>
</ul>
<p>CoroutineScope도 CoroutineContext를 가지고 있습니다.</p>
<pre><code>public interface CoroutineScope {
    /**
     * The context of this scope.
     * Context is encapsulated by the scope and used for implementation of coroutine builders that are extensions on the scope.
     * Accessing this property in general code is not recommended for any purposes except accessing the [Job] instance for advanced usages.
     *
     * By convention, should contain an instance of a [job][Job] to enforce structured concurrency.
     */
    public val coroutineContext: CoroutineContext
}
</code></pre></br>
</br>
</br>
</br>
</br>
다음에는 Android에서는 어떻게 사용이 되고 있는지 알아보겠습니다.

<p><a href="https://www.youtube.com/watch?v=x0KY6qtuNHg&amp;ab_channel=DroidKnights">드로이드나이츠</a>에 있는 영상을 정리한 글인데요. 다른 영상은 안보셔도 Android 개발을 하신다면 한 번쯤 보시는걸 추천드립니다. </p>
<p>감사합니다.</p>
<hr>
<p>참고자료</p>
<p><a href="https://www.youtube.com/watch?v=x0KY6qtuNHg&amp;ab_channel=DroidKnights">https://www.youtube.com/watch?v=x0KY6qtuNHg&amp;ab_channel=DroidKnights</a>
<a href="https://gist.github.com/chaxiu/d87870528bbfe3e9d7e481e1f6acace1">https://gist.github.com/chaxiu/d87870528bbfe3e9d7e481e1f6acace1</a>
<a href="https://dev.gmarket.com/82">https://dev.gmarket.com/82</a>
<a href="https://developer.android.com/kotlin/coroutines?hl=ko">https://developer.android.com/kotlin/coroutines?hl=ko</a>
<a href="https://www.youtube.com/watch?v=eJF60hcz3EU&amp;ab_channel=%EB%8B%B9%EA%B7%BC%ED%85%8C%ED%81%AC">https://www.youtube.com/watch?v=eJF60hcz3EU&amp;ab_channel=%EB%8B%B9%EA%B7%BC%ED%85%8C%ED%81%AC</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Hilt 기초]]></title>
            <link>https://velog.io/@darby-_-/%EC%95%B1-%EA%B0%9C%EB%B0%9C-%ED%9B%84%EA%B8%B0-Hilt-%ED%8E%B8</link>
            <guid>https://velog.io/@darby-_-/%EC%95%B1-%EA%B0%9C%EB%B0%9C-%ED%9B%84%EA%B8%B0-Hilt-%ED%8E%B8</guid>
            <pubDate>Sun, 28 Jan 2024 11:39:11 GMT</pubDate>
            <description><![CDATA[<h2 id="🗡️hilt">🗡️Hilt</h2>
<p>Hilt는 Dagger를 기반으로 한 Android용 의존성 주입(Dependency Injection) 프레임워크로 입니다.
안드로이드 애플리케이션에서 의존성 주입을 쉽게 구현할 수 있도록 도와주는 도구입니다. </p>
<hr>
<h3 id="💉di-dependency-injection">💉DI (Dependency Injection)</h3>
<p>의존성 주입(Dependency Injection, DI)은 소프트웨어 개발에서 객체 간의 의존성을 외부에서 주입하는 디자인 패턴입니다. 이를 통해 코드의 유연성과 테스트 용이성이 향상되며, 코드의 결합도를 낮출 수 있습니다.</p>
<pre><code>class Engine {
    fun start() {
        println(&quot;Engine started&quot;)
    }
}

class Car(private val engine: Engine) {
    fun start() {
        engine.start()
        println(&quot;Car started&quot;)
    }
}

val e = Engine()
val car = Car(e)
car.start()</code></pre><p>위와 같이 생성자로 engine을 받아서 사용하면 됩니다.</p>
<p>저는 처음에는 무슨소리인지 잘 모르겠어서 <strong>&quot;생성자나 인터페이스 같은 것을 통해 클래스간의 결합도를 낮춘다&quot;</strong> 정도로 이해를 하고 넘어갔습니다.</p>
<p>큰 장점은 본인이 생각하기에 2가지가 있는 것 같습니다.</p>
<ul>
<li><p>모듈성 및 재사용성: DI를 통해 코드는 모듈로 나눠질 수 있습니다. 각 모듈은 독립적으로 테스트하고 재사용할 수 있게 되어 코드 베이스를 더 쉽게 관리하고 확장할 수 있습니다.</p>
</li>
<li><p>가독성 및 유지보수성: DI는 코드의 가독성을 향상시킵니다. 의존성이 외부에서 주입되기 때문에 코드의 의도가 더 명확해지며, 이는 향후 유지보수에 큰 도움이 됩니다.</p>
</li>
</ul>
<hr>
<h2 id="hilt-사용해보기">Hilt 사용해보기</h2>
<blockquote>
<p>Hilt는 기존의 Dagger에서 장점을 Android에 LifeCycle에 맞게 Google이 만든 라이브러리입니다. Dagger2 보다 낮은 러닝커브 쉬운 환경설정으로 인해 Android에서 인기가 많은 것 같습니다.</p>
</blockquote>
<p>프로젝트에 Hilt를 적용하는 방법은 <a href="https://developer.android.com/codelabs/android-hilt?hl=ko#2">해당 링크</a>에서 확인해주세요.</p>
<pre><code>class Engine @Inject constructor() {
    fun start() {
        println(&quot;Engine started&quot;)
    }
}

class Car @Inject constructor(private val engine: Engine) {
    fun start() {
        engine.start()
        println(&quot;Car started&quot;)
    }
}
</code></pre><p>의존성이 있는 클래스를 생성</p>
<pre><code>@HiltAndroidApp
class MyApplication : Application()

@Module
@InstallIn(SingletonComponent::class)
object AppModule {
    @Provides
    fun provideEngine(): Engine {
        return Engine()
    }
}
</code></pre><p>기본 Application에 추가를 해줘야합니다.
Application이 없는 경우 만든 후에 manifest에서 등록을 해주세요.</p>
<pre><code>@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var car: Car

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        car.start()
    }
}
</code></pre><p>위의 코드에서 <strong>@HiltAndroidApp</strong> 어노테이션이 지정된 MyApplication 클래스는 Hilt의 초기화를 담당합니다. <strong>@AndroidEntryPoint</strong> 어노테이션이 지정된 MainActivity 클래스에서는 Car 객체를 주입받아 사용합니다.</p>
<p>Hilt는 Android 컴포넌트 (예: Application, Activity, Fragment)에 의존성을 주입하는 데 사용되는 <strong>@AndroidEntryPoint</strong> 어노테이션과 같은 특별한 어노테이션을 제공합니다.</p>
<p>Hilt에 대한 계층구조와 어노테이션에 대한 건 다음 포스팅에 이어서 올리도록 하겠습니다.</p>
<hr>
<h3 id="참고자료">참고자료</h3>
<p><a href="https://developer.android.com/training/dependency-injection/hilt-android?hl=ko">Google Document</a>
<a href="https://medium.com/@myofficework000/dependency-injection-dagger-hilt-in-android-14a7f03050e8">Medium 블로그</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SQLite 대신 Room 사용해보기]]></title>
            <link>https://velog.io/@darby-_-/%EC%95%B1-%EA%B0%9C%EB%B0%9C-%ED%9B%84%EA%B8%B0-3%ED%8E%B8-Room</link>
            <guid>https://velog.io/@darby-_-/%EC%95%B1-%EA%B0%9C%EB%B0%9C-%ED%9B%84%EA%B8%B0-3%ED%8E%B8-Room</guid>
            <pubDate>Tue, 23 Jan 2024 16:10:07 GMT</pubDate>
            <description><![CDATA[<p>이번에 소개할 기술은 Room 입니다.</p>
<p>AAC의 하나인 Room은 기존에 SQLite를 좀 더 쉽게 쓸 수 있게 만든거라고 생각하시면 됩니다.</p>
<p>주요 구성 요소
Room에는 세 가지 주요 구성요소가 있습니다.</p>
<ul>
<li>데이터베이스를 보유하고 앱의 지속형 데이터에 대한 기본 연결을 위한 기본 액세스 지점 역할을 하는 데이터베이스 클래스 입니다.</li>
<li>앱 데이터베이스의 테이블을 나타내는 Data Entity입니다 .</li>
<li>앱이 데이터베이스의 데이터를 쿼리, 업데이트, 삽입 및 삭제하는 데 사용할 수 있는 메서드를 제공하는 데이터 액세스 개체(DAO) 입니다.</li>
</ul>
<p>Google Document에 있는 내용입니다. </p>
<p>Room에서 필요한 3가지 요소를 나타낸 것 입니다.</p>
<p>DataBase, Data Entity, DAO가 있습니다.</p>
<p>간단하게 알아보겠습니다.</p>
<h2 id="1데이터-베이스">1.데이터 베이스</h2>
<pre><code>@Database(
    entities = [LikeCharacterVo::class, RecentSearchVO::class],
    version = 2,
    exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {
    abstract fun likeCharacterDao(): LikeCharacterDao

    abstract fun recentSearchDao(): RecentSearchDao
}</code></pre><p>entities는 간단히 말하면 Table의 역할을 합니다.</p>
<p>version은 변경사항이 생길 때 하나씩 올려주면됩니다. 올리지 않고 빌드를 한다면 에러를 마주칠 것입니다.</p>
<p>exportSchema는 Room의 스키마를 폴더로 내보내도록 지시할 수 있습니다. </p>
<br>

<h2 id="2엔티티">2.엔티티</h2>
<pre><code>@Entity(tableName = &quot;like_table&quot;, indices = [Index(value = [&quot;nickName&quot;], unique = true)])
data class LikeCharacterVo(
    @PrimaryKey(autoGenerate = true)
    val id : Long = 0,
    val nickName: String,
    val imgUrl: String,
    val level : String
)</code></pre><p>이번에 메이플스토리 관련 앱을 만들면서 쓰던 캐릭터 즐겨찾기의 엔티티 구조입니다.</p>
<p>우선 Table Name을 정해주고 indices 속성에 nickName필드를 unique 고유 속성으로 만든 엔티티입니다. 만일 닉네임이 같은 경우 추후 Insert를 하게 되면 에러가 난다거나 덮어쓰기를 한다거나 개발자가 정해준 방식을 취할 것 입니다. 하지만 제가 만든 앱은 메이플에서 닉네임을 생성할 때 중복이 방지되므로 저는 덮어쓰기 방식을 채택했습니다. 나머지 필드들은 자기가 넣어주고 싶은 필드들을 넣어주시면 됩니다. </p>
<pre><code>@PrimaryKey(autoGenerate = true)
    val id : Long = 0</code></pre><p>이 부분은 보통 index로 많이 쓰는 부분입니다. 자동으로 카운트가 됩니다.</p>
<br>

<h2 id="3dao-data-access-objects">3.DAO (Data Access Objects)</h2>
<pre><code>@Dao
interface LikeCharacterDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertLickCharacter(likeCharacterVo: LikeCharacterVo)

    @Query(&quot;SELECT * FROM like_table&quot;)
    fun getAllLikeCharacter(): LiveData&lt;List&lt;LikeCharacterVo&gt;&gt;


    @Query(&quot;DELETE FROM like_table WHERE nickName = :nickName&quot;)
    suspend fun deleteData(nickName: String)

    @Query(&quot;SELECT * FROM like_table WHERE nickName = :name&quot;)
    fun getCharacterByName(name : String): LikeCharacterVo?
}</code></pre><p>즐겨찾기 Dao입니다. </p>
<p>Dao는 테이블에 접근하려고 하는 object? 정도 라고 생각하시면 편할 것 같습니다.</p>
<p>사실 Update도 만들어야 하지만 insert를 할 때 </p>
<pre><code>onConflict = OnConflictStrategy.REPLACE</code></pre><p>Insert에 옵션을 주면 해당 옵션은 중복값이 있을 때 교체를 한다는 뜻입니다.</p>
<p>그래서 저는 따로 Update는 만들지 않았습니다.</p>
<br>

<pre><code>@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
    @Singleton
    @Provides
    fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase {
        return Room.databaseBuilder(
            context,
            AppDatabase::class.java,
            &quot;LikeCharacterDatabase&quot;
        ).build()
    }

    @Provides
    fun provideLikeCharacterDao(database: AppDatabase): LikeCharacterDao {
        return database.likeCharacterDao()
    }

}
</code></pre><p>DB객체는 싱글톤으로 구현하는 걸 추천하고 있습니다. <a href="https://developer.android.com/training/data-storage/room?hl=ko">공식문서</a>를 참조해주세요.</p>
<pre><code>@HiltViewModel
class LikeCharacterViewModel @Inject constructor(private val likeCharacterRepository: LikeCharacterRepository) : ViewModel() {
    private val _characterData = MutableLiveData&lt;List&lt;LikeCharacterVo&gt;&gt;()
    val characterData: LiveData&lt;List&lt;LikeCharacterVo&gt;&gt; get() = _characterData

    fun insertLikeCharacter(name: String, imgUrl: String, level : String) {
        viewModelScope.launch {
            try {
                val likeCharacterVo = LikeCharacterVo(nickName = name, imgUrl = imgUrl, level = level)
                likeCharacterRepository.insertCharacter(likeCharacterVo)
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
    }

    fun deleteLikeCharacter(name: String) {
        viewModelScope.launch {
            try {
                likeCharacterRepository.deleteUser(name)
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
    }

    fun getAllList() {
        likeCharacterRepository.getAllCharacterList().observeForever {
            _characterData.value = it
        }
    }

    suspend fun isExistCharacter(name: String): Boolean {
        return withContext(Dispatchers.IO) {
            likeCharacterRepository.isExistCharacter(name)
        }
    }

}</code></pre><p>그리고 viewModel로 만들어서 원하는 곳에서 가져다 쓰면 됩니다. </p>
<p><a href="https://medium.com/androiddevelopers/7-pro-tips-for-room-fbadea4bfbd1">해당 링크</a>를 통해 Room에 대해 더 자세히 알아보시면 좋을 것 같습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Android Compose Navigation ]]></title>
            <link>https://velog.io/@darby-_-/Android-Compose-Navigation</link>
            <guid>https://velog.io/@darby-_-/Android-Compose-Navigation</guid>
            <pubDate>Thu, 18 Jan 2024 11:55:44 GMT</pubDate>
            <description><![CDATA[<p>이번 앱을 개발하면서 Compose로 Ui를 개발했습니다.</p>
<p>예전에는 XML을 사용하면서 Navigation을 사용했었는데 역시 페이지 이동은 Navigation !</p>
<p>기존에는 개발을 할 때 <a href="https://www.youtube.com/watch?v=2k8x8V77CrU&amp;ab_channel=AndroidDevelopers">SAA(Single Activity Architecture)</a>로 개발을 했었는데 그러다 보니 단일 또는 소수의 Activity와 나머지 페이지들은 Fragment로 개발을 했었습니다.</p>
<p>그래서 Navigation으로 개발을 했었는데 Compose로 개발을 하다보니 Fragment를 사용하지 않다보니 화면이동은 어떻게 하지?! 생각을 했었는데 역시 화면이동은 Navigation으로 그대로 하고있었습니다.</p>
<p><a href="https://developer.android.com/jetpack/compose/navigation?hl=ko">Navigation Compose</a></p>
<p>기존에는 Navigation은 nav_graph.xml에 아래와 같은 그림 처럼 그래프만 그어주면 됩니다. 
참 쉽게 만들어 놓았습니다.</p>
<p><img src="https://velog.velcdn.com/images/darby-_-/post/938edb9f-0549-4284-90e5-b44e5eb7d79b/image.png" alt=""></p>
<pre><code>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;navigation xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
            xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
            xmlns:tools=&quot;http://schemas.android.com/tools&quot;
            android:id=&quot;@+id/nav_graph&quot;
            app:startDestination=&quot;@id/a&quot;&gt;

    &lt;fragment android:id=&quot;@+id/a&quot;
              android:name=&quot;com.example.myapplication.FragmentA&quot;
              android:label=&quot;a&quot;
              tools:layout=&quot;@layout/a&quot;&gt;
        &lt;action android:id=&quot;@+id/action_a_to_b&quot;
                app:destination=&quot;@id/b&quot;
                app:enterAnim=&quot;@anim/nav_default_enter_anim&quot;
                app:exitAnim=&quot;@anim/nav_default_exit_anim&quot;
                app:popEnterAnim=&quot;@anim/nav_default_pop_enter_anim&quot;
                app:popExitAnim=&quot;@anim/nav_default_pop_exit_anim&quot;/&gt;
    &lt;/fragment&gt;

    &lt;fragment android:id=&quot;@+id/b&quot;
              android:name=&quot;com.example.myapplication.FragmentB&quot;
              android:label=&quot;b&quot;
              tools:layout=&quot;@layout/b&quot;&gt;
        &lt;action android:id=&quot;@+id/action_b_to_a&quot;
                app:destination=&quot;@id/a&quot;
                app:enterAnim=&quot;@anim/nav_default_enter_anim&quot;
                app:exitAnim=&quot;@anim/nav_default_exit_anim&quot;
                app:popEnterAnim=&quot;@anim/nav_default_pop_enter_anim&quot;
                app:popExitAnim=&quot;@anim/nav_default_pop_exit_anim&quot;
                app:popUpTo=&quot;@+id/a&quot;
                app:popUpToInclusive=&quot;true&quot;/&gt;
    &lt;/fragment&gt;
&lt;/navigation&gt;</code></pre><p>이런식으로 xml 코드가 작성이 되어있고 action Id만 가져다 쓰면 됩니다.</p>
<p>하지만 Compose는 xml이 없기 때문에 본인이 직접 작성을 해야하는것 같다.. 원래는 fragment 생성해주고 드래그 쓰윽 ~ 하면 알아서 코드 만들어줬었는데 ㅠㅠ</p>
<pre><code>@Composable
fun Navigation() {
    val navController = rememberNavController()

    NavHost(navController = navController, startDestination = Screen.MainScreen.route) {
        composable(route = Screen.MainScreen.route) {
            MainView(navController = navController)
        }

        composable(
            route = Screen.SearchScreen.route,
            arguments = listOf(navArgument(name = &quot;searchId&quot;) {
                type = NavType.StringType
            })
        ) {
            ParallaxEffect(
                navigateBack = {
                    navController.popBackStack()
                },
                id = it.arguments?.getString(&quot;searchId&quot;, &quot;&quot;),
            )
        }

        composable(route = Screen.LikeScreen.route) {
            LikeCharacterView(navController = navController, navigateBack = {
                navController.popBackStack()
            })
        }

        composable(route = Screen.RecentSearchScreen.route) {
            RecentSearchView(
                navController = navController,
                navigateBack = { navController.popBackStack() }
            )
        }
    }
}</code></pre><p>해당 코드는 실제 제가 만든 앱 Navigation 코드입니다.</p>
<p>Navgation을 구성하는 요소</p>
<ol>
<li><p>NavGraph:
앱의 전체 네비게이션 구조를 정의합니다.
NavGraph는 하나 이상의 Destination으로 구성되며, 각 Destination은 화면이나 일부 화면을 나타냅니다.
NavGraph에 포함된 각 화면이나 대상을 나타냅니다.
Fragment, Activity, 또는 Dialog와 같은 화면을 Destination으로 사용할 수 있습니다.</p>
</li>
<li><p>NavController:
현재의 Destination을 추적하고, 목적지 간의 이동을 관리합니다.
NavController는 UI 구성 요소와 상호 작용하여 목적지 간의 이동을 쉽게 처리할 수 있게 합니다.</p>
</li>
<li><p>NavHost:
NavController를 포함하는 UI 요소로, NavGraph의 일부 목적지를 표시하는 곳입니다.
일반적으로 Fragment 또는 Activity에 내장되어 사용됩니다.</p>
</li>
</ol>
<p>보통 이정도만 알면 기본적인 Navigation을 사용하면 됩니다.</p>
<pre><code>composable(route = Screen.MainScreen.route) {
            MainView(navController = navController)
        }</code></pre><p>하지만 Compose에서는 따로 XML같은 View가 없기 때문에 이게 NavGarph다! 라고 생각하시면 될 것 같습니다.</p>
<p>Compose에서 Navigation을 사용할 때 Screen들을 정의할 필요가 있습니다. </p>
<p><em>To link the items in a bottom navigation bar to routes in your navigation graph, it is recommended to define a sealed class, such as Screen seen here, that contains the route and String resource ID for the destinations.</em></p>
<p>Android Compose Navigation Developer에서 나온 말 인데요. Screen을 정의할 때는 Sealed Class를 사용하는 것을 추천한다고 합니다.</p>
<p>그 이유는 <a href="https://stackoverflow.com/questions/69686087/why-use-sealed-class-and-make-object-in-navigation-kotlin-jetpack-compose">stackoverflow</a>에서 본 글을 참조해주세요.</p>
<p>제가 이해한 바로는 인수가 있는 경우는 Sealed Class로 해놓은 경우가 좋은 방법이라고 합니다.</p>
<p>인수가 없다면 사실 안해도 되는 것 같아요. 다른 외국 유튜브를 봤는데 Sealed Class로 만든 사람들은 잘 없더라구요.. 기초적인 Navigation 방법을 만들어줘서 그런 것 같습니다.</p>
<p>저는 인수로 쓸 경우가 있어서 Sealed Class로 만들어 줬습니다.</p>
<pre><code>sealed class Screen(val route: String) {
    object MainScreen : Screen(&quot;main_screen&quot;)
    object SearchScreen : Screen(&quot;search_screen/{searchId}&quot;) {
        fun searchCharacter(searchId: String) = &quot;search_screen/$searchId&quot;
    }

    object LikeScreen : Screen(&quot;like_screen&quot;)

    object RecentSearchScreen : Screen(&quot;recentSearch_screen&quot;)

}</code></pre><p>아무래도 장점은 저런식으로 </p>
<pre><code>fun searchCharacter(searchId: String) = &quot;search_screen/$searchId&quot;</code></pre><p>이런 식으로 쓸 수 있다는 게 장점인것 같습니다. 추가, 삭제가 편한 것 같아요.</p>
<p>그래서 Screen들을 정의를 해주고 </p>
<pre><code>composable(
            route = Screen.SearchScreen.route,
            arguments = listOf(navArgument(name = &quot;searchId&quot;) {
                type = NavType.StringType
            })
        ) {
            ParallaxEffect(
                navigateBack = {
                    navController.popBackStack()
                },
                id = it.arguments?.getString(&quot;searchId&quot;, &quot;&quot;),
            )
        }</code></pre><p>이런식으로 arguments를 넘겨줄 수 있습니다. listOf로 만든 이유는 인자를 여러개를 넘겨줄 때 listOf로 여러개 넣을 수 있습니다.</p>
<pre><code>class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaSearchTheme {
                Navigation()
            }
        }
    }
}</code></pre><p>그리고 마지막으로 MainActivity에서 이렇게 정의를 해주시면 됩니다.</p>
<p>글쓰기가 많이 부족한데 이 글을 읽어주신 분들에게 감사합니다 !
</br>
뭔가 기존에 쓰던 Navigation과 같으면서 약간씩 다른 Compose Navigation 기존에 쓰던 분들은 쉽게 배울 것 같습니다. 그리고 기존에 Navigation처럼 이동 할 때 <a href="https://stackoverflow.com/questions/65643015/animating-between-composables-in-navigation-with-compose">animation</a>을 넣어 줄 수도 있습니다.</p>
<hr>
<p>참고자료</p>
<p><a href="https://developer.android.com/jetpack/compose/navigation">Google Document</a>
<a href="https://stackoverflow.com/questions/69686087/why-use-sealed-class-and-make-object-in-navigation-kotlin-jetpack-compose">StackOverFlow</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MVVM 적용하기]]></title>
            <link>https://velog.io/@darby-_-/%EC%95%B1-%EA%B0%9C%EB%B0%9C-%ED%9B%84%EA%B8%B0-MVVM-%ED%8E%B8</link>
            <guid>https://velog.io/@darby-_-/%EC%95%B1-%EA%B0%9C%EB%B0%9C-%ED%9B%84%EA%B8%B0-MVVM-%ED%8E%B8</guid>
            <pubDate>Tue, 16 Jan 2024 10:55:25 GMT</pubDate>
            <description><![CDATA[<h2 id="mvvmmodel-view-viewmodel">MVVM(Model-View-ViewModel)</h2>
<p><img src="https://velog.velcdn.com/images/darby-_-/post/f674eab1-e683-4e4d-9fe5-8d6bd33567eb/image.png" alt=""></p>
<p>MVVM은 거의 대부분의 Android 개발을 하다 보면 많이 보는 아키텍처이다.
처음에는 MVVM을 접했을 때 이미 AAC의 ViewModel이 나온 후 였고 MVVM의 대해 알아보려 하면 몇몇 블로그에서 AAC의 ViewModel로 MVVM을 구현을 해놓은 블로그 글들이 몇 개 있다. 그래서 MVVM의 ViewModel이랑 같은 건 줄 알았다. 근데 뭔가 이것만으로는 이해하기에 부족했다. 왜냐하면 <a href="https://developer.android.com/topic/libraries/architecture/viewmodel?hl=ko">ViewModel(AAC)</a>은 Android에서 사용하는건데 <a href="https://learn.microsoft.com/ko-kr/windows/uwp/data-binding/data-binding-and-mvvm">MVVM은 MS사</a>에서 만든 아키텍처라는 걸 알았기 때문이다. MVVM개념이 훨씬 먼저 나오기도 했다.</p>
<p>그러다가 원티드에서 챌린지를 참여하게 되었는데 어떤 영상을 보게 되었다. <a href="https://tv.naver.com/v/4637223">네이버 2018 테크 콘서트 영상입니다.</a> 오래된 영상이지만 MVVM의 기초를 이해하기에는 좋은 영상인 것 같다. 지금은 XML을 이용해서 개발한 앱이 아니지만 왜 DataBinding이 들어가야하는지 Observer패턴만으로는 왜 반쪽짜리 MVVM인지 잘 설명해주는 영상입니다.</p>
<p>근데 최근 Google을 보면 DataBinding은 2019년 3월에 업데이트 된 게 마지막이다. 
사용하는데는 문제는 없지만 ,,</p>
<p>Compose는 선언형 UI 프레임워크이다보니 사실 DataBinding을 사용하지 않고 MVVM 아키텍처를 만드는데 지장이 없다고 생각한다. </p>
<p><img src="https://velog.velcdn.com/images/darby-_-/post/2f0647d4-06c9-488c-837f-ae126b06585b/image.png" alt=""></p>
<p>MS사의 MVVM을 설명해놓은 이미지이다. ViewModel이 하는 역할을 View에서 Event가 발생하면 Model에게 Update요청을 해달라고하면 Model의 업데이트가 끝나면 다시 역순으로 알려주게된다.
해당 View는 데이터를 구독하는 형태로 있다가 데이터가 바뀌면 화면에서 보여주는 데이터가 바뀌게 된다.</p>
<p>개인적으로 중요하다고 생각되는 부분은 <strong>ViewModel은 View에 대해 절대 종속되면 안된다는 것</strong>이다. ViewModel은 1:N 관계를 가지는데 어떠한 View 종속되면 나중에 수정이나 재활용하기 어려워진다. 그렇게되면 MVVM을 쓰는 이유가 사라진다.</p>
<pre><code>@HiltViewModel
class MainViewModel @Inject constructor(private val userRepository: UserRepository) : ViewModel() {
    private val _userData = MutableLiveData&lt;ResultVO&gt;()
    val userData: LiveData&lt;ResultVO&gt; get() = _userData

    private val _errorLiveData = MutableLiveData&lt;String&gt;()
    val errorLiveData: LiveData&lt;String&gt; get() = _errorLiveData
    fun clearErrorData() {
        _errorLiveData.value = null
    }


    fun getUserData(id: String) {
        viewModelScope.launch {
            try {
                val result = userRepository.getUserData(id)
                _userData.value = result
                clearErrorData()
            } catch (e: Exception) {
                _errorLiveData.value = e.message.toString()
            }
        }
    }
}</code></pre><p>제 앱에 들어가있는 ViewModel인데 사실 AAC의 ViewModel을 사용하여 MVVM의 ViewModel 기능을 같이하게 만들었다. 
<em>아니 위 영상에서는 DataBinding 없는 MVVM은 반쪽짜리 MVVM이라 했는데?</em>
처음에는 저도 그렇게 생각을하다가 Compose를 사용하는데 DataBinding이 필요없다라는 것을 알고 이렇게 구현을 하게 되었다. </p>
</br>
MVVM에 대한 나의 생각 3줄 요약

<ol>
<li>MVVM의 ViewModel과 AAC의 ViewModel은 서로 상관이 없는 개념이다.</li>
<li>MVVM을 알고 싶으면 MS사의 문서를 보는게 낫다.</li>
<li>그렇다고 AAC의 ViewModel이 MVVM의 ViewModel 기능을 할 수 없다 라는 것은 아니다. 구현하기에 따라서 가능하다. (+ kotlin Flows, LiveData를 같이 사용해서 구현하면 좋다.)</li>
</ol>
<p>나중에는 MVI도 알아봐야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[개인 앱 제작 회고...]]></title>
            <link>https://velog.io/@darby-_-/%EA%B0%9C%EC%9D%B8-%EC%95%B1-%EC%A0%9C%EC%9E%91-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@darby-_-/%EA%B0%9C%EC%9D%B8-%EC%95%B1-%EC%A0%9C%EC%9E%91-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Mon, 15 Jan 2024 10:02:26 GMT</pubDate>
            <description><![CDATA[<h2 id="앱을-혼자-만들기">앱을 혼자 만들기</h2>
<p>이번에 쉬는 김에 내가 자주 안써본 기술들을 채용해서 앱을 하나 만들어 보고 싶다는 마음에 시작하게 되었다.. </p>
<p>하지만 뭘 만들까 &lt;&lt; 이게 제일 문제였다</p>
<p>그러다가 넥슨의 한 유명 버섯게임을 하고 있었는데 해당 게임은 원래 Open Api가 없어서 만들고 싶어도 만들지 못하다가 친구에게 여기있는 아이템을 긁어오면 어떻게하면 만들 수 있을 것 같다고 얘기를 하니까 친구가 크롤링으로 Api를 만들어줬다.  </p>
<p>크롤링은 별로 좋아하는 방법이 아니긴한데 딱히 다른방법이 없었다.. 다른 유사한 정보를 제공하는 사이트들을 보니 다른 사이트들도 다들 크롤링을 하는 것 같았다. 그런데 크롤링을 반복적으로 하다보면 보통은 크롤링을 막기 위해 해당 IP주소의 접근을 막는다. 그래서 크롤링이 원활하게 되지 않는다. 그래서 아마도 다른 사이트들은 VPN같은 것을 쓰지 않을까 생각하며 .. 기술 스택만 생각해봤다.. </p>
<p>앱에 넣은 기술 스택</p>
<p><strong>1.MVVM</strong> (무조건 넣어야 함 이거 때문에 사실 만든거임)
<strong>2.Hilt</strong> (사실 생성자와 어떤게 다른지 궁금하기도 하고 편하다고 해서 넣음)
<strong>3.Room</strong> (예전에는 SQLite를 사용했는데 이거 사용하면 더 편하대서 한 번 써보고 싶어짐)
<strong>4.Compose</strong> 
<strong>5.Navigation</strong></p>
<p>이정도는 꼭 넣고 싶었다.</p>
<h4 id="우선-어느정도까지-개발을-할-것인가"><strong><em>우선 어느정도까지 개발을 할 것인가?</em></strong></h4>
<p>스코프를 먼저 설정을 했어야 했는데
우선 기본적인것만 할려고 했다.
<strong>1.캐릭터검색
2.장착아이템
3.즐겨찾기
4.최근검색기록</strong></p>
<p>이렇게 설정하게 되었다.
그 이유는 캐릭터 검색은 가장 기본적인 서비스라고 생각해서 넣었고 장착아이템 또한 그렇다
3,4번을 넣게된 이유는 AAC의 하나인 Room을 사용해 보고 싶어서였다. </p>
<h4 id="그-다음은-디자인을-어떻게-할-것인가"><strong><em>그 다음은 디자인을 어떻게 할 것인가?</em></strong></h4>
<p>이게 정말 어려웠다.. 나는 디자인에 대한 감각이 없어서 어려웠는데 그 동안 회사를 다니며 디자이너분한테 종종 질문을 많이 했었는데 <strong>&quot;왜 이렇게 디자인 한 건가요?&quot;</strong> (지금 생각해보니 뭔가 좀 그렇네요) 순수 궁금해서 물어본 질문인데 디자이너분께서 의도를 설명해주기 때문에 꽤 자주 물어봤었던것 같다. + Material design에서 많이 쓰는 걸 자주 쓸려고 했다. 전 회사도 머터리얼 디자인을 많이 따라갈려고 했던것 같다. </p>
<div style="display:inline-block;">  
<img src = "https://velog.velcdn.com/images/darby-_-/post/cd41aea0-22cd-42fe-ac3c-4515ffba11b1/image.png" width = "30%" height = "30%" align = "left" style="margin-right:20px;" title ="메인화면"><img src ="https://velog.velcdn.com/images/darby-_-/post/688d9c60-07a5-44e3-b148-3542ff519599/image.png" width = "30%" height = "30%" align = "left" style="display:inline-block;" title ="캐릭터정보창">
</div>


<p>캐릭터정보 디자인창만 만들고 뜯어고치고만 1주일 넘게 한 것 같다..
캐릭터가 가지고 있는 스탯이 워낙 많아서 어떻게 보여줘야 한 눈에 자기가 보고 싶어하는 걸 한 번에 볼 수 있을까? 라는 생각을 하다보니 내가 만들어놓고 아 진짜 구린데 ? 라는 생각을 하다보니 오래걸린것 같다.. 디자이너분들은 항상 대단하신 것 같다..</p>
<p>개발에 대한 내용은 다음 포스트에 이어서 쓰겠습니다!</p>
<p>firebase 연결해서 crashlytics도 연결하고 앱 아이콘도 제작하고.. 이번에 Compose에서 Splash도 테마로 쉽게 만들수 있는게 있어서 그것도 사용해봤다. Splash도 만들고 다 만들어서 앱을 등록하려고 했는데.. 
그 동안 앱을 회사에서만 만들고 올려서 그랬는데 최근에는 플레이콘솔을 볼 일이 없어서 안보다가 작년 11월쯤에 업데이트가 있었다. 11월이후 신규 개인 개발자는 20명의 테스터가 14일 연속으로 테스트를 해야 플레이 스토어에 등록을 할 수 있다고한다.. ㅠㅠ
<em>아 미리 만들어 둘 걸..</em> 그래서 지금은 테스터를 구해야 하나.. 돈주고 테스트해주는 업체에 맡겨야 하나 고민중에 있다.. 이유는 인도쪽에서 스미싱 앱을 만들어서 그렇다는 썰이 있다..</p>
<p>회고 3줄 요약..</p>
<ol>
<li>어디까지 서비스 할 건지 스코프를 확실히 하고 시작할 것</li>
<li>디자인은 어렵다.. 좋은 UI/UX를 보면 어딘가에 적어두던가 해야 할 것 같다.</li>
<li>아 계정 빨리 만들걸..</li>
</ol>
]]></description>
        </item>
    </channel>
</rss>