<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>ricky_0_k.log</title>
        <link>https://velog.io/</link>
        <description>valuable 을 추구하려 노력하는 개발자</description>
        <lastBuildDate>Sat, 18 Nov 2023 09:19:59 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>ricky_0_k.log</title>
            <url>https://images.velog.io/images/ricky_0_k/profile/e0c81d4e-148c-4d0d-a303-f94e0310333e/social.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. ricky_0_k.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/ricky_0_k" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Effective dart - 0. Overview]]></title>
            <link>https://velog.io/@ricky_0_k/Effective-dart-0.-Overview</link>
            <guid>https://velog.io/@ricky_0_k/Effective-dart-0.-Overview</guid>
            <pubDate>Sat, 18 Nov 2023 09:19:59 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p><a href="https://dart.dev/effective-dart">https://dart.dev/effective-dart</a> 를 보면서 해석 및 내용정리를 해보려 한다.
예전에 <a href="https://www.youtube.com/watch?v=fxdPHw8vhog">https://www.youtube.com/watch?v=fxdPHw8vhog</a> 에서 키워드 정리법에 대해 본 것이 있어 키워드로 정리해보려 한다.</p>
<h1 id="키워드">키워드</h1>
<ol>
<li>일관적이고 간결함</li>
<li>스타일 가이드</li>
<li>doc 가이드</li>
<li>사용 가이드</li>
<li>디자인 가이드</li>
<li>Do</li>
<li>Don&#39;t</li>
<li>Prefer</li>
</ol>
<h1 id="결론">결론</h1>
<p>음 1년뒤에 이 키워드를 보고 내가 뭘 읽었는지 추리해낼 수 있을까?</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[FlutterActivity 는 무엇일까? - 1. 주석 분석하기]]></title>
            <link>https://velog.io/@ricky_0_k/FlutterActivity-%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C</link>
            <guid>https://velog.io/@ricky_0_k/FlutterActivity-%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C</guid>
            <pubDate>Sat, 29 Apr 2023 16:34:18 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p>문득 Flutter 작업을 하면서, 네이티브에서는 어떻게 띄워지는지 궁금해졌다.</p>
<blockquote>
<p>Flutter 는 skia 그래픽 엔진을 통해 각 플랫폼에 종속되지 않고 화면을 자체적으로 그려준다.</p>
</blockquote>
<p>물론 이렇게는 알고 있다.</p>
<h2 id="1-runapp">1. runApp</h2>
<pre><code class="language-dart">void main() {
  runApp(const MyApp());
}</code></pre>
<p>main 함수를 통해 시작되어, MyApp 이라는 Widget 를 켜줌으로써 flutter 앱은 시작된다.
그런데 어떻게 Android 에서 인식되어 앱이 실행되는걸까?</p>
<h2 id="2-android-폴더-확인">2. .android 폴더 확인</h2>
<p>flutter 프로젝트를 생성하며 같이 생기는 .android 모듈로 들어가 보았다. 
아래는 AndroidManifest.xml 내용이다.</p>
<pre><code class="language-xml">&lt;application
     android:label=&quot;playground&quot;
     android:name=&quot;${applicationName}&quot;
     android:icon=&quot;@mipmap/ic_launcher&quot;&gt;
     &lt;activity
         android:name=&quot;.MainActivity&quot;
         android:exported=&quot;true&quot;
         android:launchMode=&quot;singleTop&quot;
         android:theme=&quot;@style/LaunchTheme&quot;
         android:configChanges=&quot;orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode&quot;
         android:hardwareAccelerated=&quot;true&quot;
         android:windowSoftInputMode=&quot;adjustResize&quot;&gt;
         &lt;!-- Specifies an Android theme to apply to this Activity as soon as
              the Android process has started. This theme is visible to the user
              while the Flutter UI initializes. After that, this theme continues
              to determine the Window background behind the Flutter UI. --&gt;
         &lt;meta-data
           android:name=&quot;io.flutter.embedding.android.NormalTheme&quot;
           android:resource=&quot;@style/NormalTheme&quot;
           /&gt;
         &lt;!-- key point --&gt;
         &lt;intent-filter&gt;
             &lt;action android:name=&quot;android.intent.action.MAIN&quot;/&gt;
             &lt;category android:name=&quot;android.intent.category.LAUNCHER&quot;/&gt;
         &lt;/intent-filter&gt;
     &lt;/activity&gt;
     &lt;!-- Don&#39;t delete the meta-data below.
          This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --&gt;
     &lt;meta-data
         android:name=&quot;flutterEmbedding&quot;
         android:value=&quot;2&quot; /&gt;
 &lt;/application&gt;</code></pre>
<p>여기서 <code>&lt;!-- key point --&gt;</code> 아래 <code>intent-filter</code> 로 인해 MainActivity 가 앱의 시작점이 된다.
그럼 MainActivity 는 뭘까? 아래 코드이다.</p>
<pre><code class="language-kotlin">package com.example.playground

import io.flutter.embedding.android.FlutterActivity

class MainActivity: FlutterActivity() {
}</code></pre>
<p>위 내용을 통해 flutter 로 android 앱을 빌드하게 되면 MainActivity 를 실행하고
MainActivity 자체는 Flutter 와 연관이 있다는 걸 알 수 있다.</p>
<p>그런데.... MainActivity 내용이 너무 빈약하다.
보다시피 MainActivity 코드는 달랑 저거인데, flutter UI 를 보여주고 있다.
어떻게 가능한걸까? 공식문서와 코드를 좀 더 확인할 필요를 느꼈다.</p>
<p>이제 본론으로 들어가 FlutterActivity 코드를 파헤쳐보려 한다.</p>
<h1 id="flutteractivity">FlutterActivity?</h1>
<p>IDE 상에서 직접 타고 들어가 볼 수도 있고, <a href="https://api.flutter.dev/javadoc/io/flutter/embedding/android/FlutterActivity.html">공식문서</a>에서도 볼 수 있다.
1차적으론 공식문서 내용을 참고하려 한다.</p>
<h2 id="정의">정의</h2>
<p>먼저 정의를 보자면 이렇다.</p>
<ul>
<li><code>전체 화면 Flutter UI 를 표시</code>하는 Activity</li>
<li>Android 앱 내에서 Flutter 를 통합하는 가장 간단하고 직접적인 방법</li>
</ul>
<h2 id="dart-진입점-진입점-인수-초기-경로-및-앱-번들-경로">Dart 진입점, 진입점 인수, 초기 경로 및 앱 번들 경로</h2>
<p>여기서는 제법 실험해보고 싶은 것들이 있었다. 하나씩 실험해보려 한다.</p>
<h3 id="1-진입점-변경">1. 진입점 변경</h3>
<p>FlutterActivity가 실행하는 진입점을 변경하려면
FlutterActivity를 하위 클래스로 만들고, <code>getDartEntrypointFunctionName()</code> 을 재정의 해야 한다.</p>
<p>먼저 getDartEntrypointFunctionName() 에 대해 확인해보았다.</p>
<pre><code class="language-java">// FlutterActivity.kt (내장 코드) - 내가 짠 코드 아님 ㅇㅅㅇ..

@NonNull
public String getDartEntrypointFunctionName() {
  try {
    // 1
    Bundle metaData = getMetaData();
    // 2
    String desiredDartEntrypoint =
        metaData != null ? metaData.getString(DART_ENTRYPOINT_META_DATA_KEY) : null;
    return desiredDartEntrypoint != null ? desiredDartEntrypoint : DEFAULT_DART_ENTRYPOINT;
  } catch (PackageManager.NameNotFoundException e) {
    return DEFAULT_DART_ENTRYPOINT;
  }
}</code></pre>
<ol>
<li><p><code>getMetaData()</code> 는 <code>AndroidManifest.xml</code> 에 명세된 meta 데이터들을 Bundle 형태로 가져온다. 
(Bundle 은 Map 과 비슷하게 생각하면 된다.)
어떤 meta 데이터를 가져오는 것일까 궁금해 AndroidManifest.xml 을 보면 아래 코드가 보인다.</p>
<pre><code class="language-xml">&lt;!-- Don&#39;t delete the meta-data below.
    This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --&gt;
&lt;meta-data
   android:name=&quot;flutterEmbedding&quot;
   android:value=&quot;2&quot; /&gt;</code></pre>
<p>뭘 가져오는지 쉽게 알 수 있었다. <br></p>
</li>
<li><p><code>io.flutter.Entrypoint</code> 키값에 매치되는 값을 가져온다. 
해당 값이 존재하지 않으면 null 이 되며, 자연스럽게 기본 entryPoint 인 <code>main</code> 을 반환하게 된다.</p>
</li>
</ol>
<p><code>main</code>... dart 코드에서 본 것 같지 않은가?
이를 통해 android 모듈에서 어떻게 flutter dart 코드의 Screen 을 불러내는지 알 수 있었다.</p>
<p>여기서 더 나아가 간단한 코드로 실험해보았다.</p>
<pre><code class="language-dart">// main.dart

void main() {
  runApp(const MyApp());
}

// 1
void mainSecond() {
  runApp(const SecondScreen());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: &#39;Flutter Demo&#39;,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      initialRoute: &#39;/&#39;,
      routes: {
        &#39;/&#39;: (context) =&gt; const FirstScreen(),
        &#39;/second&#39;: (context) =&gt; const SecondScreen(),
      },
    );
  }
}

// ...

// android 모듈 내 MainActivity.kt

class MainActivity : FlutterActivity() {
    override fun getDartEntrypointFunctionName(): String {
        Log.i(&quot;riflockle7&quot;, super.getDartEntrypointFunctionName())
        // 1
        return &quot;mainSecond&quot;
    }
}</code></pre>
<p><code>mainSecond()</code> 함수를 새로 만들어주었다. 
그리고 MainActivity.kt 에서는 강제로 <code>mainSecond</code> 를 반환하도록 해주었다. 
(<code>// 1</code> 코드 참고)</p>
<p>이렇게 수정한 뒤 flutter 프로젝트에서 run 을 해주니 SecondScreen 이 실행되었다.
android 폴더에서 빌드 후 실행해도 결과는 동일하다.</p>
<blockquote>
<p>android 폴더에서 run 시 주의할 점</p>
<p>코드 수정 시 flutter 가 아닌 android 폴더에서만 run 을 실행하면 변경사항이 반영되지 않는다.
예를 들어 위의 코드 상태에서 Log 내용과 mainSecond 함수명을 수정한 뒤 android 폴더에서 run 을 하면 수정 내용이 반영되지 않는다.</p>
<p>수정 내용을 반영하려면 flutter app 에서 build 및 run 을 실행해야 한다.</p>
</blockquote>
<p>위 내용은 응용하여 getDartEntrypointFunctionName() 재정의 필요 없이
AndroidManifest 에 <code>io.flutter.EntryPoint</code> 메타데이터를 추가하여 처리할 수 있다.</p>
<pre><code class="language-xml">&lt;meta-data
    android:name=&quot;io.flutter.Entrypoint&quot;
    android:value=&quot;mainSecond&quot; /&gt;</code></pre>
<p><code>&lt;activity&gt;</code> 태그 내에 위의 meta 데이터를 추가하면, 강제로 mainSecond 문자열을 반환하지 않아도 된다.</p>
<h3 id="2-dart-진입점-매개변수">2. Dart 진입점 매개변수</h3>
<p>Dart 진입점의 인수는 문자열 목록으로 Dart의 진입점 함수에 전달된다.
FlutterActivity.NewEngineIntentBuilder.dartEntrypointArgs 를 통해 
<code>FlutterActivity.NewEngineIntentBuilder</code> 를 사용하여 전달할 수 있다. 
(초기 경로(initialRoute) 제어에도 블록 내용이 사용된다.)</p>
<p>엔진 직접 설정으로는 애로사항이 있었다.
다만 MainActivity 내에 아래 함수를 재정의하면 main 함수에 인수를 받는 것을 확인했다.</p>
<pre><code class="language-dart">// main.dart

void main(List&lt;String&gt; args) {
  print(&#39;riflockle7 $args&#39;);
  runApp(const MyApp());
}

// android 모듈 내 MainActivity.kt

class MainActivity : FlutterActivity() {
    override fun getDartEntrypointArgs(): MutableList&lt;String&gt; {
        return mutableListOf(&quot;비둘기&quot;)
    }
}</code></pre>
<p>dart 의 main 함수에도 문자열 배열을 인수를 받을 수 있도록 해야한다/
이렇게 처리하면 &quot;비둘기&quot; 항목이 있는 배열을 dart 에서 받을 수 있다.</p>
<p>getDartEntrypointArgs() 함수 원본을 보면 <code>dart_entrypoint_args</code> 을 getExtra 형태로 가져온다.
이를 응용하여 처리도 가능해 보였는데, 이는 포스팅 올린 후 확인해보려 한다.</p>
<h3 id="3-routing">3. routing</h3>
<p>처음 로드되는 Flutter 경로는 &quot;/&quot; 이다. 
초기 경로는 FlutterActivityLaunchConfigs.EXTRA_INITIAL_ROUTE 에서 문자열로 경로 이름을 전달하여 명시적으로 지정할 수 있다.</p>
<pre><code class="language-kotlin">class MainActivity : FlutterActivity() {
override fun getInitialRoute(): String {
    return &quot;/second&quot;
}
}</code></pre>
<p>이 역시 이런식으로 처리는 가능해보이지만, MaterialApp 에서 initialRoute 를 설정하는 게 더 좋아 보였다.</p>
<p>이렇게 FlutterActivity 하위 클래스에서 Dart 진입점, Dart 진입점 인수 및 초기 경로 등을 제어할 수 있다.</p>
<h2 id="flutterengine">FlutterEngine</h2>
<p>FlutterEngine 에 대한 내용도 나오는 데, 요 내용은 깊게 들어가면 산으로 갈 것 같아 다른 포스트에서 언급하려 한다.
일단 이 포스트에서는 새로운 FlutterEngine 을 만드는 대신, <code>캐시된 FlutterEngine</code> 을 사용할 수 있다 정도만 언급하려 한다.</p>
<h2 id="flutteractivity-대안">FlutterActivity 대안</h2>
<p><code>FlutterFragment</code> 와 <code>FlutterView</code> 를 사용할 수 있다.
이 경우에도 android 의 생명주기와 관련된 코드 작업이 필요할 수 있다.</p>
<h2 id="launch-screen-splash-screen">Launch Screen, Splash Screen</h2>
<p>Android 테마를 사용하여 LaunchTheme, NormalTheme 를 만들 수 있다.</p>
<p>시작 테마에서 windowBackground를 시작 화면에 대해 원하는 Drawable로 설정할 수 있으며
일반 테마에서 일반적으로 Flutter 콘텐츠 뒤에 나타나야 하는 원하는 배경색으로 windowBackground를 설정할 수 있다</p>
<p>직접 보는 게 나을 것 같아 코드를 가져와 보았다.</p>
<pre><code class="language-xml">&lt;!-- styles.xml --&gt;

&lt;resources&gt;
    &lt;!-- 프로세스가 시작되는 동안 Android Window에 적용되는 테마  --&gt;
    &lt;style name=&quot;LaunchTheme&quot; parent=&quot;@android:style/Theme.Light.NoTitleBar&quot;&gt;
        &lt;!-- activity 위에 splash screen 을 보여준다. 플러터 엔진이 첫 번째 프레임을 그려냈을 때 자동으로 제거된다. --&gt;
        &lt;item name=&quot;android:windowBackground&quot;&gt;@drawable/launch_background&lt;/item&gt;
    &lt;/style&gt;

    &lt;!-- 프로세스가 시작되자마자 Android Window 에 테마가 적용된다.
         이 테마는 Flutter UI가 초기화되는 동안 Android Window 의 색상을 결정하고 Flutter UI 가 실행되는 동안의 색상도 결정한다.
         이 테마는 Flutter의 Android embedding V2 부터만 사용된다. --&gt;
    &lt;style name=&quot;NormalTheme&quot; parent=&quot;@android:style/Theme.Light.NoTitleBar&quot;&gt;
        &lt;item name=&quot;android:windowBackground&quot;&gt;?android:colorBackground&lt;/item&gt;
    &lt;/style&gt;
&lt;/resources&gt;

&lt;!-- AndroidManifest.xml --&gt;
&lt;activity
    android:name=&quot;.MainActivity&quot;
    ...
    android:theme=&quot;@style/LaunchTheme&quot;&gt;
    &lt;!--
         Android 프로세스가 시작되는 즉시 이 활동에 적용할 Android 테마를 지정합니다.
         이 테마는 Flutter UI 가 초기화되는 동안 사용자에게 표시됩니다.
         그 후 이 테마는 계속해서 Flutter UI 뒤의 창 배경을 결정합니다.
    --&gt;
    &lt;meta-data
        android:name=&quot;io.flutter.embedding.android.NormalTheme&quot;
        android:resource=&quot;@style/NormalTheme&quot; /&gt;

    ...

&lt;/activity&gt;</code></pre>
<p><code>styles.xml</code> 에서 dark theme 인 경우, Light 대신 Dark 가 들어간다.</p>
<p><code>LaunchTheme</code> 는 우리가 각 Activity 에서 theme 를 적용하는 것을 이야기하는 것 같았다.
실제로 배경 색상을 바꿔보니 즉각 적용되는 걸 확인했다.</p>
<p><code>NormalTheme</code> 는 화면에 반영된 내용을 즉각 확인하지 못했다.
찾아보니 Android의 기본 테마와는 별개로, 자체적으로 Theme 를 가지고 있지 않은 
일부 Flutter Widget 에 적용되는 테마라고 하니 못 느끼는 게 정상인가 싶기도 하다.</p>
<h1 id="정리">정리</h1>
<p>간단하게 보려고 했는 데 주석만 벌써 한 바퀴 돌고 나니 제법 많은 내용의 글이 써졌다....
이번 포스트는 맛보기만 하고, 다음 포스트에서 FlutterActivity 코드를 분석해보려 한다.</p>
<h1 id="참고">참고</h1>
<ul>
<li><a href="https://api.flutter.dev/javadoc/io/flutter/embedding/android/FlutterActivity.html">https://api.flutter.dev/javadoc/io/flutter/embedding/android/FlutterActivity.html</a></li>
<li><a href="https://flutter.dev/docs/development/add-to-app/performance">https://flutter.dev/docs/development/add-to-app/performance</a></li>
<li><a href="https://brunch.co.kr/@myner/5">https://brunch.co.kr/@myner/5</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter 디코딩 하기 시리즈] 3. BuildContext?!]]></title>
            <link>https://velog.io/@ricky_0_k/Flutter-%EB%94%94%EC%BD%94%EB%94%A9-%ED%95%98%EA%B8%B0-%EC%8B%9C%EB%A6%AC%EC%A6%88-3.-BuildContext</link>
            <guid>https://velog.io/@ricky_0_k/Flutter-%EB%94%94%EC%BD%94%EB%94%A9-%ED%95%98%EA%B8%B0-%EC%8B%9C%EB%A6%AC%EC%A6%88-3.-BuildContext</guid>
            <pubDate>Tue, 04 Apr 2023 16:07:18 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p>저번에 생명주기로 골머리를 겪었는데 이번엔 더 심층적인 것이 나왔다.
안드에서도 골머리를 앓았던 Context.. 인데 Flutter 에서는 5단어가 더 생긴 <code>BuildContext</code> 이다..
이번에도 동영상 독파 후 시작한다.</p>
<h1 id="소개">소개</h1>
<p><a href="https://www.youtube.com/watch?v=rIaaH87z1-g&amp;list=PLjxrf2q8roU1fRV40Ec8200rX6OuQkmnl">https://www.youtube.com/watch?v=rIaaH87z1-g&amp;list=PLjxrf2q8roU1fRV40Ec8200rX6OuQkmnl</a></p>
<p>동영상에서는 먼저 위젯에 대해 복습을 간단하게 시작한다.
Flutter 의 위젯은 <code>UI 가 갖춰야 하는 청사진</code> 을 이야기한다.</p>
<h2 id="위젯의-관계">위젯의 관계</h2>
<p><img src="https://velog.velcdn.com/images/ricky_0_k/post/1af5bee4-b6cb-486f-8731-a0a821f41305/image.png" alt=""></p>
<p>위젯들은 서로 독립적이지 않고 관계를 맺는다고 이야기한다. 실제로 그렇다.</p>
<ul>
<li>빨간색 영역처럼 다른 위젯 옆에 있을 수 있다.</li>
<li>초록색 영역처럼 위젯 내에 종속될 수도 있다.</li>
</ul>
<p>위의 말이 이해가 잘 안될수도 있을 것 같아 코드도 준비해 보았다.</p>
<pre><code class="language-dart">@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Column(
      children: [
        Row(
          children: [
            ColoredBox(
              color: Color(0xffff0000),
              child: Text(&quot;기둘기&quot;),
            ),
            Text(&quot;니둘기&quot;),
          ],
        ),
        ColoredBox(
          color: Color(0xffff0000),
          child: Padding(
            padding: EdgeInsets.all(8),
            child: Text(&quot;비둘기&quot;),
          ),
        ),
      ],
    ),
  );
}</code></pre>
<p>위 도식화된 그림에 따라 정리를 해본다면 이렇게 표현이 가능할 것이다. (한번 빌드한 내용도 가져와보았는데 예쁘지는 않다)</p>
<p><img src="https://velog.velcdn.com/images/ricky_0_k/post/6ea12249-002f-4add-89bc-f5442a6ecfd8/image.png" alt=""></p>
<p>어쨌거나 이로서 위젯이 서로 관계를 가진다는 건 알았는데 이게 어떻게 가능할까?</p>
<h3 id="위젯이-이-정보를-가지고-있을까">위젯이 이 정보를 가지고 있을까?</h3>
<p>위젯 간 관계를 나타내려면 최소 <code>트리 내의 특정한 인스턴스 위치 정보</code> 는 가지고 있어야 할 것이다.
아니 적어도 <code>상위 위젯</code> 이 무엇인지 정도는 알아야 할 것이다. 
위의 정보들을 과연 위젯이 가지고 있을까?</p>
<p>그러기엔 위젯은 어느 위치에서든 자유롭게 사용이 가능해야하므로, <code>그 정보</code> 들을 가지고 있기엔 무겁다.
그 정보를 가지고 있을 때 위젯의 위치가 바뀐다고 가정하면, 위젯 내 값들을 모조리 바꿔주어야 할테니 
화면 그리기도 하는 데 위치까지 신경쓰기엔, 위젯이 전지전능한 기능이 되어 무거워질 것이다.</p>
<p>그리고 결정적으로 위젯은 <code>변경할 수 없다</code>.
그런데 위젯 위치 정보는 쉽게 바뀔 수 있는 정보인데 그걸 가지고 있는다? 
서로 상충되는 내용이다.</p>
<p>실제 저자도 위젯 내에 해당 값들을 가지고 있지 않다고 언급했다.
그러므로 위젯 자체에서는 트리 내의 특정한 위치 정보를 보관하지 않을 것이다.</p>
<p>그러므로 정답은 ❌ 이다. 그러면 이런 역할을 누가 해주는 것일까?</p>
<h2 id="element">Element</h2>
<p>이후 저자는 Widget 코드를 분석하면서 <code>createElement()</code> 를 이야기한다.</p>
<h3 id="createelement"><code>createElement()</code></h3>
<p>인스턴스를 구체화해주는 함수이다.
이 함수로 인해 만들어진 Element 는 위젯 트리에서 <code>해당 위젯이 어디있는지</code> 알려주는 데 활용된다.</p>
<p><img src="https://velog.velcdn.com/images/ricky_0_k/post/6c0a03c5-1a8f-4bee-b9d7-c2476a7a6d65/image.png" alt=""></p>
<p>실제로 createElement() 를 통해 widget, parent(상위 Element), renderObject(위젯 트리에서 현재 위치에 있는 렌더링 개체) 등을 설정 또는 가져올 수 있다.
(실제 Element 클래스 명세를 보면 _widget, _parent 등의 변수들을 볼 수 있다)</p>
<p>결론적으로 위치 관계를 다루는 역할을 <code>Element</code> 가 해준다는 걸 알 수 있다.</p>
<h3 id="그런데-buildcontext-는-element-와-뭔-상관">그런데 BuildContext 는 Element 와 뭔 상관?</h3>
<p>그런데 BuildContext 와 Element 는 뭔 상관일까?
Element 의 명세를 자세히 보면 이렇게 되어 있다.</p>
<p><img src="https://velog.velcdn.com/images/ricky_0_k/post/6f54366b-abdd-4ccc-81f8-74e72376ab3b/image.png" alt=""></p>
<p>결국은 오늘의 주제 <code>BuildContext</code> 가 그 역할을 하는 것이다.</p>
<blockquote>
<p>Element 는 다른 Element 들에게 유용한 노드가 되며, 이들이 나타내는 위젯에 대한 각각의 참조를 제공한다.</p>
</blockquote>
<p>정리하면 이렇고, 그러면 build()에서 BuildContext 파라미터를 제공하는 것도 어느정도 이해가 된다.
해당 BuildContext 를 통해 상위 Element 를 확인하거나, 현재 위젯의 정보를 가져오거나하여 개발자는 유용한 작업을 할 수 있다.
(ex. <code>MediaQuery.of(context)</code>)</p>
<p>이렇게 또 영상은 끝난다.
동영상에서 본 내용을 바탕으로 추가로 본 내용들을 정리해놓으려 한다.</p>
<h1 id="element-deep-dive">Element Deep Dive</h1>
<p>위젯은 불변하다. 그리고 build 를 하면 위젯을 새로 그려낸다.</p>
<p>그러므로 StatefulWidget 을 통해 위젯을 변경해주거나 (setState), 
화면을 자주 왔다갔다하면 그만큼 위젯을 버리고 새로 그리는 작업이 많아질 것이다.</p>
<p>상태는 깨끗하게 관리되겠지만 그리는 데 많은 비용을 소모할 것이다.
flutter 는 이런 문제를 어떻게 해결하는 걸까?</p>
<h2 id="flutter-tree">Flutter Tree</h2>
<p>먼저 Flutter 에서 자주 언급되는 Tree 를 이야기해보려 한다.
<img src="https://velog.velcdn.com/images/ricky_0_k/post/a723c087-61b9-401a-913c-a91c80e36139/image.png" alt="">
우리는 flutter 공식 문서나 위젯 설명 내용을 보면서 Widget Tree 를 들어봤을 것이고,
Widget Tree 뒤에 그림자가 스며든 그림과 Element Tree 에 대한 이야기도 많이 보았을 것이다.
그 외에 Render Tree 라는 것도 있다.</p>
<p>이 3개 Tree 에 대해 간단히 이야기해보려 한다.</p>
<h3 id="widget-tree">Widget Tree</h3>
<p>말 그대로 우리가 구현한 위젯 구조를 이야기한 것이다. (ex. 위의 Column 코드)</p>
<p>runApp() 에서 파라미터로 들어가는 위젯이 루트 위젯이 되고
거기서 호출되는 위젯에 따라 아래로 계속 타고 내려갈 것이다.</p>
<p>flutter inspector 를 보면 아래와 같은 구조를 볼 수 있을 것이다. 이것도 Widget Tree 이다.
<img src="https://velog.velcdn.com/images/ricky_0_k/post/63f5b326-8529-44d2-91b8-772e3ddeeba7/image.png" alt=""></p>
<h3 id="element-tree">Element Tree</h3>
<p>각 Widget 이 생성되면서 동시에 Element 도 생성된다. (일전에 createElement() 에서 언급)
Element 에서 이야기한대로 다른 위젯과의 부모 또는 자식 관계를 나타내는 Tree 이다.
Widget 생성과 함께 만들어지는 만큼 Element 는 Widget 과 1:1 연결된다.</p>
<p>만약 Widget Tree 가 파괴 후 재창조 수준으로 다시 만들어지는 경우
Element Tree 는 Widget Tree 의 새로 생긴 위젯들과 1:1 연결을 다시하고
<code>기존 위젯과 변경된 내용이 있는지 체크 ( = canUpdate() )</code>한다.</p>
<p>변경된 내용이 있다면 <code>해당 부분만 다시 그리라고(=렌더링하라고)</code> 한다.</p>
<h3 id="render-tree">Render Tree</h3>
<p>실제 그려진 화면에 대한 Tree 이다. Render Tree 는 Element Tree 랑만 연결되어 있다.</p>
<p><code>Widget Tree 랑 연결 되지 않았기 때문에, 객체만 만들어질 뿐 화면을 다시 구성하는 데에는 큰 영향을 주지 않는다고 한다.</code></p>
<p>Tree 이야기 도중 성능과 관련 있어보이는 내용을 블록 표시 해놓았다.</p>
<ul>
<li>위젯도 재사용을 활용한다. -&gt; 재사용을 통해 위젯을 새로 그리는 걸 줄인다.</li>
<li>그리고 렌더링 작업은 Widget 과 연결되지 않았다. -&gt; 잦은 위젯 변경으로 인해 렌더링이 과하게 행해지는 것도 없다.</li>
</ul>
<p>사실상 Element 를 flutter 의 핵심으로 보아도 될 것 같았다.</p>
<h2 id="element-생명주기">Element 생명주기</h2>
<p>Element 의 생명주기에 대해 이야기한 <a href="https://api.flutter.dev/flutter/widgets/Element/mount.html">공식문서 내용</a>이 있어 가져와 보았다. </p>
<p><strong>생성</strong>
createElement() 를 통해 생성된 Element 는, 부모 영역 아래 트리에 추가하기 위해 mount() 를 호출한다.</p>
<blockquote>
<p>mount
주어진 부모의 주어진 슬롯에 있는 트리에 새로운 Element 를 추가하는 작업
새로 생성된 Element 가 tree 에 처음 추가될 때 호출되며, 이 함수를 통해 부모에 따라 달라지는 상태를 초기화한다.</p>
<p>연결된 렌더 객체를 Render Tree 에 연결하기위해, 
필요에 따라 자식 위젯을 그려주거나 attachRenderObject 를 호출하기도 한다.</p>
</blockquote>
<p>createElement() 이후 <code>_firstBuild()</code> 를 통해 
state 초기 함수들 (ex. 이전에 Widget Lifecycle 로 언급했던 initState 등), build() 를 실행해준다.</p>
<p>이후 비로소 Element 는 활성으로 간주되어 화면에 나타날 수 있다.</p>
<p><strong>변경</strong></p>
<p>element 가 재사용이 불가능하다면 element 를 새로 만들어준다.
아래 canUpdate() 함수를 통해 먼저 재사용이 가능한지 체크한다.</p>
<pre><code class="language-dart">@immutable
abstract class Widget {
   …
  @factory
  Element createElement();
   …
  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType &amp;&amp; oldWidget.key == newWidget.key;
  }
}</code></pre>
<p>runtimeType 과 key 를 비교해서 기존 위젯과의 변경점을 찾고, 변경점이 있다면 업데이트한다.
runtimeType 과 key 중 하나라도 변경되면 위젯은 새로 그려진다고 보면 된다.
새로 그려지는 flow 에서는, 이전 Element 를 mount 해제하고 새로운 Element 를 생성 후 mount 하여 새로운 위젯으로 변경할 수 있다.</p>
<p><strong>소멸</strong>
Element 가 제거될 때 deactivateChild 가 호출되어 해당 Element의 랜더링 개체가 제거된다.
이후 이 Element 는 비활성화되고 unmount 된다.
unmount 된 Element 는 더 이상 사용할 수 없는 상태가 되어 화면에 나타나지 않고 트리에 통합되지도 않는다.</p>
<p>다만 애니메이션이 있을 경우, 애니메이션 종료 전까지는 비활성 상태로 있다가 
애니메이션이 끝난 이후 비활성 상태 Element 들이 모두 unmount 된다.</p>
<p><strong>복귀</strong>
특수한 경우 Element 가 다시 들어올수도 있다. (ex. 자신이나 부모 위젯에서 GlobalKey 를 사용하는 경우 등)
복귀하는 경우 비활성 Element 목록에서 제거된 뒤 활성상태가 되고 렌더링 개체를 Render Tree 에 다시 연결한다.</p>
<h1 id="결론">결론</h1>
<p>Flutter 는 처음 접근하기 쉽다고 한다. 실제로 그렇다.
개발자가 Widget으로 화면을 구성하면 많은 부분을 Flutter가 해주고, Element들을 재사용해주기 때문에
Widget 만 가지고도 성능 저하 없이 화면 개발을 쉽게 할 수 있다.</p>
<p>하지만 깊게 들어가면 화면 표시가 느려지거나 버벅거림을 마주할 수도 있을 것이다.
또는 잘못된 context 를 사용하여 알 수 없는 에러를 마주하고 고치는 데 애를 먹을수도 있다.</p>
<p>이 경우에는 Element 관리를 잘하고 있는지, 잘못된 context 를 사용하지 않았는지 확인이 필요할 것이다.</p>
<p>이번 글을 쓰면서 거의 참고 링크 글을 읽고 그대로 작성한 부분도 있다.
깊게 들어가면 한 없이 어려운 내용인만큼 이번 정리 이후로도 참고 링크 내용을 주기적으로 봐야겠다는 생각이 들었다.</p>
<h1 id="참고">참고</h1>
<p><a href="https://papabee.tistory.com/77">https://papabee.tistory.com/77</a>
<a href="https://docs.flutter.dev/resources/inside-flutter">https://docs.flutter.dev/resources/inside-flutter</a>
<a href="https://docs.flutter.dev/resources/architectural-overview#build-from-widget-to-element">https://docs.flutter.dev/resources/architectural-overview#build-from-widget-to-element</a>
<a href="https://api.flutter.dev/flutter/widgets/Element/mount.html">https://api.flutter.dev/flutter/widgets/Element/mount.html</a>
<a href="https://ctoahn.tistory.com/30">https://ctoahn.tistory.com/30</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter 디코딩 하기 시리즈] 2. Life of a Widget?!]]></title>
            <link>https://velog.io/@ricky_0_k/Flutter-%EB%94%94%EC%BD%94%EB%94%A9-%ED%95%98%EA%B8%B0-%EC%8B%9C%EB%A6%AC%EC%A6%88-2.-Life-of-a-Widget</link>
            <guid>https://velog.io/@ricky_0_k/Flutter-%EB%94%94%EC%BD%94%EB%94%A9-%ED%95%98%EA%B8%B0-%EC%8B%9C%EB%A6%AC%EC%A6%88-2.-Life-of-a-Widget</guid>
            <pubDate>Mon, 20 Mar 2023 17:48:51 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p>으 제일 보기 힘든 거 나왔다...
어느 플랫폼에서나 생명주기는 중요한 개념이니 빠르게 훑어보자</p>
<h1 id="소개">소개</h1>
<p><a href="https://youtu.be/cyFM2emjbQ8?list=PLjxrf2q8roU1fRV40Ec8200rX6OuQkmnl">https://youtu.be/cyFM2emjbQ8?list=PLjxrf2q8roU1fRV40Ec8200rX6OuQkmnl</a></p>
<p><img src="https://velog.velcdn.com/images/ricky_0_k/post/42e24403-ebbc-4577-adee-8874363f19f8/image.png" alt=""> </p>
<p>플랫폼에는 화면 역할을 하는 기능이 있고</p>
<p>크게 보면 Android 에서는 <code>Activity</code>, iOS 는 <code>UIViewController</code> 이다.
Flutter 에서는 Widget 이 가장 비슷한 역할을 한다.</p>
<blockquote>
<p>Flutter 는 위젯을 이용해, <code>화면에 그릴 것을 결정</code>하기 때문이다.</p>
</blockquote>
<p>Android 와 iOS 에서는 각 기능이 고유한 수명 주기를 가지고 있다.
onCreate, willFinishLaunchingWithOptions 을 본다면 이해하는 사람들도 있을 것이다.</p>
<p>Flutter 는 어떨까? 영상에서 저자는 <code>Widget 에는 생명주기가 없다</code> 고 한다</p>
<p><img src="https://velog.velcdn.com/images/ricky_0_k/post/bb5ffcdf-224d-404c-914c-0b13a89c67d1/image.png" alt=""></p>
<p>응? 그러면 그렇게 오늘 글은 끝? 당연히 아니다
위젯의 정의를 좀 더 깊게 알아볼 필요가 있다.</p>
<h2 id="케이크-와-widget-의-유사성">케이크 와 Widget 의 유사성</h2>
<p><img src="https://velog.velcdn.com/images/ricky_0_k/post/bbf2474a-3441-4999-83f3-3bb491a421e2/image.png" alt=""></p>
<p>여기서도 또 비유법이 나온다. 갑자기 층마다 맛이 다른 레이어 케이크가 나온다.
케잌을 만들 때 당연히 순서가 있을 것이고, 구체적인 지시사항이 있다면 따라야 한다.
이는 만드는 도중이라도 변경되지 않는 내용이다.</p>
<p>여기서 맛이 예상보다 별로였다면, 우리는 어떻게 해야할까?</p>
<p><img src="https://velog.velcdn.com/images/ricky_0_k/post/04aec0d6-6898-4461-9235-a708f722e498/image.png" alt=""></p>
<p>(또 뵙네요?) 일단 화가 나겠지만 참고... 다음에 케이크를 어떻게 만들지를 고민할 것이다.
밍밍했다면 더 강한 단 맛의 설탕을 사용할 것이고, 다른 맛을 첨가할 수도 있다.</p>
<p><img src="https://velog.velcdn.com/images/ricky_0_k/post/acd8a18d-1dc9-45db-a9d8-efd00bef8e4f/image.png" alt=""></p>
<p>위젯도 마찬가지이다. 각 위젯에 순서가 있고, 각 순서에 필수적인 구현사항이 있다면 따라야 한다.
그렇게 만들어진 앱은 변경이 불가한 것도 케이크 개념과 유사하다.</p>
<p>(결론을 빠르게 찾는 사람들을 위해), 그렇게 케이크와 Flutter 의 유사점은 아래와 같다.</p>
<ul>
<li>Widget 구현에 순서가 있다.</li>
<li>각 Widget 에 지시사항 ( = 필수 구현사항) 을 따라야 한다</li>
<li>생성된 앱은 변경이 불가능하다.</li>
</ul>
<h2 id="케이크-와-widget-의-차이">케이크 와 Widget 의 차이</h2>
<p>그런데 앱에서는 사용자 입력 등을 통해 Widget 을 변경할 수는 있다.
(이게 불가능하다고 생각하지 말자. 이게 안된다면 우리는 번호를 눌러 전화를 할 수 없다.)</p>
<p>Flutter 에서는 이걸 어떻게 처리할까?
케이크 전체! 즉 생성된 앱을 다시 만드는 방식으로 처리한다.
앱에 변화가 필요할 때 Flutter 에 <code>새로운 위젯 모음을 전달</code>한다</p>
<p>굉장히 큰 작업처럼 보이지만 Flutter 는 이를 잘할 수 있도록 설계되었다고 한다.</p>
<ul>
<li>다양한 element, render 객체들은, 위젯과 관련하여 <code>위치를 계속 추적</code>해 나감</li>
<li>상태에 따라 재구성이 필요한지 체크함</li>
<li>필요한 경우 화면 업데이트도 수행</li>
</ul>
<h2 id="widget-의-수명">Widget 의 수명</h2>
<p>다시 돌아왔다. 그래서 Widget 에는 수명이 있나?</p>
<p>그렇지 않다고 한다. 
앱에서 위젯이 생성되기도 하고 전혀 사용되지도 않다고 하며 자세한 건 flutter.dev 를 보라고 한다. 
그리고 진짜 끝이다.</p>
<p><img src="https://velog.velcdn.com/images/ricky_0_k/post/90fe1174-9d8e-48d4-a3ca-d2f9e22995c9/image.png" alt=""></p>
<p>아 하긴 외국 사람이지... 하라는 데로 flutter.dev 도 좀 보자...</p>
<h1 id="1-state">1. State</h1>
<p>먼저 Widget 의 생명주기를 이해하려면 State 개념을 먼저 알아야 한다.
State 는 말 그대로 <code>상태</code>이다. 상태의 예는 여러가지가 있다.</p>
<ul>
<li>텍스트 내용</li>
<li>글자 크기 및 색상</li>
<li>클릭 이벤트</li>
</ul>
<p>위 내용 이외에도 많은 것들이 있을 것이고, 뭉뚱그려 각 위젯이 가질 수 모든 있는 것들을 상태라 볼 수 있다.
Flutter 는 선언형으로 Widget 을 구성하므로 이 특징이 더 두드러진다.
선언형이 뭔가요? 싶으면 잘 쓴 글은 아니지만 <a href="https://velog.io/@ricky_0_k/%EC%9D%98%EC%8B%9D%EC%9D%98-%ED%9D%90%EB%A6%84%EC%9C%BC%EB%A1%9C-%EC%84%A0%EC%96%B8%ED%98%95-%EC%84%A0%EC%96%B8%ED%98%95-UI-%EA%B0%9C%EB%85%90-%EC%9E%AC%EC%A0%95%EB%A6%AC%ED%95%98%EA%B8%B0">여기</a>를 참고해보자 (앞 광고 굿)</p>
<h2 id="종류">종류</h2>
<p><a href="https://docs.flutter.dev/development/data-and-backend/state-mgmt/ephemeral-vs-app">공식 문서</a>를 보니 이것도 종류가 있는 듯 하다. 크게는 이렇게 이야기할 수 있다고 한다.</p>
<ol>
<li><p>임시 상태 (UI 상태, 로컬 상태)
단일 위젯에 깔끔하게 포함할 수 있는 상태</p>
<blockquote>
<p>ex</p>
<ul>
<li>BottomNavigationBar에서 현재 선택된 탭</li>
<li>애니메이션의 현재 상태</li>
<li>PageView의 현재 페이지</li>
</ul>
</blockquote>
<p>StateFulWidget 및 setState 등으로 간단하게 관리 가능</p>
</li>
<li><p>앱 상태
앱의 여러 부분에서 공유될 수 있는 상태</p>
<blockquote>
<p>ex</p>
<ul>
<li>로그인 정보</li>
<li>소셜 네트워킹 앱의 알림</li>
<li>커머스 앱의 장바구니</li>
</ul>
</blockquote>
<p>여러 화면 또는 기능에서 불릴 수 있으므로 이를 위한 상태 관리 라이브러리가 존재함
(bloC, provider, getX 등)</p>
</li>
</ol>
<p>정리하다보니 중요한 개념이라는 생각이 들었다.
더불어 &quot;어떤 상태를 사용하면될지&quot; 는 아래 순서도를 참고하면 된다.</p>
<p><img src="https://velog.velcdn.com/images/ricky_0_k/post/e7188a60-e714-4aad-b8aa-8a8321fea651/image.png" alt=""></p>
<p>그 외 안티 패턴, Provider, ChangeNotifie 등에 대한 이야기도 있는데, 내용이 산으로 갈 것 같아 <a href="https://docs.flutter.dev/development/data-and-backend/state-mgmt/simple">링크</a>만 이야기하려 한다.</p>
<h1 id="2-statelesswidget-statefulwidget">2. StatelessWidget, StatefulWidget</h1>
<p><img src="https://velog.velcdn.com/images/ricky_0_k/post/fe26991d-db2d-4130-a691-b1b28544d761/image.png" alt=""></p>
<p>Flutter Widget 은 앱의 상태값을 후술할 build() 함수를 통해 그려내고, 그렇게 나온 결과물을 UI 로 보여준다.
Flutter Widget 은 위에서 이야기한 State 유무로 위젯을 크게 두 개로 분류가 가능하다.</p>
<h2 id="1-statelesswidget"><a href="https://api.flutter.dev/flutter/widgets/StatelessWidget-class.html">1. StatelessWidget</a></h2>
<p>State 가 <code>없는 위젯</code>이라고 보통 이야기한다. 
하지만 개인적으로는 State 를 <code>관리하지 않는 위젯</code>이라고 본다.</p>
<p>State 가 존재하지 않아 관리할 방법이 없으므로, StatelessWidget 은 위젯 내용을 변경할 방법이 없다.
그래서 생명주기도 비교적 단순한 편이다. 만들어주고 끝이다.</p>
<p><a href="https://api.flutter.dev/flutter/widgets/State/build.html">1. build()</a></p>
<ul>
<li>위젯을 새로 그린다.</li>
<li>위젯을 렌더링 해줄 때마다 (= 그려줄 때마다) 호출된다.<ul>
<li>위젯이 트리에 처음 삽입될 때</li>
<li>위젯의 부모가 구성을 변경할 때</li>
<li>InheritedWidget 이 변경 사항에 종속될 때</li>
</ul>
</li>
</ul>
<h2 id="2-statefulwidget"><a href="https://api.flutter.dev/flutter/widgets/StatefulWidget-class.html">2. StatefulWidget</a></h2>
<p>이 역시 State 가 <code>있는 위젯</code>이라고 보통 이야기한다
하지만 개인적으로는 State 를 <code>관리하는 위젯</code> 이라고 본다.</p>
<p>관리하므로, StatefulWidget 은 위젯의 내용을 변경할 수 있다.</p>
<h3 id="createstate">createState()</h3>
<p>StatefulWidget 는 createState() 함수에 의해 별도의 State 를 생성하여 위젯을 관리한다.
그래서 해당 State 내에 Widget 생명주기 함수들이 존재한다.
build() 함수도 여기에 존재한다.</p>
<h3 id="함수-종류">함수 종류</h3>
<p>주기가 있으므로 StatefulWidget 에는 몇 가지 함수가 더 있다.
build() 를 이용해 위젯을 그리는 건 똑같다. 주로 마주할 함수만 이야기하고 넘어가려 한다.</p>
<p><a href="https://api.flutter.dev/flutter/widgets/State/initState.html">1. initState()</a>
이 개체가 트리에 처음 삽입될 때 호출된다.
각 State 내에서는 이 메서드를 <code>맨 처음</code>에 <code>정확히 한 번</code> 호출된다.</p>
<p><a href="https://api.flutter.dev/flutter/widgets/State/setState.html">2. setState()</a>
state 가 변경되었음을 State 객체에 알린다.
변경된 상태 값 내용들을 기반으로 build() 를 실행하여 현재 Widget 을 갱신해준다.</p>
<p><a href="https://api.flutter.dev/flutter/widgets/State/dispose.html">3. dispose()</a>
이 개체가 트리에서 영구적으로 제거될 때 호출된다.
각 State 내에서는 이 메서드를 <code>맨 나중</code>에 <code>정확히 한 번</code> 호출된다.
initState() 의 반대이다.</p>
<p>기타 함수들은 <a href="https://api.flutter.dev/flutter/widgets/State-class.html#instance-methods">여기</a> 에 명세되어 있다.</p>
<h2 id="3-widget-은-주기로-설명이-가능한가">3. Widget 은 주기로 설명이 가능한가?</h2>
<h3 id="1-상태가-없는-위젯-statelesswidget">1. 상태가 없는 위젯 (StatelessWidget)</h3>
<p>상태가 없는 위젯은 주기를 어떻게 설명해야 할까?</p>
<p><img src="https://velog.velcdn.com/images/ricky_0_k/post/4cb15807-59be-498c-86a2-b3b1bba96bee/image.png" alt=""></p>
<blockquote>
<p>만들어진 케익과 같은 상태인 StatelessWidget 은, 완료된 상태로 보아야 하는가? 아직 동작중인 상태로 봐야하는가?</p>
</blockquote>
<p>난 이 질문에 명확한 답을 하기 어려웠다.</p>
<h3 id="2-상태가-있는-위젯-statefulwidget">2. 상태가 있는 위젯 (StatefulWidget)</h3>
<p><img src="https://velog.velcdn.com/images/ricky_0_k/post/bfd5ec45-dfd0-4e1d-97b1-b29f3d00c1e7/image.png" alt=""></p>
<p>그나마 상태가 있는 위젯은 주기가 있다.
상태가 만들어지고, 위젯이 초기화하고, 위젯을 그리고, 위젯이 파괴되는 일련의 주기가 존재한다.</p>
<hr>
<p>주기보다 상태로 먼저 이야기가 진행되고, 상태가 있을 경우 비로소 주기가 만들어지는 모습을 보면서
왜 Flutter Widget 을 동영상에서 애매하게 표현했는지 약간은 이해할 수 있었다.</p>
<h1 id="결론">결론</h1>
<p>이 외에도 많은 내용들이 있었지만 다 제외하고 간단하게만 작성하였다.
GlobalKey, const, CustomWidget 적극 생성하기 등등 관심 있는 분들은 공식 문서를 더 봐도 좋을 것 같다.</p>
<hr>
<p>사실 글을 별 생각 없이 내 주관적으로 감성적으로 썼었는데, 쓰고 난 직후 조회수를 보고 깜짝 놀랐다.
이럴 줄 알았으면 좀 더 노력해서 쓸 걸 하는 아쉬움이 있다.
추후 이 시리즈를 마치게 되면 이 포스트 내용은 필히 보강해야겠다 😅</p>
<h1 id="참고">참고</h1>
<ul>
<li><a href="https://devmg.tistory.com/186">https://devmg.tistory.com/186</a></li>
<li><a href="https://docs.flutter.dev/development/data-and-backend/state-mgmt/intro">https://docs.flutter.dev/development/data-and-backend/state-mgmt/intro</a></li>
<li><a href="https://api.flutter.dev/flutter/widgets/StatelessWidget-class.html">https://api.flutter.dev/flutter/widgets/StatelessWidget-class.html</a></li>
<li><a href="https://api.flutter.dev/flutter/widgets/StatefulWidget-class.html">https://api.flutter.dev/flutter/widgets/StatefulWidget-class.html</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter 디코딩 하기 시리즈] 1. hot reload?!]]></title>
            <link>https://velog.io/@ricky_0_k/Flutter-%EB%94%94%EC%BD%94%EB%94%A9-%ED%95%98%EA%B8%B0-%EC%8B%9C%EB%A6%AC%EC%A6%88-1.-hot-reload</link>
            <guid>https://velog.io/@ricky_0_k/Flutter-%EB%94%94%EC%BD%94%EB%94%A9-%ED%95%98%EA%B8%B0-%EC%8B%9C%EB%A6%AC%EC%A6%88-1.-hot-reload</guid>
            <pubDate>Sun, 19 Mar 2023 16:13:25 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p>첫 단원이다. Youtube 가 임베드가 안 되어 살짝 불편하긴 하다.</p>
<h1 id="소개">소개</h1>
<p><a href="https://youtu.be/sgPQklGe2K8?list=PLjxrf2q8roU1fRV40Ec8200rX6OuQkmnl">https://youtu.be/sgPQklGe2K8?list=PLjxrf2q8roU1fRV40Ec8200rX6OuQkmnl</a></p>
<p>hot reload 에 관련 내용을 정리하는 영상이었다.</p>
<h2 id="hot-reload">hot reload</h2>
<p>간단하게 이야기하면, 화면 수정 사항을 <strong>즉시</strong> 볼 수 있는 기능이다.
이해가 안된다면 바로 아래 gif 를 보자</p>
<p><img src="https://velog.velcdn.com/images/ricky_0_k/post/03812574-5299-47cf-aafa-66f8005dfbbd/image.gif" alt=""></p>
<p>기존 android, iOS 네이티브에서는 변경 내역을 확인하려면,
빌드 과정을 거치고, 직접 특정 화면을 들어가서 확인 할 수 있다.</p>
<p>하지만 hot reload 는 실행만 하면 위와 같이 <code>앱 화면</code>에서 직접 볼 수 있다. 그 것도 <code>실행 즉시</code> 말이다.
몇 분 이상 소요될 수 있는 확인 작업을, flutter 에서는 몇 초 내로 확인할 수 있다.</p>
<p>이게 왜 가능할까?</p>
<h2 id="hot-reload-시-실제-실행되는-내용">hot reload 시 실제 실행되는 내용</h2>
<p><img src="https://velog.velcdn.com/images/ricky_0_k/post/a98067e4-12e4-4dc1-96e2-85d43e5730fe/image.png" alt=""></p>
<p>hot reload 는 코드 변경 사항을 구동중인 dart 가상 머신에 업데이트 하고
위젯 트리의 <code>루트 위젯에서부터</code> 위젯 트리를 다시 <code>빌드 (build)</code>한다.</p>
<p>오롯이 build() 만 재실행하기 때문에, main() 또는 initState() 을 다시 실행하지 않는다.
아래에 예를 몇가지 들어보았다. 아래의 경우엔 hot reload 하더라도 변화가 없다.</p>
<ol>
<li>main() 에 runApp 하는 루트 위젯을 변경</li>
<li>build() 이외 함수 (ex. initState(), dispose()) 에 추가 작업</li>
<li>정적 변수 추가</li>
<li>StatelessWidget -&gt; StatefulWidget </li>
</ol>
<p>위의 경우에는 재시작 개념인 <code>hot restart</code> 를 사용하면 된다. 
hot restart 를 통해 state 까지 새로 설정하여 루트 위젯 빌드가 가능하다.
여기는 hot reload 관련 내용이니 깊게 설명은 하지 않고 넘어간다.</p>
<h2 id="hot-reload-가-안-되는-경우">hot reload 가 안 되는 경우</h2>
<p>dart 가상 머신에 들어가는 코드 또는 앱 자체에 문제가 있을 경우 hot reload 를 사용할 수 없다.
수정한 코드에 오류가 있다거나, 앱이 종료된다거나, dart 이외의 영역(Kotlin, swift 등) 을 수정하면 hot reload 는 동작하지 않는다</p>
<p>기타 특정 케이스도 존재한다.</p>
<ul>
<li>class -&gt; enum, </li>
<li><a href="https://github.com/flutter/flutter/issues/43574">CupertinoTabView’s builder</a></li>
<li>Generic 변경 등</li>
</ul>
<p>그 외에도 다양한 경우는 <a href="https://docs.flutter.dev/development/tools/hot-reload#special-cases">공식 문서</a>에 설명되어 있다.</p>
<h2 id="how-it-works">How it works</h2>
<p>hot reload 가 호출되면 호스트 시스템은 <code>마지막 컴파일 이후 편집된 코드</code>를 확인한다. 
이후 다음 라이브러리가 <code>다시 컴파일</code>되는 방식으로 실행된다.</p>
<p>소스 코드는 <a href="https://github.com/dart-lang/sdk/tree/master/pkg/kernel">커널 파일</a>로 컴파일되어 모바일 장치의 Dart VM 으로 보내진다.
이후로 실행되는 내용은 위의 <code>hot reload 시 실제 실행되는 내용</code> 과 같다.</p>
<h2 id="다른-플랫폼에서는-왜-불가능할까">다른 플랫폼에서는 왜 불가능할까?</h2>
<p>이게 가능한 이유는 dart 의 컴파일 방식 덕분이다.</p>
<p>dart 에서는 경우에 따라 컴파일 방식을 다르게 할 수 있는데
디버그용 앱에서는 JIT(Just-In-Time) 방식, 릴리즈 앱에서는 AOT(Ahead-of-time) 방식으로 컴파일 할 수 있다.</p>
<ul>
<li>JIT : 프로그램을 <code>실행할 때</code> 컴파일</li>
<li>AOT : 프로그램 <code>실행 전</code> 에 컴파일</li>
</ul>
<p>비유할 수 있는 걸 생각해봤는데 음... 짜장면을 받는다고 할 때</p>
<ul>
<li>JIT 는 면과 짜장 소스를 따로 받는 경우?</li>
<li>AOT 는 이미 소스에 비벼진 짜장면을 받는 경우?</li>
</ul>
<p>로 비유할 수 있을 것 같다. (<code>짜장면을 비비는 과정</code>을 컴파일로 비유할 수 있을 것이다 🫠)</p>
<p><img src="https://velog.velcdn.com/images/ricky_0_k/post/429c9821-6869-41db-a608-e6d8088c23b1/image.png" alt=""></p>
<p>기왕에 비유법을 활용한 거 좀 더 깊게 들어가보자.
만약 짜장면이 나왔는데 내용물에 벌레가 나와서 다시 받는 상황을 가정해보려 한다.</p>
<p><img src="https://velog.velcdn.com/images/ricky_0_k/post/886069ad-422c-448a-8d35-85f3f8faa4c1/image.png" alt=""></p>
<p>일단 화가 나겠지만 참고... 우리는 당장 배고프기 때문에 짜장면을 다시 받아야 할 것이다.
각 방식은 어떻게 짜장면을 다시 받을 수 있을까?</p>
<p>AOT 는 이미 비벼진 짜장면에서 벌레가 나왔기 때문에 다시 짜장면을 받아야 할 것이다.
면부터 소스까지 새로 만들고 비벼서 받아야 할 것이다.</p>
<p>JIT 는 어떨까? 소스나 면 둘 중 하나에서 벌레가 나왔을 것이다.
그러면 면과 소스 중 벌레 나온 것을 바꾸고, 비벼 먹으면 될 것이다.
(물론 <code>소스를 비비는 도중에 소스에서 나왔다</code> 등 특이한 경우에는 AOT 처럼 해야할 수도 있다)</p>
<p><img src="https://velog.velcdn.com/images/ricky_0_k/post/242b536f-cffa-4102-a46f-293aac3f59e9/image.png" alt=""></p>
<p>어쨌거나 JIT 는 전체 구성 변경 없이 <code>일부만 바꾸어서</code> 처리가 가능할 수도 있다.
여기서 <code>일부</code>를 <code>build() 함수 내용</code>들로 대치해서 생각해본다면 
우리는 비로소 JIT 방식을 활용해 hot reload 가 왜 가능한지 알 수 있다.</p>
<p>참고로 위에서 살짝 언급한 hot restart 도 JIT 방식 덕분에 활용이 가능하다.</p>
<h2 id="기타">기타</h2>
<p>다트에서 사용하는 컴파일 이름도 따로 있다
<code>dart2native</code> 이며 관련 내용은 따로 언급하지 않겠다.</p>
<p>그리고 안드로이드에서도 예전엔 JIT 방식을 지원했었다.
하지만 특정 버전 이후 (21 인지 기억이 가물..) AOT 방식으로 변경 및 획일화 되었다.</p>
<p>당시에 JIT 방식을 경험하면서 온갖 버그들을 많이 경험했던만큼
당시 안드에서 hot reload, restart 가 가능했어도 별 감흥은 없었을 것 같다.</p>
<h2 id="마무리">마무리</h2>
<p>글이 길어졌지만 결국 <code>JIT 컴파일 방식</code> 덕분에 우리는 <code>hot reload</code> 를 사용할 수 있다.
이 덕분에 우리는 Flutter 에서 UI 개발을 하는데 걸리는 시간을 줄일 수 있다.</p>
<p>참고</p>
<ul>
<li><a href="https://docs.flutter.dev/development/tools/hot-reload#special-cases">https://docs.flutter.dev/development/tools/hot-reload#special-cases</a></li>
<li><a href="https://www.youtube.com/watch?v=sgPQklGe2K8&amp;list=PLjxrf2q8roU1fRV40Ec8200rX6OuQkmnl&amp;index=25">https://www.youtube.com/watch?v=sgPQklGe2K8&amp;list=PLjxrf2q8roU1fRV40Ec8200rX6OuQkmnl&amp;index=25</a></li>
<li><a href="https://velog.io/@restl2seung/Flutter-reload%EA%B0%80-%EA%B0%80%EB%8A%A5%ED%95%9C-%EC%9D%B4%EC%9C%A0">https://velog.io/@restl2seung/Flutter-reload%EA%B0%80-%EA%B0%80%EB%8A%A5%ED%95%9C-%EC%9D%B4%EC%9C%A0</a></li>
<li><a href="https://docs.flutter.dev/resources/faq#why-did-flutter-choose-to-use-dart">https://docs.flutter.dev/resources/faq#why-did-flutter-choose-to-use-dart</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter 디코딩 하기 시리즈] 0. 소개]]></title>
            <link>https://velog.io/@ricky_0_k/Flutter-%EB%94%94%EC%BD%94%EB%94%A9-%ED%95%98%EA%B8%B0-%EC%8B%9C%EB%A6%AC%EC%A6%88-0.-%EC%86%8C%EA%B0%9C</link>
            <guid>https://velog.io/@ricky_0_k/Flutter-%EB%94%94%EC%BD%94%EB%94%A9-%ED%95%98%EA%B8%B0-%EC%8B%9C%EB%A6%AC%EC%A6%88-0.-%EC%86%8C%EA%B0%9C</guid>
            <pubDate>Sun, 19 Mar 2023 11:17:22 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p>최근 다루고 있는 flutter 관련해서 정리해보려 한다
딮한 부분을 다뤄보고 싶다 생각하던 도중, 해당 시리즈가 생각나 다시 영상을 보며 정리하려 한다.</p>
<h1 id="소개">소개</h1>
<p><a href="https://youtu.be/QIW35-vcA2o?list=PLjxrf2q8roU1fRV40Ec8200rX6OuQkmnl">https://youtu.be/QIW35-vcA2o?list=PLjxrf2q8roU1fRV40Ec8200rX6OuQkmnl</a></p>
<h2 id="flutter-관련된-deep-dive">flutter 관련된 deep dive</h2>
<ul>
<li>hot reload 가 작동하지 않는 이유</li>
<li>위젯의 생명주기</li>
<li>상태와 관련된 것들의 동작 방식</li>
</ul>
<h1 id="마무리">마무리</h1>
<p>첫 소개라 그런지 어떤 내용을 다룰 건지에 대한 소개가 전부였다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Compose 무작정 맛보기 [6. 설정화면 및 마무리]]]></title>
            <link>https://velog.io/@ricky_0_k/Compose-%EB%AC%B4%EC%9E%91%EC%A0%95-%EB%A7%9B%EB%B3%B4%EA%B8%B0-6.-%EC%84%A4%EC%A0%95%ED%99%94%EB%A9%B4-%EB%B0%8F-%EB%A7%88%EB%AC%B4%EB%A6%AC</link>
            <guid>https://velog.io/@ricky_0_k/Compose-%EB%AC%B4%EC%9E%91%EC%A0%95-%EB%A7%9B%EB%B3%B4%EA%B8%B0-6.-%EC%84%A4%EC%A0%95%ED%99%94%EB%A9%B4-%EB%B0%8F-%EB%A7%88%EB%AC%B4%EB%A6%AC</guid>
            <pubDate>Thu, 21 Jul 2022 16:17:24 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p>정말 오랜만의 포스팅이다. 유구무언이다..</p>
<p>포스팅을 써야지 써야지는 마음 먹었는데...
정신적으로 지쳤어서 쉬고 싶다는 생각이 컸고, 
이것 저것 일들이 겹쳐져 미루다가 지금까지 오게 되었다.</p>
<p>마음의 여유가 이제 약간은 생겼고, 
개인적으로 했던 약속이나마 지켜야겠다는 생각이 들어
해당 Compose 맛보기 경험의 마무리를 지어보려 한다. </p>
<h1 id="화면에-대한-소개">화면에 대한 소개</h1>
<p>몇 개월만에 글을 적다보니, 내가 어떤 목차로 글을 작성했는지도 잊어버렸다...</p>
<h2 id="1-설정화면">1. 설정화면</h2>
<p><img src="https://velog.velcdn.com/images/ricky_0_k/post/9818411c-9f70-4275-b34a-1b3241a99e79/image.png" alt=""></p>
<p>UI 적으로는 비교적 간단하다.
각 설정 아이템 뷰가 있고 이는 리스트로 구현되어 있다.</p>
<p>다만 이전에 만들었던 화면들과 연결되는 요소가 많다.
(ex. 기존 점수 설정, 졸업 학점 설정 등)
위와 같이 경우에 따라 띄워지는 토스트 메시지도 다르기에 
Intent 를 통해 화면 이동 및 결과 받기 작업을 해주어야 했다.</p>
<p>그런데 Compose 에서는 startActivityForResult, onActivityResult 를 사용하기 애매한 구조로 되어 있던 기억이었다.
마침 해당 기능이 deprecated 되어 있기도 해서 ActivityResultContracts 를 활용해 구현하였다.</p>
<h1 id="설정-화면">설정 화면</h1>
<p>그럼 바로 이야기를 진행해보겠다.</p>
<h2 id="설정-화면-전체-view">설정 화면 전체 View</h2>
<pre><code class="language-kotlin">fun SettingView(vm: SettingViewModel? = null) {
    // 1
    val context = LocalContext.current
    val activity = LocalContext.current as SettingActivity

    // 2
    val launcher = rememberLauncherForActivityResult(
        ActivityResultContracts.StartActivityForResult()
    ) {
        when (it.resultCode) {
            Activity.RESULT_CANCELED -&gt; {
                Toast.makeText(context, &quot;작성 혹은 선택을 취소하였습니다.&quot;, Toast.LENGTH_SHORT).show()
            }
            Const.RESULT_INIT_OK -&gt; {
                val messageText = StringBuilder()
                val score = it.data?.getIntExtra(Const.EXTRA_RESULT_INIT_SCORE, 0)
                messageText.append(
                    if (score == 45) R.string.toast_setting_to_4_5_warning.getString(context)
                    else R.string.toast_setting_change_success.getString(context)
                )
                vm?.sendToast(messageText)
                MyApplication.sCalculate()
            }
            Const.RESULT_GRADUATION_OK -&gt; {
                vm?.sendToast(R.string.toast_setting_change_success.getString(context))
                MyApplication.sCalculate()
            }
        }
    }

    val titles = stringArrayResource(id = R.array.tv_setting_titles)

    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(color = colorResource(id = R.color.white))
    ) {
        Toolbar(
            navigationIcon = { vm?.let { BackButton(it) } },
            titleRes = R.string.tv_setting_title
        )

        Divider(
            modifier = Modifier
                .height(10.dp)
                .background(color = colorResource(id = R.color.grayBrightColor))
        )

        // 3
        val scoreClickAction: () -&gt; Unit = {
            launcher.launch(Intent(activity, InitActivity::class.java))
        }

        val graduateClickAction: () -&gt; Unit = {
            launcher.launch(Intent(activity, GraduationActivity::class.java))
        }

        val versionClickAction: () -&gt; Unit = {
            val str = &quot;market://details?id=${context.packageName}&quot;
            val intent = Intent()
            intent.action = Intent.ACTION_VIEW
            intent.data = Uri.parse(str)
            launcher.launch(intent)
        }

        val messageClickAction: () -&gt; Unit = {
            val emailIntent = Intent(Intent.ACTION_SEND).apply {
                putExtra(Intent.EXTRA_EMAIL, arrayOf(&quot;...&quot;))
                ...
            }
            try {
                launcher.launch(Intent.createChooser(emailIntent, &quot;메일로 문의하기&quot;))
            } catch (ex: android.content.ActivityNotFoundException) {
                vm?.sendToast(&quot;There are no email clients installed.&quot;)
            }
        }

        LazyColumn(modifier = Modifier.fillMaxWidth()) {
            items(count = titles.size) { index -&gt;
                SettingItemView(
                    index,
                    titles[index],
                    when {
                        vm == null -&gt; null
                        index == 0 -&gt; scoreClickAction
                        index == 1 -&gt; graduateClickAction
                        index == 2 -&gt; versionClickAction
                        index == 3 -&gt; messageClickAction
                        else -&gt; null
                    }
                )
            }
        }
    }
}</code></pre>
<ol>
<li><p>지난 포스트에서도 언급했던 context 가져오는 방식이다.
지난번에 이야기했다시피 application 차원의 context 를 가져오려면
LocalContext.current.applicationContext 를 통해 가져와야 한다.</p>
</li>
<li><p>방금 언급했다시피 startActivityForResult()와 onActivityResult() 가 deprecated 로 되어 있다.
이에 대해 구글은 Activity Result API 사용을 적극 권장하고 있다. 그래서 사용해보았다.</p>
<p><a href="https://developer.android.com/training/basics/intents/result?hl=ko">공식 문서</a> 를 읽다보니 &quot;<code>메모리 부족</code>으로 인해 <code>process와 Activity 가 소멸</code> 되는 케이스&quot; 때문에 
해당 API 사용을 적극 권장하는 듯한 생각이 들었다.</p>
<p><code>다른 화면을 호출</code>하여 <code>돌아온 후 처리</code>를 작성하는 방식은 여러가지가 있다.
그중 눈에 보였던 것 <code>2개 방식</code>을 이야기하려 한다.</p>
<ol>
<li>registerForActivityResult 를 활용한 <code>1:1 대응</code> 방식 <pre><code class="language-kotlin">// 호출 방법
getContent.launch(&quot;image/*&quot;)
</code></pre>
</li>
</ol>
<p>...</p>
<p>// 결과 처리
val getContent = registerForActivityResult(GetContent()) { uri: Uri? -&gt;
  // Handle the returned Uri
}</p>
<pre><code>getContent 를 통해 `하나의 화면 이동`에 대해, `하나의 결과 처리`만 해주는 것을 확인할 수 있다. 

2. StartActivityForResult 를 활용한 `1:다 대응` 방식
```kotlin
// 호출 방법
getContent.launch(&quot;image/*&quot;)

...

// 결과 처리
val startForResult = registerForActivityResult(StartActivityForResult()) { result: ActivityResult -&gt;
  if (result.resultCode == Activity.RESULT_OK) {
    val intent = result.data
    // Handle the Intent
  } else if (....) {
    ....
  }
}</code></pre><p>startForResult 를 통해 <code>하나의 화면 이동</code>에 대해, 조건문으로 <code>여러개의 결과 처리</code> 를 해주는 것을 확인할 수 있다. </p>
<p>나는 설정화면에서 1:다 대응 방식을 통해 다른 화면 호출 및 결과 처리를 해주었다.
<code>rememberLauncherForActivityResult</code> 를 통해 Contract 를 등록하고 결과 처리를 해주는 건 동일하다.
자세한 내용은 <a href="https://developer.android.com/jetpack/compose/libraries?hl=ko#activity_result">공식 문서</a> 에서 확인이 가능하다.</p>
</li>
<li><p>각 설정 아이템 뷰에서 사용할 이벤트 명세이다. (<code>____Action</code>)
2번의 연장선으로 <code>launch(Intent)</code> 를 통해 화면 이동을 하는 것을 확인할 수 있다.</p>
</li>
</ol>
<h2 id="설정-화면-아이템-view">설정 화면 아이템 View</h2>
<p>정말 별거 없다. (사실은 내가 복습용으로 넣은 내용이라 카더라)</p>
<pre><code class="language-kotlin">@Composable
fun SettingItemView(index: Int, title: String, action: (() -&gt; Unit)?) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .clickable(
                enabled = true,
                interactionSource = remember { MutableInteractionSource() },
                indication = rememberRipple(bounded = true),
                onClick = { action?.let { it() } }
            )
    ) {
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .padding(start = 32.dp, end = 32.dp, top = 16.dp, bottom = 16.dp)
        ) {
            Text(
                modifier = Modifier.align(Alignment.CenterStart),
                text = title,
                fontSize = 15.sp,
                color = colorResource(id = R.color.defaultTextColor)
            )
            if (index == 2)
                Text(
                    modifier = Modifier.align(Alignment.CenterEnd),
                    text = &quot;v${BuildConfig.VERSION_NAME}&quot;,
                    fontSize = 15.sp,
                    color = colorResource(id = R.color.defaultTextColor)
                )
            else
                Image(
                    modifier = Modifier.align(Alignment.CenterEnd),
                    painter = painterResource(id = R.drawable.setting_next),
                    contentDescription = &quot;setting_next&quot;
                )
        }

        Divider(
            modifier = Modifier
                .height(1.dp)
                .background(color = colorResource(id = R.color.statisticTabColor))
        )
    }
}</code></pre>
<p>Modifier, remember, clickable 을 활용한 단출한 코드이다.</p>
<h1 id="이후로-내가-한-작업들">이후로 내가 한 작업들</h1>
<p>난 설정화면 작업 이후로 아래 작업들을 수행했다.</p>
<ol>
<li>패키지 구조 일괄 정리</li>
<li>Application 에 있었던 비즈니스 로직 Repository 로 옮김</li>
<li>일부 비효율적인 로직 리펙터링</li>
<li>DB 로직 정리</li>
<li>자잘한 버그 수정 및 기능 개선 (ex. x학년 x학기 개행 안되는 내용, 꺾은선 그래프 커스텀뷰로 만들기 등)</li>
<li>Clean Architecture 개념에 최대한 입각하여 프로젝트 멀티 모듈화</li>
<li>Room 테이블 정리 및 마이그레이션 처리</li>
<li>오버 엔지니어링 (백그라운드에서 종료될 시 shimmer view 보여주기 등) 및 주석 정리</li>
</ol>
<p>한 10일 정도 걸린 것 같고, 그 이후로 바로 런칭을 했다.
원래는 위 내용들도 싸그리 포스팅 주제로 다루려고 했으나.....
보여주기 위험한 부분들도 보이고, 시리즈의 끝이 보이지 않을듯하여 이렇게 언급만 하고 마무리를 지으려 한다. </p>
<h1 id="마무리">마무리</h1>
<p>이전 포스트에서 살짝 언급했었지만, 
나는 해당 앱을 주제로 <code>새로운 기능 개선</code> 및 <code>유저의 이야기를 반영하는</code> 모든 과정 하나하나를 
포스트로 작성하려 했었다. 일종의 로그형 포스트랄까? </p>
<p>실제로 첫 주제로 잡고 있던 것도 있었다.
<code>멀티 모듈에 맞춘 CD 로직</code> 을 작성중이었고, 
그에 따른 versionCode 업로드 로직도 다 만들어놓은 상태였기 때문이다.</p>
<p>하지만 개인의 사정이 생겨 <code>유지보수</code>만 개인적으로 진행하고, 
포스팅할 주제가 생긴다면 그 주제를 가지고 포스트를 남기려 한다.</p>
<p>나중에 이야기하겠지만, 나의 2022년은 강화가 아닌 <code>도전</code>의 느낌이 강해졌다.
이러면서 2022년 계획도 완전히 바뀌게 되었다.</p>
<p>빚쟁이에게 빚을 진 느낌으로 로그형 포스트를 만들기보다는,
내가 <code>개발하면서 마주한 빛</code> 을 주제로 포스팅을 하는 게 더 유의미할거란 생각이 들어
이 주제는 잠시 개인적인 공간에 두고 마무리 지으려 한다.</p>
<p>시리즈 마무리는 지었지만 
약속을 완벽하게 지키지 못한 나 자신에게 
먼저 사과를 건네며 이 글을 마무리한다.</p>
<h1 id="참고">참고</h1>
<ol>
<li><a href="https://developer.android.com/jetpack/compose/libraries?hl=ko#activity_result">https://developer.android.com/jetpack/compose/libraries?hl=ko#activity_result</a></li>
<li><a href="https://developer.android.com/training/basics/intents/result?hl=ko">https://developer.android.com/training/basics/intents/result?hl=ko</a></li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[Compose 무작정 맛보기 [5. 학점 통계 및 크롭 화면]
]]></title>
            <link>https://velog.io/@ricky_0_k/Compose-%EB%AC%B4%EC%9E%91%EC%A0%95-%EB%A7%9B%EB%B3%B4%EA%B8%B0-5.-%ED%95%99%EC%A0%90-%ED%86%B5%EA%B3%84-%EB%B0%8F-%ED%81%AC%EB%A1%AD-%ED%99%94%EB%A9%B4</link>
            <guid>https://velog.io/@ricky_0_k/Compose-%EB%AC%B4%EC%9E%91%EC%A0%95-%EB%A7%9B%EB%B3%B4%EA%B8%B0-5.-%ED%95%99%EC%A0%90-%ED%86%B5%EA%B3%84-%EB%B0%8F-%ED%81%AC%EB%A1%AD-%ED%99%94%EB%A9%B4</guid>
            <pubDate>Mon, 11 Apr 2022 03:11:46 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p>이 화면에서는 시행착오보다는 Compose 내에서 기존의 뷰를 사용하는 방법을 고민했던 시간이 더 많았다.</p>
<p>그리고 반드시 해야하는 작업으로 생각했던 건 Crop 기능 개선!
Crop 라이브러리 코드를 그대로 때려박았기 때문에 반드시 수정이 필요한 코드였다.</p>
<p>작업 시작 날짜를 보니 1월 15일이다. (TMI 로 코로나 걸리기 7일전의 나였다)
이 당시의 나는 어떻게 작업을 했었을까?</p>
<h1 id="화면에-대한-소개">화면에 대한 소개</h1>
<p>저번에 화면에 대한 소개를 넣어보니 개인적으로 좋았다.
그래서 이번에도 추가한다 ㅇㅅㅇ</p>
<h2 id="1-학점-통계-화면-기본">1. 학점 통계 화면 (기본)</h2>
<p><img src="https://velog.velcdn.com/images/ricky_0_k/post/92cdff4b-f839-46e2-8ad3-8d784b26caa4/image.png" alt=""></p>
<p>갑자기 범블비로 업뎃하면서 안스 내에 내장이 되어있다. (이거 원래대로 밖에 빼는 방법 아시는 분 댓글좀...)
전체, 각 학기 탭이 있고, 각 카테고리 별로 아래의 3단 그래프들과, 꺾은선 그래프들을 볼 수 있다.
그리고 우측 상단의 버튼을 통해 화면 캡처를 할 수 있다.</p>
<p>BottomSheet 가 전체를 아우르고, 그 안에 ToolBar, TabLayout, ContentView(Fragment) 등이 들어있다.</p>
<h2 id="2-크롭-화면-및-선택-후-화면">2. 크롭 화면 및 선택 후 화면</h2>
<p><img src="https://velog.velcdn.com/images/ricky_0_k/post/428c116a-70f4-423e-a637-db91d4e56509/image.png" alt=""></p>
<p>위와 같이 크롭 화면을 지정하여 카카오톡, 페이스북으로 공유할 수 있는 프로세스이다.
크롭하기를 선택하면 bottom sheet 가 나오고 카카오톡 공유하기, <del>페북 게시글 쓰기</del> 를 할 수 있다.</p>
<p>페북 게시글 쓰기 취소선도 설명이 필요할 듯 한데.... 최신 버전 앱에서는 아직 불가능하다. 
페이스북에서 queries 권한을 설정하라고 했는데, 하라는 대로 했는데도 안 되어서... 
일단 대응만 해놓고 개선 작업으로 두었다..</p>
<h1 id="학점-통계-화면">학점 통계 화면</h1>
<p>앞서 이야기했다시피 난 Compose 내에서 기존 xml 뷰를 쓰고 싶다고 했었다.
그리고 Compose ViewPager 와 xml 의 ViewPager 를 한번 비교해보고 싶었다.
그래서 전체를 아우르는 화면은 Composable View 로 구현하였다</p>
<h2 id="1-학점-통계화면-bottomsheet-view">1. 학점 통계화면 BottomSheet View</h2>
<p>이번에도 전체 코드를 올리면서 키포인트들을 설명해보려 한다.
코드를 보면 알겠지만 <code>BottomSheetScaffold</code> 가 전체 View 를 감싸는 구조이다. 
그렇다보니 설명을 어떻게할지 고민했었고, 전체 View 보다는 BottomSheet View 를 먼저 설명하게 되었다.</p>
<pre><code class="language-kotlin">@OptIn(ExperimentalMaterialApi::class)
@Composable
fun StatisticView(vm: StatisticViewModel? = null) {
    // 1
    val context = LocalContext.current
    val activity = context as StatisticActivity

    // 2
    val coroutineScope = rememberCoroutineScope()

    // 3
    val bottomSheetState by remember {
        mutableStateOf(BottomSheetState(BottomSheetValue.Collapsed))
    }
    var isVisible by remember { mutableStateOf(false) }

    // 4
    val bottomSheetScaffoldState =
        rememberBottomSheetScaffoldState(bottomSheetState = bottomSheetState).apply {
            if (bottomSheetState.isCollapsed) coroutineScope.launch { isVisible = false }
        }

    // 5
    var statisticViewPager: ViewPager2? = null
    fun getStatisticViewPager(context: Context): ViewPager2 {
        statisticViewPager = statisticViewPager ?: ViewPager2(context).apply {
            layoutParams = ViewGroup.MarginLayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT
            )
            adapter = StatisticPagerAdapter(activity)
            offscreenPageLimit = 1
        }
        return statisticViewPager!!
    }

    // 6
    val result = remember { mutableStateOf&lt;CropImageView.CropResult?&gt;(null) }
    val cropResultLauncher = rememberLauncherForActivityResult(CropImageContract()) {
        Timber.i(&quot;$result&quot;)

        coroutineScope.launch {
            result.value = it
            isVisible = if (result.value?.isSuccessful == true) {
                bottomSheetState.expand()
                true
            } else {
                PhotoUtil.INSTANCE.clear()
                // throw result.value?.error ?: Throwable()
                result.value?.error?.printStackTrace()
                false
            }
        }
    }

    // 7
    /** 권한 관련 ActivityResult 처리 */
    val permissionLauncher = rememberLauncherForActivityResult(
        ActivityResultContracts.RequestMultiplePermissions()
    ) {
        if (it.values.filter { isGranted -&gt; isGranted }.size == 2) {
            val bitmap = activity.window.decorView.rootView.getBitmapFromView()
            val originalUri = PhotoUtil.INSTANCE.saveBitmapUri(context.applicationContext, bitmap)
            cropResultLauncher.launch(
                CropImageContractOptions(
                    originalUri, CropImageOptions().apply {
                        guidelines = CropImageView.Guidelines.ON
                        customOutputUri = originalUri
                    }
                )
            )
        } else vm?.sendToast(R.string.toast_crop_permission_error)
    }

    // 8
    BottomSheetScaffold(
        sheetElevation = 0.dp,
        sheetBackgroundColor = colorResource(id = R.color.transparent),
        backgroundColor = colorResource(id = R.color.transparent),
        scaffoldState = bottomSheetScaffoldState,
        sheetContent = {
            // 9
            AnimatedVisibility(
                visible = isVisible,
                enter = fadeIn(animationSpec = tween(1000)),
                exit = fadeOut(animationSpec = tween(0)),
            ) {
                // 10
                Box(
                    modifier = Modifier
                        .fillMaxSize()
                        .background(color = colorResource(id = R.color.transparent_gray))
                        .clickable(
                            interactionSource = remember { MutableInteractionSource() },
                            indication = null,
                        ) {
                            coroutineScope.launch {
                                bottomSheetState.collapse()
                                isVisible = false
                            }
                        },
                ) {
                    Column(
                        Modifier
                            .fillMaxWidth()
                            .background(color = colorResource(id = R.color.white))
                            .align(Alignment.BottomCenter),
                        verticalArrangement = Arrangement.Center,
                        horizontalAlignment = Alignment.CenterHorizontally
                    ) {
                        Text(
                            modifier = Modifier
                                .padding(12.dp)
                                .clickable(
                                    enabled = true,
                                    interactionSource = remember { MutableInteractionSource() },
                                    indication = rememberRipple(bounded = true),
                                    onClick = {
                                        vm?.startSnsAction(ShareSNSAction.KAKAO)
                                        // 11
                                        coroutineScope.launch {
                                            bottomSheetState.collapse()
                                            isVisible = false
                                        }
                                    }
                                ),
                            text = stringResource(id = R.string.tv_statistic_share_kakao)
                        )
                        Text(
                            modifier = Modifier
                                .padding(12.dp)
                                .clickable(
                                    enabled = true,
                                    interactionSource = remember { MutableInteractionSource() },
                                    indication = rememberRipple(bounded = true),
                                    onClick = {
                                        vm?.startSnsAction(ShareSNSAction.FACEBOOK)
                                        // 11
                                        coroutineScope.launch {
                                            bottomSheetState.collapse()
                                            isVisible = false
                                        }
                                    }
                                ),
                            text = stringResource(id = R.string.tv_statistic_share_facebook)
                        )
                    }
                }
            }
        },
        sheetPeekHeight = 0.dp,
        modifier = Modifier.fillMaxSize(),
    ) { ... }
}</code></pre>
<ol>
<li><p>리소스 가져오는 데 context 를 안쓰다보니 이 값들을 가져오는 방법에 대해 전혀 신경을 안쓰고 있었다.
하지만 xml View 를 구현하려다 보니 필요해져 이런식으로 가져올 수 있음을 알게 되었다.
여기서 응용하여 application 차원의 context 를 가져오려면 어떻게 해야할까?
<code>LocalContext.current.applicationContext</code> 를 통해 가져올 수 있다.</p>
</li>
<li><p>의외로 코루틴 스코프를 선언하는 게 매우 쉬웠다. 
처음에 나는 viewModelScope 를 사용했었지만 동작 도중 (ex. 크롭 화면 다녀온 후, 화면이 다시 백그라운드에서 보여질 때 등) 먹통이 될 때가 있었다. 그러다가 이 방법을 알고 이대로 처리하게 되었다.</p>
<p>정의부를 보았을 때 무슨 Dispatcher 를 사용하는지 알기 힘들어 전문가의 글들을 좀 검색해보았다.
참고해보니 이런 문구가 있었다.</p>
<blockquote>
<p>Composable 내부에서 코루틴을 수행할 경우 Composable 에 대한 Recomposition 이 일어날 때 정리되어야 하는 Coroutine 이 정리가 안된 상태로 계속해서 Coroutine 이 쌓일 수 있다. Recomposition 은 자주 일어나는 동작이므로 Recomposition 마다 Coroutine 을 생성하는 것은 위험하며 심할 경우 앱 crash 를 발생시킬 수도 있다.</p>
<p>따라서 <code>Composable 에서 Coroutine 을 생성한다면 Recomposition 이 일어날 때 취소 되어야 한다.</code> (꼭 그렇지 않은 경우도 있지만 그래야 하는 경우가 대부분이다). Compose 는 이를 위해 Composable 의 Lifecycle 을 따르는 CoroutineScope을 반환하는 rememberCoroutineScope() 함수를 제공한다.</p>
</blockquote>
<p>이 내용을 보니 왜 이게 있는지는 알게 되었고, Compose 내에서는 무조건 rememberCoroutineScope() 를 사용해야겠다는 생각이 들었다.
위 설명을 언급한 <a href="https://kotlinworld.com/247">전문가의 글</a>을 꼭 읽어보기를 개인적으론 추천한다.</p>
</li>
<li><p>기존 bottomSheet 에서 Expend, Collapsed 등을 state 형태로 관리하는듯 하다.
recomposition 하더라도 상태가 유지되어야 하니 저게 맞는 듯 하다.</p>
</li>
<li><p>bottomSheet (BottomSheetScaffold) 에서 사용하는 state 이다.
apply { ... } 는 실험하면서 남은 내용이어서 추후 제거해야 할 내용이다.
bottomSheetState 가 Collapsed 일 때 isVisible 를 false 로 해주는 건데, 저 코드가 없어도 동작엔 무리가 없다.</p>
</li>
<li><p>조금 동작 방식이 희안한데, composable 함수 내에 ViewPager 변수가 있고, 이 값을 내부 singleTone 형태로 받을 수 있는 형태이다.
이로 인해 추후 언급할 학점 통계화면 Content View 에서 해당 함수를 인지하고 viewPager 를 가져올 수 있다.</p>
<p>하지만 개인적으로 마음에 들지 않는 코드이다. 
추후 ViewPager 를 바로 선언 형태로 하고 내부에서도 참조 할 수 있는 방법이 있을지, 그러면서 ViewPager 위치 자체를 옮길지도 생각하고 있다.</p>
</li>
<li><p>뒷장의 Crop 화면 라이브러리와 연관이 되는 내용이기도 하다. 하지만 내용은 쉽다.
크롭 화면에 이동하여 이에 상응하는 결과값 (CropImageView.CropResult) 을 받아오고
성공, 실패 여부에 따라 동작을 다르게 해주는 것이다.
성공했을 경우 띄워줄 이미지를 기반으로 bottomSheet 를 띄워주어 어디에 공유할 지 선택할 수 있다.
참고로 <code>CropImageContract</code> 는 ActivityResultContract 를 상속받아 custom 하게 만든 내용이며 이는 뒷 내용에서 후술할 것이다.</p>
</li>
<li><p>위의 Crop 과 비슷하다. 크롭화면에서의 결과값 대신 권한 결과값을 가지고 처리를 진행한다.
6번에서 대충 예상했겠지만, Compose 에서는 startActivityResult 대신 Contract 를 활용하여 처리한다. 실제 deprecated 되었기도 하고, 이걸 활용하는 게 맞는거긴 하다.
여기에서는 현재 화면을 bitmap 으로 저장하고 크롭 화면으로 이동하는 역할을 수행한다.</p>
</li>
<li><p>BottomSheet (BottomSheetScaffold) 이다. 큰 내용은 없고 세부 컬러들을 설정할 수 있다.
개인적으론 여기서 좀 삽질이 있었다. 2중으로 색이 씌워진다거나 등등의 이슈가 있었지만 지금은 해결했다.</p>
</li>
<li><p>bottomSheet 가 뜰 때 Animation 을 적용했다. 뒷 배경이 fadeIn 형태로 회색 배경이 되고, fadeout 형태로 원래 배경으로 돌아온다.</p>
</li>
<li><p>회색 배경에 하단에만 컨텐츠가 있기 때문에 Box 를 활용하여 구현했다. 
Dim 영역을 클릭할 경우 bottom sheet 가 닫힌다.</p>
</li>
<li><p>bottom sheet 를 닫는 건 coroutinescope.launch 를 통해 진행해야 한다.   </p>
</li>
</ol>
<h2 id="2-학점-통계화면-view">2. 학점 통계화면 View</h2>
<p>이제 실제 보여지는 View 이다.
이것도 전체 이야기는 아니고, 탭 레이아웃과, ViewPager 에 대한 내용만 있다.</p>
<pre><code class="language-kotlin">
const val cd_btn_statistic_share = &quot;btn_statistic_share&quot;

fun StatisticView(vm: StatisticViewModel? = null) {

    ...

    BottomSheetScaffold(
        ...
    ) {
        // 1
        Toolbar(
            navigationIcon = { vm?.let { BackButton(vm) } },
            titleRes = R.string.tv_statistic_toolbar_title,
            actions = {
                Image(
                    painterResource(R.drawable.btn_statistic_share),
                    contentDescription = cd_btn_statistic_share,
                    modifier = Modifier
                        .clickable(
                            enabled = true,
                            interactionSource = remember { MutableInteractionSource() },
                            indication = rememberRipple(bounded = true),
                            onClick = {
                                coroutineScope.launch {
                                    delay(300)

                                    // 2
                                    permissionLauncher.launch(
                                        arrayOf(
                                            Manifest.permission.READ_EXTERNAL_STORAGE,
                                            Manifest.permission.WRITE_EXTERNAL_STORAGE,
                                        )
                                    )
                                }
                            }
                        )
                        .padding(12.dp)
                )
            }
        )

        // 3
        AndroidView(factory = { context -&gt;
            // 4
            val tabLayout = TabLayout(context).apply {
                layoutParams = ViewGroup.MarginLayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT
                )
                setBackgroundColor(R.color.white.getColor(context))
                elevation = 3.dp.value
                tabMode = TabLayout.MODE_SCROLLABLE
                setTabTextColors(
                    R.color.statisticTabColor.getColor(context),
                    R.color.statisticTabSelectedColor.getColor(context)
                )
            }

            // 5
            tabLayout.apply {
                val tabTitles = resources.getStringArray(R.array.tb_statistic_tab_titles)
                TabLayoutMediator(this, getStatisticViewPager(context)) { tab, position -&gt;
                    tab.text = tabTitles[position]
                }.attach()
            }
        })

        // 6
        AndroidView(factory = { context -&gt; getStatisticViewPager(context) })
    }
}</code></pre>
<ol>
<li><p>최상위에 보여주는 ToolBar 이다.
navigationIcon (왼쪽 영역) 에는 백 버튼이 있고, actions (우측 영역) 에는 권한 체크 후 크롭 화면을 킬 수 있도록 하였다. 
권한 체크 후의 동작은 BottomSheetScaffold 설명의 권한 로직 내용 (7번) 을 확인하면 된다.</p>
</li>
<li><p>위에서 언급했던 권한 체크하는 요청 로직이다. 기존 방식과 별 다른 차이가 없다.</p>
</li>
<li><p>Composable 내에서 기존의 View 를 사용하는 방식 중 Kotlin(Java) 파일 차원에서 View 를 만드는 방법이다. 기존 방식을 사용하는 경우에는 <code>AndroidView</code> 라는 Composable 을 사용한다.</p>
</li>
<li><p>위에서 말했던 AndroidView 를 사용하여 TabLayout 를 선언하였다.
사실 말만 거창하지 별다른 차이가 없다. 인스턴스 선언에서부터 params 설정 및 기타 설정까지 모두 기존 방식과 같다.
굳이 차이를 두자면 AndroidView 와 factory 의 context 를 활용하는 게 차이가 되겠다.</p>
</li>
<li><p>TabLayoutMediator 를 사용하는 것 또한 이전과 차이가 없다.</p>
</li>
<li><p>ViewPager 또한 <code>AndroidView</code> 를 통해 가져오며, 이는 위에서 이야기했다시피 추후 개선해야 할 내용이다.</p>
</li>
</ol>
<h2 id="3-학점-통계화면-content-viewfragment-pageradapter">3. 학점 통계화면 Content View(Fragment), PagerAdapter</h2>
<p>위에서 언급했다시피 TabLayout 의 탭에 맞게 띄워주어야 하는 화면들은 Fragment 를 이용해 구현하였다.
그리고 ViewPager 를 통해 swipe 할 수 있도록 구현하였다.</p>
<h3 id="1-학점-통계화면-content-view-fragment">1. 학점 통계화면 Content View (Fragment)</h3>
<p>Fragment 에서는 Compose 를 사용하는 내용이 없어 따로 언급하지 않을 예정이다.
이 부분에 대해서는 코드 라인이 어느정도 줄었는지만 언급하려 한다.
<img src="https://velog.velcdn.com/images/ricky_0_k/post/5d72f0bd-e5ed-4837-ba84-2c7264c3addc/image.png" alt="">
그래.. 140 줄 정도 줄이고 코드를 깨끗하게 한 것에 만족한다.</p>
<h3 id="1-pageradapter">1. PagerAdapter</h3>
<p>PagerAdapter 에 대해서는 짤막하게 코드만 언급하고 넘어가려 한다.</p>
<pre><code class="language-kotlin">class StatisticPagerAdapter(activity: StatisticActivity) : FragmentStateAdapter(activity) {
    private val tabTitles = activity.resources.getStringArray(R.array.tb_statistic_tab_titles)

    override fun getItemCount(): Int = tabTitles.size

    override fun createFragment(position: Int): Fragment {
        val fragment = StatisticFragment()
        fragment.arguments = Bundle().apply { putInt(Const.BUNDLE_SEMESTER_NUM, position + 1) }
        return fragment
    }
}</code></pre>
<p>기존 구현과 큰 차이는 없다. 일반적으로 우리가 구현했던 adapter 로 생각하면 더 좋을 것이다.</p>
<h2 id="4-기타">4. 기타</h2>
<p>위에서 언급했던 CropImageContract 에 대해서만 언급하려 한다.
CropImageContract 는 크롭 화면으로 넘어가서 결과를 받아오는 명세를 한 내용이다.</p>
<pre><code class="language-kotlin">class CropImageContract :
    ActivityResultContract&lt;CropImageContractOptions, CropImageView.CropResult&gt;() {

    override fun createIntent(context: Context, input: CropImageContractOptions): Intent {
        input.cropImageOptions.validate()
        return Intent(context, CropActivity::class.java).apply {
            val bundle = Bundle().apply {
                putParcelable(CropImage.CROP_IMAGE_EXTRA_SOURCE, input.uri)
                putParcelable(CropImage.CROP_IMAGE_EXTRA_OPTIONS, input.cropImageOptions)
            }
            putExtra(Const.EXTRA_SHARE_URI, input.uri)
            putExtra(CropImage.CROP_IMAGE_EXTRA_BUNDLE, bundle)
        }
    }

    override fun parseResult(
        resultCode: Int,
        intent: Intent?
    ): CropImageView.CropResult {
        val result = intent?.getParcelableExtra&lt;Parcelable&gt;(CropImage.CROP_IMAGE_EXTRA_RESULT)
            as? CropImage.ActivityResult?

        return if (result == null || resultCode == Activity.RESULT_CANCELED)
            CropImage.CancelledResult
        else result
    }
}</code></pre>
<p>CropResult 는 데이터 뭉치이다. bitmap 등의 정보들이 있다.
<code>createIntent()</code> 를 통해 화면을 이동하고, <code>parseResult()</code> 를 통해 받아온 결과를 분석한다고 이해하면 편할 것이다.</p>
<h1 id="크롭-화면">크롭 화면</h1>
<p>사실 크롭화면도 Compose 를 쓰는 게 없다.
다만 리펙터링을 통해 코드량이 어느정도 줄었는지만 언급하려 한다.</p>
<p><img src="https://velog.velcdn.com/images/ricky_0_k/post/a394ea9b-3c10-4b5c-8fb4-92dac08d64c9/image.png" alt=""></p>
<p>기존에 라이브러리 코드를 다 넣어서 개조했던 걸 수정해서 그런지 최소 250 줄 가량이 줄었다.</p>
<h1 id="결론">결론</h1>
<p>이번 작업을 통해 Compose 내에서 기존의 View 를 사용하려면 <code>AndroidView</code> 를 사용해야 한다는 것을 알 수 있었다.
그리고 구현하면서 coroutineScope 를 어떻게 써야할지, startActivityResult 대신 어떻게 써야할지를 알 수 있었다.</p>
<p>다음으로 내가 작업한 내용은 <code>설정화면</code>이다.
여기서는 아키텍처 부분도 같이 언급하면서 포스팅을 할 예정이다.</p>
<h1 id="끄적임">끄적임</h1>
<p>포스팅을 쓰면서 드는 생각이 있다.</p>
<ol>
<li><p>프로젝트 public 하게 만들기
앞서 이야기했지만 Compose 이외의 영역은 언급하지 않고 넘어가다보니 일부 아쉬운 점이 있었다. 
개선한 코드에서도 내가 열심히 한 작업이고, 내가 놓친 것들이 있을 것 같다는 생각이 들어서 말이다...</p>
<p>개선하면서 이 프로젝트를 public 하게 열 수 있는 방법도 생각해보려 한다.
가능하면 사람들의 PR 도 받아보고 싶고, issue 도 받아보고 싶지만 가능할지는 모르겠다.
일단은 보일 수 있게 만들어 놓으려 한다.</p>
</li>
<li><p>리펙터링 내용 따로 다뤄보기
기존에는 설정화면이 마지막 포스팅이었다.
그런데 1번 생각을 하면서 예정에 없었던 리펙터링 내용을 다뤄볼까 생각중이다.
Room 마이그레이션, 로직 정리 등 제법 굵직한 내용들이 있었기 때문이다.<br>내가 했던 작업들도 머리에 다시 정리할 겸, <code>설정화면</code> 대신 <code>리펙터링</code>을 마지막 포스팅 내용으로 하고자 한다.</p>
</li>
</ol>
<p>참고</p>
<ul>
<li><a href="https://kotlinworld.com/247">https://kotlinworld.com/247</a></li>
<li><a href="https://developer.android.com/reference/kotlin/androidx/compose/material/package-summary#BackdropScaffold(kotlin.Function0,kotlin.Function0,kotlin.Function0,androidx.compose.ui.Modifier,androidx.compose.material.BackdropScaffoldState,kotlin.Boolean,androidx.compose.ui.unit.Dp,androidx.compose.ui.unit.Dp,kotlin.Boolean,kotlin.Boolean,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Shape,androidx.compose.ui.unit.Dp,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,kotlin.Function1)">https://developer.android.com/reference/kotlin/androidx/compose/material/package-summary#BackdropScaffold(kotlin.Function0,kotlin.Function0,kotlin.Function0,androidx.compose.ui.Modifier,androidx.compose.material.BackdropScaffoldState,kotlin.Boolean,androidx.compose.ui.unit.Dp,androidx.compose.ui.unit.Dp,kotlin.Boolean,kotlin.Boolean,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Shape,androidx.compose.ui.unit.Dp,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,kotlin.Function1)</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Compose 무작정 맛보기 [4. 학점 및 과목 입력 화면]
]]></title>
            <link>https://velog.io/@ricky_0_k/Compose-%EB%AC%B4%EC%9E%91%EC%A0%95-%EB%A7%9B%EB%B3%B4%EA%B8%B0-4.-%ED%95%99%EC%A0%90-%EB%B0%8F-%EA%B3%BC%EB%AA%A9-%EC%9E%85%EB%A0%A5-%ED%99%94%EB%A9%B4</link>
            <guid>https://velog.io/@ricky_0_k/Compose-%EB%AC%B4%EC%9E%91%EC%A0%95-%EB%A7%9B%EB%B3%B4%EA%B8%B0-4.-%ED%95%99%EC%A0%90-%EB%B0%8F-%EA%B3%BC%EB%AA%A9-%EC%9E%85%EB%A0%A5-%ED%99%94%EB%A9%B4</guid>
            <pubDate>Sun, 27 Mar 2022 17:06:09 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p>지난번에 이야기했다시피 state 를 깊게 다뤄보고, focusing 처리 등 UI 의 다양한 것을 접했던 시기였다. 
디버깅해보면서 어느 시점에서 state 를 처리해주는 게 좋을지 등을 고민했던 기억도 난다.
그런 만큼 작업량도 많았고 코드량도 어마무시해졌다.</p>
<p>이렇다 보니 무작정의 여파가 많이 남았던 코드이기도 하다. 
지금 돌아보면 수정이 필요한 내용들도 많다.
포스팅 완료 후 리펙터링을 할 때 제일 많이 건드려야 할 것 같은 화면이다.</p>
<p>포스팅 내용은 최종 작업 코드를 언급하면서, 번호 형태로 정리하려 한다.
코드 량이 많다보니 최대한 분리해서 볼 수 있도록 쪼개서 작성하려 노력해보려 한다.</p>
<p>2주간에 내가 어떤 작업들을 했었는지 이야기해보려 한다.</p>
<h1 id="화면에-대한-소개">화면에 대한 소개</h1>
<p>다량의 코드를 접할 예정이다보니(?), 화면에 대한 소개도 필요할듯하여 스크린샷을 먼저 첨부했다.</p>
<p><img src="https://images.velog.io/images/ricky_0_k/post/2ef21255-947c-4159-a0fe-7fba622296a5/image.png" alt=""></p>
<p>이런식으로 구현되어 있으며 간단한 기능 명세를 하면 이렇다.</p>
<ol>
<li><p>우측 상단에 창에 눈이 있는 것 같은 버튼은 <code>보기모드</code>이다.
스와이프 도중 클릭 이벤트가 동작하는 케이스가 예전에 있었어서 개선 건으로 추가했다.</p>
</li>
<li><p>x학년 y학기가 수평 정렬 되어있는 View 는 <code>Scroll 가능한 탭</code>이다.
특정 학기를 선택 시, 해당 학기에 맞는 정보들이 갱신된다.</p>
</li>
<li><p>그 아래에는 학기 ItemView 이다. <code>각 학기에 맞는 정보와 과목들</code>이 나온다.
해당 과목들은 모두 <code>Room</code> 에 저장되어 있으며 각 과정시에 <code>create</code>, <code>update</code>, <code>delete</code> 를 실행한다.</p>
</li>
<li><p>각 학기로 이동할 때마다, 초기에 x학년 y학기, 총 학점, 취득 학점 등이 갱신된다.
총 학점, 취득 학점의 경우 아이템에 <code>값을 입력하고 다른 곳을 터치할 때에도 즉시 갱신</code>된다. (update)</p>
</li>
<li><p>각 과목 아이템 레이아웃에 <code>swipe 기능</code>이 있으며, swipe 시 <code>action 버튼</code>이 보여진다. 
(현재는 <code>삭제</code>(delete)만 가능하다)</p>
</li>
<li><p><code>+ 버튼</code>을 통해 학기 내 과목을 추가할 수 있다. (create)</p>
</li>
</ol>
<h1 id="초기-작업-및-고민">초기 작업 및 고민</h1>
<p>여기서도 MVP -&gt; MVVM 작업은 어김없이 있었고, sharedPreference key 값 상수화 등 일부 코드 개선 작업도 병행했다.
이후 맞닥들인 내용은 기존의 방식과 다른 <code>ViewPager 구현 방식</code>이었다.</p>
<p>기존에는 </p>
<blockquote>
<p><code>TabLayout</code> 과 <code>ViewPager 또는 ViewPager2</code> 를 두고, 
Pager 내에 들어가는 View 는 <code>Fragment</code> 를 사용했었다.</p>
</blockquote>
<p>Compose 에서는 </p>
<blockquote>
<p><code>ScrollableTabRow</code> 과 <code>HorizontalPager</code> 를 두고,
Pager 내에 들어가는 View 는 <code>Composable UI</code> 로 만들어주면 되었다.</p>
</blockquote>
<p>일단 Compose 에서 언급된 HorizontalPager 을 사용하기 위해 아래 라이브러리를 import 했다.</p>
<pre><code class="language-groovy">implementation &quot;com.google.accompanist:accompanist-pager:0.19.0&quot;
implementation &quot;com.google.accompanist:accompanist-pager-indicators:0.19.0&quot;</code></pre>
<h1 id="학점-입력-화면-inputview">학점 입력 화면 (InputView)</h1>
<p>어떻게 포스트를 작성할지 고민하다가 거대한 코드를 설명 부분에 맞춰 ... 으로 나누고
차례차례 이야기를 나아가보려 한다.
기타 글자색 설정이나, 크기를 설정했던 내용 등은 코드가 좀 길기에 일부 생략했다.</p>
<h2 id="1-변수부-선언">1. 변수부 선언</h2>
<p>먼저 변수부 선언이다.</p>
<p>해당 파일에서만 사용할 수 있는 전역변수도 일부 활용했는데, 
이는 내가 고민을 덜하고 작성한 것 같아 수정하려 한다.</p>
<pre><code class="language-kotlin">// 1-1
/** 선택 다이얼로그에서 활용되는 정보 모음 */
private lateinit var inputSelectDialogInfo: InputDialogInfo.InputSelectDialogInfo

// 1-2
/** 삭제 다이얼로그에서 활용되는 정보 모음 */
private lateinit var inputDeleteDialogInfo: InputDialogInfo.InputDeleteDialogInfo

// 1-3
/** 유저가 마지막으로 입력한 값을 기억하기 위한 data class 기반 인스턴스 */
private var inputDisposedState = InputDisposedState()

@OptIn(ExperimentalPagerApi::class)
@Composable
fun InputView(vm: InputViewModel? = null) {
    inputDisposedState = InputDisposedState()

    val tabTitles = stringArrayResource(id = R.array.tb_input_tab_titles)

    // 2
    // ViewPager 의 현재 Page 정보. 1을 뺀 원본 index 값이 등록되어 있으므로 유의하여 작업할 것
    val pagerState = vm?.pagerState ?: rememberPagerState()
    // ViewPager 의 현재 Page index. 1을 뺀 원본 index 값이 등록되어 있으므로 유의하여 작업할 것
    val tabOriginalIndex = pagerState.currentPage

    // 애니메이션을 위한 코루틴 스코프
    val coroutineScope = rememberCoroutineScope()

    // 3
    val semesterIndex = vm?.semesterIndex?.observeAsState(1)
        ?: remember { mutableStateOf(0) }
    val columnIndex = remember { mutableStateOf(0) }

    // 4
    val revealedCardId = vm?.revealedCardId?.collectAsState()
        ?: remember { mutableStateOf(0) }
    val editSubject: (Int, Int, Int, String) -&gt; Job = vm
        ?.let { it::editSubject } ?: { _: Int, _: Int, _: Int, _: String -&gt; Job() }
    val getDialogItems: (InputDialogType) -&gt; Array&lt;String&gt; = vm
        ?.let { it::getDialogItems } ?: { arrayOf() }

    // 5
    val focusManager = LocalFocusManager.current
    val viewModeEnabled = remember { mutableStateOf(false) }

    inputSelectDialogInfo = InputDialogInfo.InputSelectDialogInfo(
        openDialog = remember { mutableStateOf(false) },
        inputDialogType = remember { mutableStateOf(InputDialogType.GRADE) },
        selectItems = remember { mutableStateOf(listOf()) },
        semesterIndex = semesterIndex,
        editableRowIndex = remember { mutableStateOf(0) },
        columnIndex = columnIndex,
    )
    inputDeleteDialogInfo = InputDialogInfo.InputDeleteDialogInfo(
        openDialog = remember { mutableStateOf(false) },
        semesterIndex = semesterIndex,
        columnIndex = columnIndex,
    )
    ...
}</code></pre>
<ol>
<li><p>사실 추후 개선건이라고 보아도 될 듯 하다.</p>
<p>1-1, 1-2
dialog 가 열렸는지에 대한 값, dialog 의 타입, 선택한 행열 index 등의 값을 state 형태로 관리했었고, 
이런 state 들을 모아둔 data class 인스턴스이다.
dialog 정보를 학점 및 과목 입력 화면에서 알고 있어야 하기에 이렇게 작성했었는데,
차라리 ViewModel 에 놓거나, 해당 값을 계속 하위로 넘겨주는 게 나을 것 같다는 생각이 들기도 했다.
이 관리 방법은 어떻게 할지 고민해보려 한다.</p>
<p>1-3
이건 직접 state 를 넣는게 아닌 primitive 유사값 (String, int 등) 들을 관리하는 인스턴스이다.
이것도 1-1, 1-2 의 해결책이 그려지면 같이 작업하지 않을까 싶다.
그래도 이건 state 타입 변수들이 아니다 보니, 1-1, 1-2 보단 나은 것 같다.</p>
</li>
<li><p>HorizontalPager 에서 스크롤을 제어하고 관찰하기 위한 State object 이다.
현재 보여지는 Index 를 가져오거나 새로 설정하기도 하고, 애니메이션 효과를 주거나, TabIndicator 과 연결도 될 수 있다.
ViewPager 로 비유하면 음... adapter..? (정확히 같은 기능은 아니다.)</p>
<blockquote>
<p>ViewModel 에 hoisting 되어 있네요..?</p>
<p>난 해당 값을 원래 내부에서 썼었다가 ViewModel 로 hoisting 해서 사용했다.
ViewModel 차원에서 현재 page index 를 확인해야 해서 그렇게 처리했다.</p>
<p>이로 인해 우려되는 점이 있다면, View 의 로직을 ViewModel 에서 제어할 수 있다는 단점이 있긴하다.
실제 animateScrollToPage 를 통해 페이지를 이동시켜줄 수 있는데 이건 View 의 로직이라 고민이 있었다.</p>
<p>하지만 현재 view 의 Index 를 정확히 알기 위해서 이런 식으로 hoisting 하여 사용했다.</p>
</blockquote>
<p>주석 내용을 보충설명하자면,
2018년도의 나는 학기 index 를 0부터 하지않고 1부터 시작했었다.
그러다보니 저런 주석이 추가되었고, 실제 이를 컨트롤하기위한 코드가 내부에 들어가 있다.
마이그레이션하면서 이것도 같이 건드릴까했었지만, 1부터 시작하는 게 잘못된 건 또 아니고
괜히 긁어 부스럼을 만드는 것 같아 그대로 두었다.</p>
</li>
<li><p>현재 학기 index 와, 선택된 열 index 는 여기에서 state 를 관리했다.
나중에 후술하겠지만 최소한의 state 변수들을 최상위 Composable 에 두려고 했었고, 
해당 state 는 학기 ItemView 를 그리거나, 선택한 열에 맞는 팝업을 띄우기 위해 필요했기에 여기에 작성했다.   </p>
</li>
<li><p>action 이 보여지는 cardId state 도 여기에서 관리했다.
전체 학기 차원에서, 오직 하나의 열만 action 이 보여야하기에 여기에서 관리했다.
만약 각 학기마다 action 이 보여져도 상관 없다면 하위에 넣었을 것 같다.</p>
<p>그 밑에는 과목 데이터 변경 기능, dialog 에 띄워줄 항목을 가져오는 기능을 변수화했다.
이들은 모두 ViewModel 에 정의되어 있는 함수이다.</p>
</li>
<li><p>Compose 에서는 FocusManager 를 통해 focus 를 관리한다.
<code>LocalFocusManager.current</code> 를 통해 focus 되어있는 view 를 가져올 수 있고
<code>LocalFocusManager.current.moveFocus(..)</code> 를 통해 특정 방향으로 focus 를 이동할 수도 있다.</p>
<p>focus 는 modifier 에도 설정이 가능하다.
포커스를 요청할 수 있게 만들거나(<code>Modifier.focusRequester()</code>), 
포커스가 되거나 잃을 시 어떻게 처리할지(<code>Modifier.onFocusChanged()</code>)를 명세할 수 있다.</p>
<p>다른 버튼을 클릭할 때, 변경된 내역을 과목에 반영하면서 총 학점과 취득 학점을 계산 후 갱신해야 하고
focus 를 가질 수 없는 view 에 focus 를 부여해야 했기 때문에, 난 위에 언급된 일부 기능들을 사용했다.</p>
</li>
</ol>
<h2 id="2-custom-toolbar">2. Custom Toolbar</h2>
<p>기존에는 위에 제목만 가운데에 있는 Toolbar 를 구현하면 되었지만
이제는 왼쪽, 오른쪽에 버튼이 있는 Toolbar 를 구현할 필요가 있었다.</p>
<pre><code class="language-kotlin">@OptIn(ExperimentalPagerApi::class)
@Composable
fun InputView(vm: InputViewModel? = null) {
    ...
    BoxWithConstraints() {
        val constraints = ConstraintSet {
            val btnInputAdd = createRefFor(btn_input_add)
            constrain(btnInputAdd) {
                bottom.linkTo(parent.bottom)
                end.linkTo(parent.end)
            }
        }

        ConstraintLayout(constraints) {
            Column(modifier = Modifier.background(color = Color.White)) {
                // 1
                Toolbar(
                    navigationIcon = { vm?.let { BackButton(vm) } },
                    titleRes = R.string.tv_input_toolbar_title,
                    actions = {
                        Image(
                            painterResource(...),
                            contentDescription = btn_viewer_toggle,
                            modifier = Modifier
                                .clickable(
                                    enabled = true,
                                    interactionSource = remember { MutableInteractionSource() },
                                    indication = rememberRipple(bounded = true),
                                    onClick = {
                                        focusManager.clearFocus()
                                        vm?.onItemClear()
                                        viewModeEnabled.value = !viewModeEnabled.value
                                    }
                                )
                                .padding(16.dp),
                        )
                    }
                )
                ...</code></pre>
<p>구글에서 검색을 하다가 좋은 코드와 설명 링크를 발견했고 이를 일부 개조하여 사용하였다. 
(설명링크는 유실되어서 참고에 언급하지 못했다..)</p>
<p><code>navigationIcon</code> 을 통해 좌측의 버튼을 명세할 수 있고, <code>actions</code> 를 통해 우측의 버튼을 명세할 
수 있다.</p>
<p>actions 에 명세한 버튼은 상단에 언급했던 보기모드 버튼으로
클릭 시 focus 를 clear 하여 변경 내역을 갱신하고, 삭제 버튼이 보이는 열도 모두 닫고
viewModeEnabled 값을 바꿔, 앞에 투명한 창이 보이도록 하여 터치를 할 수 없도록 막았다.</p>
<h2 id="3-scroll-가능한-수평정렬-탭">3. Scroll 가능한 수평정렬 탭</h2>
<p>ScrollableTabRow 을 활용하여 구현하였다.</p>
<pre><code class="language-kotlin">@OptIn(ExperimentalPagerApi::class)
@Composable
fun InputView(vm: InputViewModel? = null) {
                ...
                // 1
                ScrollableTabRow(
                    selectedTabIndex = tabOriginalIndex,
                    backgroundColor = Color.White,
                    edgePadding = 0.dp,
                    modifier = Modifier.height(input_item_action_icon_size),
                    indicator = { tabPositions -&gt;
                        TabRowDefaults.Indicator(
                            color = colorResource(id = R.color.colorTheme),
                            height = 2.dp,
                            modifier = Modifier.pagerTabIndicatorOffset(pagerState, tabPositions),
                        )
                    }
                ) {
                    // 2
                    tabTitles.forEachIndexed { titleIndex, title -&gt;
                        Tab(
                            selected = tabOriginalIndex == titleIndex,
                            onClick = {
                                focusManager.clearFocus()
                                coroutineScope.launch { pagerState.animateScrollToPage(titleIndex) }
                            },
                            text = {
                                Text(
                                    text = title,
                                    fontFamily = nanumSquareFamily,
                                    letterSpacing = 0.16.sp
                                )
                            },
                            selectedContentColor = colorResource(id = R.color.black),
                            unselectedContentColor = colorResource(id = R.color.statisticTabColor),
                            icon = null
                        )
                    }
                }

                Divider(...)

                ...</code></pre>
<ol>
<li><p>선택된 tabIndex, 배경색 등 xml 에서 설정할 수 있는 내용들은 거의 다 살정할 수 있다.
Pager 와의 연동은 indecator 설정을 통해 가능하다. (<code>pagerTabIndicatorOffset()</code>)</p>
</li>
<li><p>학기 개수에 맞는 Tab Composable 를 만들 수 있도록 했다.
클릭 이벤트라거나, 색상 설정 등 역시 가능하다.</p>
</li>
</ol>
<h2 id="4-학기-itemview">4. 학기 ItemView</h2>
<p>기존의 Fragment 에서 보여주었던 내용들을 여기에서 구현하였다.</p>
<pre><code class="language-kotlin">@OptIn(ExperimentalPagerApi::class)
@Composable
fun InputView(vm: InputViewModel? = null) {
                ...
                // 1
                HorizontalPager(
                    state = pagerState,
                    modifier = Modifier.weight(1f),
                    count = Const.GRADE_END
                ) { tabOriginalIndex -&gt;
                    if (pagerState.currentPage != tabOriginalIndex) return@HorizontalPager

                    // 2
                    val semesterNum = tabOriginalIndex + 1

                    // 3
                    val subjects = vm?.allSubjects?.get(semesterNum)?.observeAsState()
                        ?: remember { mutableStateOf(listOf()) }
                    val totalScore = vm?.allTotalScore?.get(semesterNum)?.observeAsState()
                        ?: remember { mutableStateOf(0f) }
                    val totalGrade = vm?.allTotalGrade?.get(semesterNum)?.observeAsState()
                        ?: remember { mutableStateOf(0) }

                    Column(
                        modifier = Modifier.fillMaxSize(),
                        horizontalAlignment = Alignment.CenterHorizontally
                    ) {
                        Spacer(여백)
                        Image(책 이미지)

                        Spacer(여백)
                        Text(x학년 y학기)

                        Spacer(여백)
                        Row(중앙 정렬) {
                            Text(총 학점)
                            Spacer(여백)
                            Text(
                                text = String.format(&quot;%.2f&quot;, totalScore.value),
                                fontSize = 12.sp,
                                color = colorResource(id = R.color.themeTextColor)
                            )
                            Spacer(여백)
                            Text(취득 학점)
                            Spacer(여백)
                            Text(
                                text = &quot;${totalGrade.value}&quot;,
                                fontSize = 12.sp,
                                color = colorResource(id = R.color.themeTextColor)
                            )
                        }

                        Spacer(modifier = Modifier.height(20.dp))
                        InputItemTitleView()
                        // 4
                        LazyColumn(modifier = Modifier.fillMaxHeight()) {
                            // 5
                            items(count = subjects.value?.size ?: 0) { subjectIndex -&gt;
                                val subject = subjects.value?.get(subjectIndex) ?: Subject()
                                val getItemIdAction: () -&gt; Int? = {
                                    vm?.getItemSId(semesterNum, subjectIndex)
                                }
                                Box(Modifier.fillMaxWidth()) {
                                    // 6
                                    InputItemActionsRow(
                                        modifier = Modifier
                                            .fillMaxWidth()
                                            .padding(
                                                start = input_item_view_margin_horizontal,
                                                end = input_item_view_margin_horizontal
                                            ),
                                        subjectIndex = subjectIndex,
                                        subject = subject,
                                        onDelete = vm?.let { inputDeleteDialogInfo::update },
                                        inputStateMap = vm?.inputFieldStateMap,
                                    )

                                    // 7
                                    InputItemView(
                                        editSubject = editSubject,
                                        getDialogItems = getDialogItems,
                                        subject = subject,
                                        semesterNum = semesterNum,
                                        subjectIndex = subjectIndex,
                                        viewModeEnabled = viewModeEnabled,
                                        isRevealed = !viewModeEnabled.value &amp;&amp; revealedCardId.value == subject.sId,
                                        cardOffset = input_item_action_icon_size.value.dpToPx(),
                                        getItemIdAction = getItemIdAction,
                                        onExpand = vm?.let { it::onItemExpanded },
                                        onCollapse = vm?.let { it::onItemCollapsed },
                                        inputStateMap = vm?.inputFieldStateMap,
                                    )
                                }
                            }
                        }
                    }
                    // 8
                    if (viewModeEnabled.value)
                        Divider(
                            modifier = Modifier
                                .fillMaxSize()
                                .clickable(
                                    interactionSource = remember { MutableInteractionSource() },
                                    indication = null,
                                ) {},
                            color = colorResource(id = R.color.transparent)
                        )
                }
            }
            ...</code></pre>
<ol>
<li><p>수평형 ViewPager 이다.
pagerState 를 설정하고, pager 개수를 설정할 수 있다.</p>
</li>
<li><p>해당 page index 가 아닌 경우엔 그려지지 않도록 했다.
좌우가 모두 보여야 할 필요는 없을 것 같아 이렇게 구현했었다.
이건 옵셔널하게 다르게 구현할 필요가 있다면 손을 보려한다.</p>
</li>
<li><p>학기 과목 목록, 총 학점, 취득 학점은 여기에서 상태 관리를 하였다.
상위에서 관리하면 그 상위에서부터 recomposition 을 했어서, 최대한 하위에서 동작하도록 했다.</p>
</li>
<li><p>LazyColumn 은 화면에 보여지는 Composable 만을 보여주면서 scroll 이 가능한 Column 이다.
일전에 state 에서 한번 언급이 되었던 Composable UI 이기도 하다.</p>
</li>
<li><p>LazyColumn 특성 상 list 를 구현 시 많이 사용한다고 한다.
추후 여기에 key 를 적용하여 새로 인스턴스가 생기는 걸 줄이려고 한다.</p>
</li>
<li><p>해당 과목의 action 버튼 (삭제 버튼) View 이다.
처음에는 구현하면서 시행착오로 여기에 있지만, 추후 개선건으로 과목 ItemView 내에 넣으려고 한다.
참고로 삭제 버튼을 누르면 DialogInfo 내부 state 값을 갱신하여 삭제 확인 dialog 가 띄워진다.
(Dialog Info 값에 대해서는 Dialog 를 이야기하면서 언급할 예정이다.)</p>
</li>
<li><p>성적, 이수구분, 학점, 과목명을 입력할 수 있는 View 이다.
겹치는 기능도 있고, 각 View 마다 focus 를 체크해야 하여 row 형태로 구현했다.
(자세한 내용은 과목 ItemView 를 이야기하면서 다룰 예정이다.)</p>
</li>
<li><p>viewMode 가 활성화될 시 보여지는 투명한 창이다.
<code>viewModeEnabled</code> state 값에 의해 보여지거나 가려진다.
클릭 이벤트가 명세되어 있는 건... 
예전에 클릭해도 무슨 동작하게 해야겠다고 했었는데 무산되면서 남은 코드여서 제거 예정이다.</p>
</li>
</ol>
<h2 id="5--버튼-및-기타-ui">5. + 버튼 및 기타 UI</h2>
<p><code>+ 버튼</code>과 경우에 따라 보여지고 가려지는 Dialog 를 여기에 명세하였다.</p>
<pre><code class="language-kotlin">@OptIn(ExperimentalPagerApi::class)
@Composable
fun InputView(vm: InputViewModel? = null) {
            ...
            // 1
            if (!viewModeEnabled.value)
                Image(
                    painter = painterResource(id = R.drawable.ip_add_btn),
                    contentDescription = btn_input_add,
                    modifier = Modifier
                        .layoutId(btn_input_add)
                        .padding(end = 20.dp, bottom = 20.dp)
                        .clickable(
                            enabled = true,
                            interactionSource = remember { MutableInteractionSource() },
                            indication = rememberRipple(bounded = true),
                            onClick = {
                                focusManager.clearFocus()
                                vm?.addSubject(tabOriginalIndex + 1)
                            }
                        )
                )

            // 2
            InputSelectDialogView(vm, inputSelectDialogInfo)
            InputDeleteDialogView(vm, inputDeleteDialogInfo)
        }
    }

    // 3
    DisposableEffect(Unit) {
        Timber.i(&quot;DisposableEffect: entered input&quot;)

        onDispose {
            Timber.i(&quot;DisposableEffect: exited input&quot;)

            if (inputDisposedState.isValid)
                vm?.editSubject(
                    inputDisposedState.semesterIndex!!,
                    inputDisposedState.editableRowIndex!!,
                    inputDisposedState.columnIndex!!,
                    inputDisposedState.value!!,
                )
            inputDisposedState = InputDisposedState()
        }
    }
}</code></pre>
<ol>
<li><p>viewMode 가 비활성화 상태일 시 보여지는 <code>+ 버튼</code>이다.</p>
</li>
<li><p>선택 dialog, 삭제 dialog 이다.
 (자세한 내용은 과목 Dialog 를 이야기하면서 다룰 예정이다.)</p>
</li>
<li><p>Compose 가 dispose 되었을 때 동작해야 하는 내용을 명세한 Composable 이다.
해당 화면 종료 시 일전의 과목 변경 내역들을 갱신해야 하기 때문에 DisposableEffect 를 추가했다.</p>
</li>
</ol>
<h1 id="과목-itemview-inputitemview">과목 ItemView (InputItemView)</h1>
<p><img src="https://images.velog.io/images/ricky_0_k/post/81002f70-d937-43ff-89d3-3ff23a52a2cd/image.png" alt="">
그림 상으로 사각형 영역에 대한 View 이다. 
좌측으로 스와이프 했을 때는 우측과 같이 삭제 버튼이 보인다.</p>
<p>추후 개선건으로 위의 InputView 와 분리할 생각을 가지고 있다.
한 파일에 코드가 많아졌기에 가독성이 떨어져서 그렇게 생각했었는데, 필수는 아니어서 고민하고 있다.</p>
<h2 id="1-전체">1. 전체</h2>
<p>위에서 보았다시피 과목 ItemView 에는 실제 Content 를 보여주는 View, 삭제 버튼을 보여주는 View 가 있다.
추후 개선건으로 삭제 버튼을 보여주는 View 또한 해당 영역 내로 옮길 예정이다.</p>
<pre><code class="language-kotlin">/**
 * 학점 계산하기 화면 아이템 내 각각의 열 View
 * 0,1 index 는 비활성화되어 있는 [BasicTextField]
 * 2,3 index 는 활성화되어 있는 [BasicTextField]
 *
 * @param editSubject 해당 학년 학기에 맞는 과목을 수정하는 함수 변수
 * @param getDialogItems 선택 Dialog 에서 띄워주어야 하는 아이템 목록을 가져오는 함수 변수
 * @param subject 아이템에 들어가는 과목 데이터
 * @param semesterNum &quot;((semesterIndex + 1) / 2 학년 (semesterIndex % 2) 학기&quot; 공식에서 semesterNum
 * @param subjectIndex 해당 학기 내 과목[Subject] index
 * @param viewModeEnabled 보기 모드가 활성화 되어 있는지에 대한 여부 state 값
 * @param isRevealed 해당 과목에 actionsRow 가 노출되어있는지에 대한 여부
 * @param cardOffset actionsRow 의 총 너비
 * @param onExpand 펼쳐졌을 때의 action
 * @param onCollapse 닫혔을 때의 action
 */
@SuppressLint(&quot;UnusedTransitionTargetStateParameter&quot;)
@Composable
fun InputItemView(
    editSubject: (Int, Int, Int, String) -&gt; Job,
    getDialogItems: (InputDialogType) -&gt; Array&lt;String&gt;,
    subject: Subject,
    semesterNum: Int,
    subjectIndex: Int,
    viewModeEnabled: MutableState&lt;Boolean&gt;,
    isRevealed: Boolean,
    cardOffset: Float,
    getItemIdAction: () -&gt; Int?,
    onExpand: ((sId: Int) -&gt; Unit)?,
    onCollapse: ((sId: Int) -&gt; Unit)?,
    inputStateMap: MutableMap&lt;Pair&lt;Int?, Int&gt;, MutableState&lt;String&gt;&gt;?
) {
    // 1
    val offsetX = remember { mutableStateOf(0f) }
    val transitionState = remember {
        MutableTransitionState(isRevealed).apply { targetState = !isRevealed }
    }
    val transition = updateTransition(transitionState, label = &quot;&quot;)
    val offsetTransition by transition.animateFloat(
        label = &quot;cardOffsetTransition&quot;,
        transitionSpec = { tween(durationMillis = 250) },
        targetValueByState = { if (isRevealed) -cardOffset else 0f },
    )

    Card(
        modifier = Modifier
            .padding(
                start = input_item_view_margin_horizontal,
                end = input_item_view_margin_horizontal
            )
            // 2
            .offset { IntOffset((offsetX.value + offsetTransition).roundToInt(), 0) }
            // 3
            .pointerInput(Unit) {
                detectHorizontalDragGestures { change, dragAmount -&gt;
                    if (viewModeEnabled.value) return@detectHorizontalDragGestures
                    when {
                        dragAmount &lt;= -6 -&gt; onExpand?.let { getItemIdAction()?.apply { it(this) } }
                        dragAmount &gt; 6 -&gt; onCollapse?.let { getItemIdAction()?.apply { it(this) } }
                    }
                }
            },
        elevation = 0.dp,
        shape = RoundedCornerShape(0.dp),
        content = {
            InputItemContentView(
                editSubject,
                getDialogItems,
                subject,
                semesterNum,
                subjectIndex,
                inputStateMap
            )
        },
    )
}</code></pre>
<ol>
<li><p>swipe 액션에 대한 내용을 명세한 것이다.
offsetX 는 항목 레이아웃의 시작점이므로 사실상 무조건 0이다.</p>
<p>transitionState 는 MutableTransitionState 타입 변수이다. 
내부에는 초기 state (initialState), 현재 state(currentState), target state (targetState) 이렇게 가지고 있다.
당시에는 상태를 2개를 둔다고 이렇게 targetState 와 initialState 를 다르게 두었는데 
지금 보니 굳이 다르게 둘 필요가 없어 추후 개선건으로 apply {..} 코드를 제거할 예정이다.</p>
<p>이후 updateTransition 을 통해 현재 currentState 를 갱신한 transition 값을 가져온다.
그리고 transition 을 기반으로 Float 기반 애니메이션을 만든다. 
transitionSpec 을 통해 몇 초동안 애니메이션을 실행할 것인지, targetValueByState 로 어떤 값을 반환할 것인지를 정할 수 있다.</p>
</li>
<li><p>1번 과정을 통해 offsetTransition 의 결과값으로 targetValueByState 값을 가져오고 
삭제 버튼이 보이느냐 안 보이느냐에 따라, 왼쪽으로 과목 ItemView 를 옮겨주게 된다.</p>
</li>
<li><p>pointerInput 을 통해 tap, pressed, longPressed 등에 대한 이벤트 처리를 할 수 있다.
예전 activity 에서 <code>dispatchTouchEvent(ev: MotionEvent?)</code> 와 유사해보였다.
기존과 달리 Jetpack Compose 는 각 view 차원에서 이런 처리가 가능하다,</p>
<p>어쨌거나 나는 drag 을 어디로 했느냐에 따라, 삭제 버튼이 보이는 과목 id 를 갱신해야 했으므로 해당 처리를 해주었다.
-6, 6을 둔건 미세한 드래그는 무시하기 위해 임계값을 두었다.
추후 개선건으로 임계값을 상수화 시키는 게 좋을 것 같다.</p>
</li>
</ol>
<h2 id="2-실제-content-를-보여주는-view">2. 실제 Content 를 보여주는 View</h2>
<p>과목열 Content 를 보여주는 View 이다. 특정 항목을 선택하거나 직접 타이핑으로 값을 입력할 수도 있다.</p>
<pre><code class="language-kotlin">/**
 * 학점 계산하기 화면 아이템 View 에서 데이터를 보여주는 레이아웃
 *
 * @param editSubject 해당 학년 학기에 맞는 과목을 수정하는 함수 변수
 * @param getDialogItems 선택 Dialog 에서 띄워주어야 하는 아이템 목록을 가져오는 함수 변수
 * @param subject 아이템에 들어가는 과목 데이터
 * @param semesterIndex &quot;((semesterIndex + 1) / 2 학년 (semesterIndex % 2) 학기&quot; 공식에서 semesterIndex
 * @param columnIndex 아이템 행 index
 * @param inputStateMap 아이템 행 index
 */
@Composable
fun InputItemContentView(
    editSubject: (Int, Int, Int, String) -&gt; Job,
    getDialogItems: (InputDialogType) -&gt; Array&lt;String&gt;,
    subject: Subject?,
    semesterIndex: Int,
    columnIndex: Int,
    inputStateMap: MutableMap&lt;Pair&lt;Int?, Int&gt;, MutableState&lt;String&gt;&gt;?,
) {
    Column {
        // 1
        Row(modifier = Modifier.fillMaxWidth()) {
            InputItemEditableView(
                editSubject,
                getDialogItems,
                0,
                subject,
                Modifier.weight(10f),
                stringResource(id = R.string.tv_input_item_grade_hint),
                semesterIndex,
                columnIndex,
                inputStateMap,
            )
            InputItemEditableView(
                editSubject,
                getDialogItems,
                1,
                subject,
                Modifier.weight(20f),
                stringResource(id = R.string.tv_input_item_category_hint),
                semesterIndex,
                columnIndex,
                inputStateMap,
            )
            InputItemEditableView(
                editSubject,
                getDialogItems,
                2,
                subject,
                Modifier.weight(10f),
                stringResource(id = R.string.tv_input_item_score_hint),
                semesterIndex,
                columnIndex,
                inputStateMap,
            )
            InputItemEditableView(
                editSubject,
                getDialogItems,
                3,
                subject,
                Modifier.weight(40f),
                stringResource(id = R.string.tv_input_item_name_hint),
                semesterIndex,
                columnIndex,
                inputStateMap,
            )
        }
        Divider(
            modifier = Modifier
                .fillMaxWidth()
                .height(0.5.dp)
                .background(color = colorResource(id = R.color.grayDefaultColor))
        )
    }
}</code></pre>
<ol>
<li><p>성적, 이수구분, 학점, 과목명의 차이는 아래와 같다</p>
<ul>
<li>성적 : 선택 시 팝업을 띄워주어 성적 (ex. A+, B 등) 을 입력할 수 있어야함</li>
<li>이수구분 : 선택 시 팝업을 띄워주어 성적 (ex. 전공, 교양 등) 을 입력할 수 있어야함</li>
<li>학점 : 숫자를 입력할 수 있음</li>
<li>과목명 : 일반 문자열을 입력할 수 있음</li>
</ul>
<p>각 4개가 미묘하게 다르면서도 역할은 비슷하게 묶을 수 있었어서,
이 4개를 통합하는 View (<code>InputItemEditableView</code>) 를 만들어 처리하기로 결정했다.</p>
<p>추후 개선한다면 성적, 이수구분 / 학점, 과목명 이렇게 분리할 수 있을 것 같다.
그런데 저 4개에서 생기는 공통점도 있기 때문에 공통 코드들도 많이 생기기 때문에 
분리하는 게 정답인지는 아직 모르겠다.</p>
</li>
</ol>
<h3 id="1-과목-itemview-의-각-열-view">1. 과목 ItemView 의 각 열 View</h3>
<p>위 1번에서 이야기했던 과목 ItemView 의 각 열 View 이다.
그림상에서 네모 영역 각각을 이야기한다.</p>
<p><img src="https://images.velog.io/images/ricky_0_k/post/7a29a05a-1a49-4414-92db-bdac1d07a35c/image.png" alt=""></p>
<pre><code class="language-kotlin">/**
 * 학점 계산하기 화면 아이템 내 각각의 열 View
 * 0,1 index 는 비활성화되어 있는 [BasicTextField]
 * 2,3 index 는 활성화되어 있는 [BasicTextField]
 *
 * @param editSubject 해당 학년 학기에 맞는 과목을 수정하는 함수 변수
 * @param getDialogItems 선택 Dialog 에서 띄워주어야 하는 아이템 목록을 가져오는 함수 변수
 * @param editableRowIndex 아이템내 열 index
 * @param subject 아이템에 들어가는 과목 데이터
 * @param modifier [Modifier]
 * @param hintText 힌트 문자열
 * @param semesterIndex &quot;((semesterIndex + 1) / 2 학년 (semesterIndex % 2) 학기&quot; 공식에서 semesterIndex
 * @param columnIndex 아이템 행 index
 * @param inputStateMap 아이템 행 index
 */
@Composable
fun InputItemEditableView(
    editSubject: (Int, Int, Int, String) -&gt; Job,
    getDialogItems: (InputDialogType) -&gt; Array&lt;String&gt;,
    editableRowIndex: Int,
    subject: Subject?,
    modifier: Modifier,
    hintText: String,
    semesterIndex: Int,
    columnIndex: Int,
    inputStateMap: MutableMap&lt;Pair&lt;Int?, Int&gt;, MutableState&lt;String&gt;&gt;?,
) {
    // 1
    val disabled = editableRowIndex == 0 || editableRowIndex == 1

    // 2
    val disabledValue = when (editableRowIndex) {
        0 -&gt; subject?.sGrade
        1 -&gt; subject?.sCategory
        else -&gt; &quot;&quot;
    } ?: &quot;&quot;
    val disabledModifier = modifier
        .height(input_item_action_icon_size)
        .padding(top = input_item_view_padding, bottom = input_item_view_padding)
        .fillMaxWidth()
        .indication(
            indication = rememberRipple(bounded = true),
            interactionSource = remember { MutableInteractionSource() }
        )

    // 3
    val enabledValue = if (disabled) null
    else inputStateMap?.get(Pair(subject?.sId, editableRowIndex)) ?: remember {
        mutableStateOf(
            value = when (editableRowIndex) {
                2 -&gt; if (subject?.sScore == 0) &quot;&quot; else &quot;${subject?.sScore}&quot;
                3 -&gt; subject?.sName
                else -&gt; &quot;&quot;
            } ?: &quot;&quot;
        ).apply { inputStateMap?.set(Pair(subject?.sId, editableRowIndex), this) }
    }
    val enabledModifier = if (disabled) null else {
        val focusRequester = remember { FocusRequester() }
        val focusValue = remember { mutableStateOf&lt;Boolean?&gt;(null) }

        disabledModifier
            .onFocusChanged {
                Timber.i(&quot; semesterIndex : $semesterIndex columnIndex : $columnIndex editableRowIndex : $editableRowIndex focusMode : $it&quot;)
                // focus 를 잃었을 때 해당 데이터를 갱신해주고 키보드를 내린다.
                if (!disabled &amp;&amp; focusValue.value == true &amp;&amp; !it.hasFocus &amp;&amp; !it.isFocused) {
                    editSubject(semesterIndex, editableRowIndex, columnIndex, enabledValue!!.value)
                    focusValue.value = false
                } else if (it.isFocused) {
                    focusValue.value = true
                }
            }
            .focusRequester(focusRequester)
    }

    val focusManager = LocalFocusManager.current
    // 4
    BasicTextField(
        value = if (disabled) disabledValue else enabledValue!!.value,
        onValueChange = {
            // 5
            if (validateInputCheck(editableRowIndex, it)) {
                enabledValue?.value = it
                inputSelectDialogInfo.editableRowIndex.value = editableRowIndex
                inputSelectDialogInfo.columnIndex.value = columnIndex

                inputDisposedState = if (!disabled)
                    InputDisposedState(
                        semesterIndex = semesterIndex,
                        editableRowIndex = editableRowIndex,
                        columnIndex = columnIndex,
                        value = enabledValue!!.value,
                    )
                else InputDisposedState()
            }
        },
        modifier = (if (disabled) disabledModifier else enabledModifier!!).clickable(
            enabled = true,
            interactionSource = remember { MutableInteractionSource() },
            indication = rememberRipple(bounded = true),
            // 6
            onClick = {
                focusManager.clearFocus()
                when (editableRowIndex) {
                    0 -&gt; {
                        inputSelectDialogInfo.update(
                            openDialog = true,
                            inputDialogType = InputDialogType.GRADE,
                            selectItems = getDialogItems(InputDialogType.GRADE).toList(),
                            editableRowIndex = editableRowIndex,
                            columnIndex = columnIndex
                        )
                    }
                    1 -&gt; {
                        inputSelectDialogInfo.update(
                            true,
                            InputDialogType.CATEGORY,
                            getDialogItems(InputDialogType.CATEGORY).toList(),
                            editableRowIndex,
                            columnIndex,
                        )
                    }
                    else -&gt; {}
                }
            }
        ),
        // 7
        enabled = editableRowIndex == 2 || editableRowIndex == 3,
        textStyle = TextStyle(
            fontSize = 13.sp,
            textAlign = TextAlign.Center,
            color = colorResource(id = R.color.inputTextColor),
        ),
        decorationBox = { innerTextField -&gt;
            if ((disabled &amp;&amp; disabledValue.isEmpty()) || (!disabled &amp;&amp; enabledValue!!.value.isEmpty())) {
                Text(
                    text = hintText,
                    fontSize = 13.sp,
                    color = colorResource(id = R.color.inputHintColor),
                    textAlign = TextAlign.Center
                )
            }
            innerTextField()
        },
        cursorBrush = SolidColor(colorResource(id = R.color.themeTextColor)),
    )
}

// 5
/** 입력한 값의 유효성을 체크한다. 2는 일반 문자열. 3은 숫자만을 받는다. */
private fun validateInputCheck(editableRowIndex: Int, value: String): Boolean {
    val pattern = Pattern.compile(
        when (editableRowIndex) {
            2 -&gt; &quot;[0-9]*&quot;
            3 -&gt; &quot;[0-9a-zA-Z|ㄱ-ㅎㅏ-ㅣ가-힣\\u318D\\u119E\\u11A2\\u2022\\u2025&quot; +
                &quot;\\u00B7\\uFE55\\u4E10\\u3163\\u3161 ]*&quot;
            else -&gt; return false
        }
    )
    return when (editableRowIndex) {
        2 -&gt; pattern.matcher(value).matches() &amp;&amp; value.length &lt; 3
        3 -&gt; pattern.matcher(value).matches()
        else -&gt; false
    }
}
</code></pre>
<p>성적 = 0, 이수구분 = 1, 학점 = 2, 과목명 = 3 으로 생각하여 진행하였다.
추후 개선건으로 상수화가 필요해보인다.</p>
<ol>
<li><p>위 내용에 따라 입력 기능을 활성화할건지, 안할건지에 대한 flag 이다. 
성적, 이수구분은 직접 텍스트를 입력하면 안되므로 이렇게 flag 를 두었다.</p>
</li>
<li><p>비활성화/활성화 여부에 따라 값과 modifier 를 다르게 처리해야 한다.
비활성화 영역 value 와 modifier 초기화를 해주었고, 활성화 영역은 3번에서 처리된다.</p>
</li>
<li><p>위에서 말했다시피 활성화 영역 value 와 modifier 초기화를 해준다
활성화 영역 modifier 는 disabledModifier 의 기능을 온전히 가져야 한다.</p>
<p>그래서 disabledModifier 을 기본 선언하고, 
거기에 추가 기능을 덧붙여 enabledModifier 를 만들었다.
활성화 영역에서는 포커스를 잃거나 얻었을 때의 추가 작업이 필요하므로 이 처리를 추가해주었다.</p>
</li>
<li><p>기존의 TextField 를 못쓰고 상위 TextField 를 썼다는 게 바로 이 이야기이다.
기존에는 TextField 로도 처리가 가능했지만, 기본 padding 을 0으로 할 수 없는 등의 이슈가 있어 TextField 의 상위인 BasicTextField 를 사용했다.
하지만 처음에만 어려웠지, 검색을 하면서 부족한 부분들을 채워나갔고 실제 그렇게 어려운 부분은 아니었다. 
이 과정으로 기존 Composable UI 로 한계가 있을때의 처리 방법을 깨달았다.</p>
</li>
<li><p>이건 왠지 더 좋을 방법이 있을 것 같기도 하다. 입력 시 정규식을 통해 입력을 하도록 했다.
입력 성공 시 Composable UI 내 텍스트를 갱신하고, 
학점 입력 화면이 disposed 되었을 때 넣어줄 값을 갱신하도록 했다.
키보드 상에서 학점을 클릭하면 숫자가 아닌 일반 입력 키보드가 나오는데, 이 부분은 추후 개선건으로 두었다.</p>
</li>
<li><p>클릭시에 대한 처리이다.
성적, 이수구분을 클릭 시 그에 맞는 항목들이 나와야 하기 때문에 
state 들을 모두 update 해주어, recomposition 을 발생시켜 Dialog 가 켜지도록했다.</p>
</li>
<li><p>학점, 과목명은 직접 타이핑을 칠 수 있어야 하므로 이렇게 두었다.
(생각해보니 <code>!disabled</code> 를 하면 되는데 깜빡한 것 같다. 이것도 추후 개선건이다)</p>
</li>
</ol>
<h2 id="3-과목-삭제-버튼을-보여주는-view">3. 과목 삭제 버튼을 보여주는 View</h2>
<p>swipe 를 통해 보여주는 과정은 이야기를 했었다.
실제 어떻게 보여지는 지를 이야기해보려 한다.</p>
<pre><code class="language-kotlin">/** 한 행의 Action Row. 현재는 삭제밖에 없다. */
@Composable
fun InputItemActionsRow(
    modifier: Modifier,
    subjectIndex: Int,
    subject: Subject,
    onDelete: ((Boolean, Int, String) -&gt; Unit)?,
    inputStateMap: MutableMap&lt;Pair&lt;Int?, Int&gt;, MutableState&lt;String&gt;&gt;?,
) {
    val focusManager = LocalFocusManager.current
    Row(modifier = modifier, horizontalArrangement = Arrangement.End) {
        Image(
            modifier = Modifier
                .background(color = colorResource(id = R.color.grayDefaultColor))
                .size(input_item_action_icon_size)
                .padding(input_item_view_padding)
                .clickable(
                    enabled = true,
                    interactionSource = remember { MutableInteractionSource() },
                    indication = rememberRipple(bounded = true),
                    onClick = {
                        focusManager.clearFocus()
                        inputStateMap?.remove(Pair(subject.sId, 0))
                        inputStateMap?.remove(Pair(subject.sId, 1))
                        inputStateMap?.remove(Pair(subject.sId, 2))
                        inputStateMap?.remove(Pair(subject.sId, 3))
                        onDelete?.let { it(true, subjectIndex, subject.sName) }
                    }
                ),
            painter = painterResource(id = R.drawable.btn_input_delete),
            contentDescription = btn_input_delete
        )
    }
}</code></pre>
<p>거창해보였겠지만 사실 별거 없다. 
클릭 시 onDelete 에 적합한 수행을 해주는 것(삭제 dialog 띄우기) 를 해주는 것 밖에 없다.
오히려 추후 개선건들이 눈에 보인다.
inputStateMap 의 경우 진짜 삭제될때 해주어야 하는데 이 부분에서 하고 있어 개선해야 할 것 같다.
동작엔 이상은 없지만 취소시 remember 를 통해 없앴다 생겼다가 반복하기 때문에 성능? 상으로 개선이 필요할 듯 하다.</p>
<h1 id="dialog-inputselectdialogview-inputdeletedialogview">Dialog (InputSelectDialogView, InputDeleteDialogView)</h1>
<p>선택 Dialog, 삭제 확인 Dialog 에 대한 이야기이다.
각 기능이 다르긴하지만 Dialog 라는 큰 틀은 차이가 없다.</p>
<h2 id="1-선택-dialog">1. 선택 Dialog</h2>
<p><img src="https://images.velog.io/images/ricky_0_k/post/fd65fdba-30b1-478b-aa3a-10ab861ef946/image.png" alt="">
여러 항목중에서 하나를 선택하는 Dialog 이다.
성적, 이수구분 아이템을 선택할 수 있으며, 선택 후 해당 값이 특정 과목 항목 내에 적용된다.</p>
<pre><code class="language-kotlin">/**
 * Input 화면 내에서 띄워주는 선택 Dialog 에서 사용되는 Composable 모음
 *
 * @author ricky
 * @since v3.0.0 / 2022.01.03
 */
@Composable
fun InputSelectDialogView(
    vm: InputViewModel? = null,
    inputDialogInfo: InputDialogInfo.InputSelectDialogInfo
) {
    MaterialTheme {
        // 1
        if (inputDialogInfo.openDialog.value)
            AlertDialog(
                onDismissRequest = { inputDialogInfo.openDialog.value = false },
                title = {
                    Text(
                        modifier = Modifier.padding(top = 10.dp, bottom = 10.dp),
                        text = inputDialogInfo.inputDialogType.value.title,
                        fontSize = 18.sp,
                        fontWeight = FontWeight(700),
                    )
                },
                text = {
                    Column {
                        Spacer(modifier = Modifier.height(20.dp))
                        LazyColumn {
                            items(count = inputDialogInfo.selectItems.value.size) { position -&gt;
                                InputSelectDialogItemView(
                                    vm,
                                    inputDialogInfo.openDialog,
                                    inputDialogInfo.selectItems.value[position],
                                    inputDialogInfo.semesterIndex.value,
                                    inputDialogInfo.editableRowIndex.value,
                                    inputDialogInfo.columnIndex.value,
                                )
                            }
                        }
                    }
                },
                confirmButton = {}
            )
    }
}

@Composable
fun InputSelectDialogItemView(
    vm: InputViewModel?,
    openDialog: MutableState&lt;Boolean&gt;,
    selectItem: String,
    semesterIndex: Int,
    editableRowIndex: Int,
    columnIndex: Int,
) {
    Text(
        modifier = Modifier
            .fillMaxWidth()
            .clickable(
                enabled = true,
                interactionSource = remember { MutableInteractionSource() },
                indication = rememberRipple(bounded = true),
                onClick = {
                    vm?.editSubject(semesterIndex, editableRowIndex, columnIndex, selectItem)
                    openDialog.value = false
                }
            )
            .padding(top = 10.dp, bottom = 10.dp),
        text = selectItem,
        fontSize = 15.sp,
        fontFamily = nanumSquareFamily,
        color = colorResource(id = R.color.black),
    )
}</code></pre>
<ol>
<li>JetpackCompose Playground 에서 구현 방법을 참고하여 작성했다. (AlertDialog)
state 를 통해 열리는지 닫히는지를 구분했고, 리스트를 보여주어야 하기에 LazyColumn 도 사용했다.<h2 id="2-삭제-확인-dialog">2. 삭제 확인 Dialog</h2>
<img src="https://images.velog.io/images/ricky_0_k/post/387a2876-9681-4a08-9e95-029448c9bec3/image.png" alt=""></li>
</ol>
<p>정말 삭제를 할 것인지 물어보는 Dialog 이다.</p>
<pre><code class="language-kotlin">/**
 * Input 화면 내에서 띄워주는 삭제 Dialog 에서 사용되는 Composable 모음
 *
 * @author ricky
 * @since v3.0.0 / 2022.01.03
 */
@Composable
fun InputDeleteDialogView(
    vm: InputViewModel? = null,
    inputDeleteDialogInfo: InputDialogInfo.InputDeleteDialogInfo,
) {
    MaterialTheme {
        if (inputDeleteDialogInfo.openDialog.value)
            AlertDialog(
                onDismissRequest = { inputDeleteDialogInfo.openDialog.value = false },
                title = {
                    Text(
                        modifier = Modifier.padding(top = 10.dp, bottom = 10.dp),
                        text = &quot;${inputDeleteDialogInfo.subjectInfo} 과목을 삭제하시겠습니까?&quot;,
                        fontSize = 18.sp,
                        fontWeight = FontWeight(700),
                    )
                },
                dismissButton = {
                    Text(
                        modifier = Modifier
                            .clickable(
                                enabled = true,
                                interactionSource = remember { MutableInteractionSource() },
                                indication = rememberRipple(bounded = true),
                                onClick = { inputDeleteDialogInfo.openDialog.value = false }
                            )
                            .padding(12.dp),
                        text = &quot;취소&quot;,
                        fontSize = 15.sp,
                        fontFamily = nanumSquareFamily,
                        color = colorResource(id = R.color.themeTextColor),
                    )
                },
                confirmButton = {
                    Text(
                        modifier = Modifier
                            .clickable(
                                enabled = true,
                                interactionSource = remember { MutableInteractionSource() },
                                indication = rememberRipple(bounded = true),
                                onClick = {
                                    vm?.removeSubject(
                                        inputDeleteDialogInfo.semesterIndex.value,
                                        inputDeleteDialogInfo.columnIndex.value
                                    )
                                    inputDeleteDialogInfo.openDialog.value = false
                                }
                            )
                            .padding(12.dp),
                        text = &quot;확인&quot;,
                        fontSize = 15.sp,
                        fontFamily = nanumSquareFamily,
                        color = colorResource(id = R.color.themeTextColor),
                    )
                },
            )
    }
} </code></pre>
<p>위 선택 Dialog 와 큰 차이는 없다.</p>
<h2 id="3-dialoginfo">3. DialogInfo</h2>
<p>아래와 같이 sealed class 를 통해 자식 클래스들을 만들었으며, 
각 특징에 따라 다른 값들도 가질 수 있도록 하였다.</p>
<pre><code class="language-kotlin">/**
 * InputView 에서 다이얼로그를 띄울 때 사용하는 최소 State 값 모음
 *
 * @param openDialog dialog 가 열렸는지 여부
 * @param semesterIndex &quot;((semesterIndex + 1) / 2 학년 (semesterIndex % 2) 학기&quot; 공식에서 semesterIndex
 * @param columnIndex 선택한 행의 index 값
 *
 * @author ricky
 * @since v3.0.0 / 2022.01.03
 */
sealed class InputDialogInfo(
    open val openDialog: MutableState&lt;Boolean&gt;,
    open val semesterIndex: State&lt;Int&gt;,
    open val columnIndex: MutableState&lt;Int&gt;,
) {
    /**
     * [InputView] 에서 선택 다이얼로그를 띄울 때 사용하는 State 값 모음
     *
     * @param openDialog dialog 가 열렸는지 여부
     * @param inputDialogType 지금 열린 dialog 의 타입 (성적, 이수구분)
     * @param selectItems 해당 dialog 에서 선택할 수 있는 item 목록
     * @param semesterIndex &quot;((semesterIndex + 1) / 2 학년 (semesterIndex % 2) 학기&quot; 공식에서 semesterIndex
     * @param editableRowIndex 선택한 열의 index 값 (0 : 성적, 1 : 이수구분, 2 : 학점, 3 : 과목명)
     * @param columnIndex 선택한 행의 index 값
     */
    data class InputSelectDialogInfo(
        override val openDialog: MutableState&lt;Boolean&gt;,
        val inputDialogType: MutableState&lt;InputDialogType&gt;,
        val selectItems: MutableState&lt;List&lt;String&gt;&gt;,
        override val semesterIndex: State&lt;Int&gt;,
        val editableRowIndex: MutableState&lt;Int&gt;,
        override val columnIndex: MutableState&lt;Int&gt;,
    ) : InputDialogInfo(openDialog, semesterIndex, columnIndex) {
        fun update(
            openDialog: Boolean,
            inputDialogType: InputDialogType,
            selectItems: List&lt;String&gt;,
            editableRowIndex: Int,
            columnIndex: Int,
        ) {
            this.openDialog.value = openDialog
            this.inputDialogType.value = inputDialogType
            this.selectItems.value = selectItems
            this.editableRowIndex.value = editableRowIndex
            this.columnIndex.value = columnIndex
        }
    }

    /**
     * [InputView] 에서 삭제 다이얼로그를 띄울 때 사용하는 State 값 모음
     *
     * @param openDialog dialog 가 열렸는지 여부
     * @param semesterIndex &quot;((semesterIndex + 1) / 2 학년 (semesterIndex % 2) 학기&quot; 공식에서 semesterIndex
     * @param columnIndex 선택한 행의 index 값
     * @param subjectInfo 삭제할 subject 정보 update 를 통해 항상 갱신시킨다.
     */
    data class InputDeleteDialogInfo(
        override val openDialog: MutableState&lt;Boolean&gt;,
        override val semesterIndex: State&lt;Int&gt;,
        override val columnIndex: MutableState&lt;Int&gt;,
        var subjectInfo: String = &quot;&quot;
    ) : InputDialogInfo(openDialog, semesterIndex, columnIndex) {
        fun update(openDialog: Boolean, columnIndex: Int, subjectInfo: String) {
            this.openDialog.value = openDialog
            this.columnIndex.value = columnIndex
            this.subjectInfo = subjectInfo
        }
    }
}</code></pre>
<h1 id="inputstatemap">inputStateMap</h1>
<p>화면에서의 내용들은 얼추 이야기한 것 같은데 
중간중간 언급되는 inputStateMap 에 대한 설명이 필요해보여 이 파트를 만들었다. </p>
<p>결론적으로는 key 를 제대로 활용하지 못해 생긴 map 이다.
새로 recomposition 할 때 list 아이템들이 새로 그려지면서 매번 새로운 Composable UI 가 만들어졌고, 그로 인해 remember 을 통해 새로운 state 를 항상 만들게 되었다.</p>
<p>동작에는 문제가 없었지만 당시에는 매번 새로운 state 가 생기는 걸 막고 재활용하고 싶어 이렇게 했었다.
그런데 keys 설정을 해서 리스트 갱신때마다 모든 아이템들이 새 UI 가 그려지는 걸 막으면 되는 거였다.</p>
<p>빠르게 런칭하기 위해 깊게 바라보지 못했던 문제여서, 추후 개선건으로 이 작업도 같이 진행하려 한다.</p>
<h1 id="결론">결론</h1>
<p>휴우.. 다 정리했다. 돌아보니 개선건들이 더 많이 보이는 건 뭔가... 
앞으로 할 게 더 많다는 소리겠지 :)</p>
<p>사실 이 화면작업에서 하고 싶은 건 많았다. 드래그를 통해 과목의 위치도 옮길 수 있게 구현하려 했다.
사정상 반영은 못했고 추후 개선건으로 남아있다.</p>
<p>feature 도 feature 지만 지금 이렇게 쫙 돌아봤는데도 개선건들이 많이 보인다... (일부 주석도 수정이 필요해보인다.)
1월말 런칭을 목표로 했더니 그만큼 부작용도 많이 남았던 코드인 것 같아 아쉬움도 남는다.</p>
<p>그런 만큼 이 시리즈를 작성하는 것에 노력을 해야할 것 같다.
빨리 돌아보고 취합해서 개선해야겠다는 생각이 느껴지기에 열렬히 포스팅을 올려야 겠다는 생각을 했다.</p>
<p>다음 포스팅은 <code>학점 통계 화면</code> 이다.
여기에서는 거꾸로 Compose 내에서 기존 view 를 만들어서 처리하도록 했다.
예를 들면 compose 내에 TabLayout 구현하기? 
학점 입력 화면과의 차이를 보기 위해 이렇게 구현했었는데 실제 차이를 느꼈다.
그 외에도 권한 설정, context 사용 방법, 기타 다양한 view 활용 내역들을 정리해서 포스팅할 예정이다.</p>
<p>참고</p>
<ul>
<li><a href="https://levelup.gitconnected.com/implement-tablayout-with-viewpager-in-android-jetpack-compose-d509fc6e2d8e">https://levelup.gitconnected.com/implement-tablayout-with-viewpager-in-android-jetpack-compose-d509fc6e2d8e</a></li>
<li><a href="https://gist.github.com/evansgelist/aadcd633e9b160f9f634c16e99ffe163">https://gist.github.com/evansgelist/aadcd633e9b160f9f634c16e99ffe163</a></li>
<li><a href="https://medium.com/google-developer-experts/focus-in-jetpack-compose-6584252257fe">https://medium.com/google-developer-experts/focus-in-jetpack-compose-6584252257fe</a></li>
<li><a href="https://kotlinworld.com/185">https://kotlinworld.com/185</a></li>
<li><a href="https://kotlinworld.com/210">https://kotlinworld.com/210</a></li>
<li><a href="https://kotlinworld.com/257">https://kotlinworld.com/257</a></li>
<li><a href="https://proandroiddev.com/swipe-to-reveal-in-jetpack-compose-6ffa8928a4c2">https://proandroiddev.com/swipe-to-reveal-in-jetpack-compose-6ffa8928a4c2</a></li>
<li><a href="https://developer.android.com/reference/kotlin/androidx/compose/animation/core/Transition?hl=hr">https://developer.android.com/reference/kotlin/androidx/compose/animation/core/Transition?hl=hr</a></li>
<li><a href="https://foso.github.io/Jetpack-Compose-Playground/material/alertdialog/">https://foso.github.io/Jetpack-Compose-Playground/material/alertdialog/</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Compose 무작정 맛보기 [3. 메인 화면]
]]></title>
            <link>https://velog.io/@ricky_0_k/Compose-%EB%AC%B4%EC%9E%91%EC%A0%95-%EB%A7%9B%EB%B3%B4%EA%B8%B0-3.-%EB%A9%94%EC%9D%B8-%ED%99%94%EB%A9%B4</link>
            <guid>https://velog.io/@ricky_0_k/Compose-%EB%AC%B4%EC%9E%91%EC%A0%95-%EB%A7%9B%EB%B3%B4%EA%B8%B0-3.-%EB%A9%94%EC%9D%B8-%ED%99%94%EB%A9%B4</guid>
            <pubDate>Sat, 26 Mar 2022 16:11:23 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p>자 메인 화면이다. (학점을 아름답게 볼 수 있는 화면)
메인화면 개발 당시에는 <code>xml 내에 jetpack compose 를 어떻게 쓸 수 있는지</code> 에 중점을 두어 개발을 했었다.</p>
<p>참고로 공식문서에서는 xml 과 jetpack compose 의 호환이 매우 좋다고 했었다.
몇 퍼센트였는지는 기억이 안나지만, 좋다고 했으므로 정말 그런지 한 번 확인해보자.</p>
<h1 id="메인-화면-리펙터링">메인 화면 리펙터링</h1>
<p>먼저 한 작업은 MVP 코드를 MVVM 으로 변환하는 작업이었다.
더불어 BindingAdapter 를 활용하여 총 학점에 따라 배경이나 버튼 색상이 달라지던 코드를 개선했다.
<img src="https://images.velog.io/images/ricky_0_k/post/d3f02787-3ae7-46cb-886a-e65e50900e8a/image.png" alt=""></p>
<p>(참고로, 사진에 보이는 두 번째, 세 번째 함수는 이후에 xml 에 compose 를 넣으면서 없어졌다.)</p>
<p>어쨌든 당시에는 이런 식으로 리펙터링했다만, 잘 한건지는 모르겠다.
기존에 한번에 처리했던 걸 쪼개 놓았는데, 변경될 일도 없는거긴 해서 괜히했나 싶긴 하다만 
오른쪽보다 왼쪽이 덕지덕지 붙어있는 느낌인건 왜일까 싶다.
어쨌든 이런 과정을 거치면서 코드 라인은 10~20줄 정도 줄였다.</p>
<h1 id="custom-progressbar-리펙터링">Custom ProgressBar 리펙터링</h1>
<p>일단 이름부터 바꿔야 했다. 
기존엔 <code>MainProgressBar</code> 였는데, 통계화면에서도 쓰이기 때문에 범용적인 이름 변경이 필요했다.</p>
<p>그리고 기존 Java 코드를 Kotlin 으로 바꾸었고
주석 보강, 함수 및 반복적인 로직 정리 등을 통해 코드를 정리해 나갔다.
그래서 보기에는 좋아졌지만 이건 오히려 코드가 늘었다. 한 3~40줄 정도?
코드를 다시 이해하면서 주석이 없어 불편했다보니 그것들을 추가했고 그러면서 코드라인이 많이 늘은 것 같다.</p>
<p><img src="https://images.velog.io/images/ricky_0_k/post/904eb014-ed1f-475f-8913-dd5aaa73aebf/image.png" alt=""></p>
<p>예시를 들면 이렇다고 할까...?
그 외에도 애매한 함수 명을 변경하는 등 코드를 다시봐도 이해하기 위한 방향으로 리펙터링을 했다.</p>
<h1 id="xml-에-compose-같은-걸-끼얹나">xml 에 compose 같은 걸 끼얹나?</h1>
<p>자 사실상 본론이다. 과연 xml 에 compose 는 잘 적용이 될까?
<img src="https://images.velog.io/images/ricky_0_k/post/71a6be82-849b-4ab7-8450-0226b5f08732/image.png" alt=""></p>
<p><img src="https://images.velog.io/images/ricky_0_k/post/0b9bbf4f-0892-4680-97ec-e04bfb904b6b/image.png" alt=""></p>
<p>놀랍게도 그렇다 위의 xml 처럼 기존의 레이아웃을 <code>ComposeView</code> 로 처리하고
<code>onCreate</code> 에서 <code>layoutId.setContent { ... }</code> 하여 
람다 내에 Composable 함수를 호출해주면 된다. 코드로 보면 이렇다.</p>
<pre><code class="language-kotlin">binding.clMainInput.setContent {
    MainBottomItemView(viewModel, MAIN_BOTTOM_ITEM_INPUT)
}

binding.clMainStatistic.setContent {
    MainBottomItemView(viewModel, MAIN_BOTTOM_ITEM_STATISTIC)
}</code></pre>
<p><code>MAIN_BOTTOM_ITEM_INPUT</code>, <code>MAIN_BOTTOM_ITEM_STATISTIC</code> 은 어떤 버튼인지에 대한 flag 값이다.</p>
<p><img src="https://images.velog.io/images/ricky_0_k/post/d6c0a2f8-23d8-496f-8a16-682748bb7986/image.png" alt=""></p>
<p>그리고 파란색 내의 각 버튼은 사실상 기능이 똑같다.</p>
<ol>
<li>클릭하여 다른 화면으로 넘어감</li>
<li>총 학점 현황에 따라 이미지가 다름</li>
</ol>
<p>이렇게 2개 기능밖에 없기에 이 기능들을 통합했다.
그래서 기존에 리펙터링하면서 각 버튼의 bindingAdapter 로직을 Composable 함수에 넣었다.</p>
<p>그리고 동일한 기능이라는 건 통합도 가능하다는 말이다.
위의 setContent 코드에서 예상한 분들도 있었겠지만 
Composable 함수의 두번째 파라미터 값이 각각 어떤 이미지, 클릭 이벤트를 쓸 것인지를 분기한다.</p>
<pre><code class="language-kotlin">val title = arrayOf(
    stringResource(id = R.string.tv_main_input),
    stringResource(id = R.string.tv_main_statistic)
)[position]

val drawables = arrayOf(
    listOf(
        R.drawable.m_input_a,
        R.drawable.m_input_b,
        R.drawable.m_input_c,
        R.drawable.iv_main_input
    ),
    listOf(
        R.drawable.m_statistic_a,
        R.drawable.m_statistic_b,
        R.drawable.m_statistic_c,
        R.drawable.iv_main_statistic
    )
)[position]

val clickEvent = viewModel?.let {
    arrayOf(
        it::clickInputLayout,
        it::clickStatisticLayout,
    )[position]
} ?: { Timber.i(&quot;for Preview&quot;) }</code></pre>
<p>이런 식으로 처리했으며 bindingAdapter 에서 중복 코드가 있어 아쉬웠던 내용을 이런 방식으로 개선했다.</p>
<pre><code class="language-kotlin">val mainScore = viewModel?.mainScore?.observeAsState() ?: remember { mutableStateOf(0f) }

Column(
    // 1
    modifier = Modifier.clickable(
        enabled = true,
        onClick = clickEvent,
        indication = rememberRipple(bounded = true),
        interactionSource = remember { MutableInteractionSource() }
    ),
    horizontalAlignment = Alignment.CenterHorizontally,
    verticalArrangement = Arrangement.Center
) {
    Image(
        painter = painterResource(
            id = mainScore.value?.let {
                // 2
                when {
                    it &gt;= 4 -&gt; drawables[0]
                    it &gt;= 3 &amp;&amp; it &lt; 4 -&gt; drawables[1]
                    it &gt;= 2 &amp;&amp; it &lt; 3 -&gt; drawables[2]
                    else -&gt; drawables[3]
                }
            } ?: drawables[3]
        ),
        contentDescription = title
    )
    Spacer(modifier = Modifier.height(32.dp))
    Text(
        text = title,
        fontSize = 15.sp,
        color = colorResource(id = R.color.defaultTextColor),
        textAlign = TextAlign.Center
    )
}</code></pre>
<p>실제 View 는 이렇다.</p>
<ol>
<li>이전 포스팅에서는 ripple 을 없앴지만, 여기에서는 ripple 을 넣었다.
indication, interactionSource 를 설정해야 하며, 이렇게 하면 ripple 효과가 발생한다.</li>
<li>위에서 말했던 중복 로직을 통합한 내용이다.</li>
</ol>
<h1 id="결론">결론</h1>
<p>생각보다 적을 내용이 없을 정도로 간단했다.
우리는 앞으로 ComposeView 에 setContent 를 설정하여 Composable 함수를 호출해주면 
xml 내에 녹아드는 Compose 를 볼 수 있을 것이다.</p>
<p>여기까지는 되게 간단했었다. 하지만... 다음장 (학점 입력 화면) 에서 정말 한 것이 많았다.
commit 기록을 돌아보아도 여기 리펙터링 과정이 한 2주정도 걸린 것 같다.
당연히 온전히 100% 2주는 아니고, 짜투리 시간에서 2주이지만 그래도 다른 작업에 비해 오래 걸렸다.</p>
<ol>
<li>state 관리하기</li>
<li>상위 TextField 사용하기</li>
<li>focus 에 따라 실시간 학점 계산하도록 하기</li>
</ol>
<p>많은 작업이 있었고 다음 포스팅에서 요 내용들을 다뤄보려 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Compose 무작정 맛보기 [2. State 와 Composable 의 LifeCycle 이야기]
]]></title>
            <link>https://velog.io/@ricky_0_k/Compose-%EB%AC%B4%EC%9E%91%EC%A0%95-%EB%A7%9B%EB%B3%B4%EA%B8%B0-2.-State-%EC%99%80-Composable-%EC%9D%98-LifeCycle-%EC%9D%B4%EC%95%BC%EA%B8%B0</link>
            <guid>https://velog.io/@ricky_0_k/Compose-%EB%AC%B4%EC%9E%91%EC%A0%95-%EB%A7%9B%EB%B3%B4%EA%B8%B0-2.-State-%EC%99%80-Composable-%EC%9D%98-LifeCycle-%EC%9D%B4%EC%95%BC%EA%B8%B0</guid>
            <pubDate>Fri, 25 Mar 2022 18:32:44 GMT</pubDate>
            <description><![CDATA[<h1 id="변명">변명</h1>
<p>얼마만에 업데이트인지 모르겠다.</p>
<p>코로나는 1월말에 걸렸다가 다 나았고, 2~3월에는 공식문서나 묵혔던 책도 보기로 마음 먹었었다.
묵혔던 책의 경우는 <a href="http://www.kyobobook.co.kr/product/detailViewKor.laf?mallGb=KOR&amp;ejkGb=KOR&amp;barcode=9788966263073">안드로이드 프로그래밍 NEXT STEP</a> 였는데 지금은 다 읽은 상태이다.
도움이 많이 되었었고, 아직 다 이해가 되지 않은 내용들도 있어 한 번 더 봐야될 것 같다.</p>
<p>그러면서 예상치 못했던 일들도 경험하면서 명치를 씨게 맞고 번아웃도 조금 오긴 왔지만 
그만큼 내적, 외적으로 깨달았던 것들도 있었던 것 같다. 근황은 여기까지.. 앞으로 할 건 더욱 많은 것 같다.</p>
<p>작년을 회고하면서 내가 세웠던 목표는 최소 1달에 한 번은 포스팅을 올리는 것이었다.
저번 글을 쓰고 feel 받아 계속 써야했지만 타이밍을 놓쳤던 것 같다.</p>
<p>새로운 주제를 가진 포스팅도 올리고 싶지만, 
일단은 밀린 것들 (CI/CD, Jetpack Compose) 을 정리할 필요를 느꼈기에 
밀린 것들을 상반기에 모두 정리해보려 한다.</p>
<h1 id="이제-진짜-서론">(이제 진짜) 서론</h1>
<p>(기억이 가물가물한) 이전 포스팅에서 <code>4.3</code>, <code>4.5</code> 동그라미 버튼의 선택값을 아래와 같이 기억하도록 했다.</p>
<pre><code class="language-kotlin">val enablePosition = vm?.enablePosition?.observeAsState()</code></pre>
<p>지난 포스팅에서 <code>vm?.enablePosition</code> 은 LiveData 이며
이 값을 state 형태로 바꾸어 Composable 를 새로 그려주는 작업 (이하 ReComposition) 을 할 수 있게 했다고 말했었다.
그리고 ReComposition 은 Compose LifeCycle 와도 관련이 있다고 했었는데 오늘은 이 내용을 정리해보려 한다.</p>
<h1 id="state">State</h1>
<p><a href="https://developer.android.com/jetpack/compose/state">안드로이드 공식 문서 - State and Jetpack Compose</a> 에는 이렇게 설명하고 있다.</p>
<blockquote>
<p>State in an app is <code>any value that can change over time.</code>
This is a very broad definition and encompasses everything from a Room database to a variable on a class.
...</p>
<p>app의 State 는 <code>시간이 지남에 따라 변경될 수 있는 모든 값</code>입니다. 
이것은 매우 광범위한 정의이며, Room 데이터베이스에서 클래스의 변수에 이르기까지 모든 것을 포함합니다.
...</p>
</blockquote>
<p><code>...</code> 에는 블로그 게시물 및 관련 댓글, 네트워크 연결 실패시 보여주는 snackbar, ripple 애니메이션 효과 등 다양한 예시를 이야기했다. 
state 를 활용해 앱 구현을 하고 돌아보는 입장에서 다시 보니, 이 내용이 눈에 들어왔다.</p>
<h2 id="실제-앱-내에서-사용한-state-예로-다시-본-내용">실제 앱 내에서 사용한 State 예로 다시 본 내용</h2>
<p>실제 내가 앱 내에서 사용했던 state 들은 아래와 같다.</p>
<ol>
<li>기본 점수 설정 화면에서 선택한 기본 점수 (4.3, 4.5)</li>
<li>학점 입력 화면에서, 현재 학기, 해당 학기의 과목들, 열 index, 보기모드 활성화 여부</li>
<li>학점 입력 화면에서, 내가 선택한 과목 type 과 학점 및 입력한 값</li>
<li>ripple 애니메이션 설정에서 사용했던 state</li>
<li>기타 (focus 관련 등) </li>
</ol>
<p>대체로 내가 선택하거나 직접 입력한 값들 (1,3), 보여주는 컨텐츠 또는 내부에서 관리하는 값(2), 애니메이션 및 기타 값(4,5) 이었고, 연결해보면 <code>...</code> 에서 언급된 예시와 매치가 되는 내용들이었다.</p>
<p>위에서 언급한 값들은 어떤 공통점이 있을까? 위 블록 내용대로 변경될 수 있는 값이다.</p>
<ul>
<li>내가 선택 혹은 직접 입력하면서 값이 변경될 수 있다.</li>
<li>내부의 값을 변경하여 현재 설정된 모드를 바꿀 수 있고, 보여지는 컨텐츠들이 변경될 수 있다.</li>
<li>애니메이션 활성화로 View 모습이 (잠시) 변경될 수 있다. </li>
</ul>
<p>모두 시간이 지남에 따라 변경될 수 있는 내용들이다.</p>
<p>Composable 에서는 이런 변경에 대한 내용을 state 로 관리를 하고, 
state 가 바뀌면 그 값에 따라 새로 그려준다. (Recomposition)</p>
<h2 id="공식-문서의-예">공식 문서의 예</h2>
<p>아래는 공식문서의 예제이다.</p>
<pre><code class="language-kotlin">@Composable
fun HelloContent() {
   Column(modifier = Modifier.padding(16.dp)) {
       Text(
           text = &quot;Hello!&quot;,
           modifier = Modifier.padding(bottom = 8.dp),
           style = MaterialTheme.typography.h5
       )
       OutlinedTextField(
           value = &quot;&quot;,
           onValueChange = { },
           label = { Text(&quot;Name&quot;) }
       )
   }
}</code></pre>
<p>이 코드를 실행하면 어떻게 되는지 확인해보면, 아무것도 발생하지 않는 걸 볼 수 있다.
(아마 텍스트 입력 커서는 활성화 되겠지만 입력해도 아무런 동작이 없을 것이다.)</p>
<p>왜 그럴까? Composable UI 에서 업데이트 하는 유일한 방법은 <code>새 상태를 알리는 것</code> 밖에 없기 때문이다.
기존 XML 방식과 다르게 값에 대한 상태를 <code>명시하지 않거나</code>, 상태를 <code>갱신하지 않으면</code> 
Composable 기반 UI 는 동작하지 않거나, 값을 갱신하지 않는다.</p>
<p>그럼 위 코드를 동작하게 만들려면 어떻게 해야할까?
정답부터 이야기하면 아래와 같이 작성해야 할 것이다.</p>
<pre><code class="language-kotlin">@Composable
fun HelloContent() {

    // 
    val text = remember { mutableStateOf(&quot;&quot;) }

    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = &quot;Hello!&quot;,
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.h5
        )
        OutlinedTextField(

            // 
            value = text.value,

            onValueChange = {

                // 
                text.value = it

            },
            label = { Text(&quot;Name&quot;) }
        )
    }
}</code></pre>
<p>주석(<code>//</code>) 아래에 있는 로직(?)을 추가하면 xml 에서 EditText 를 추가한 듯한 동작이 나올것이다.
저 추가된 로직(?) 에 대해 깊게 확인해보자</p>
<h3 id="1-val-text--remember--mutablestateof-">1. val text = remember { mutableStateOf(&quot;&quot;) }</h3>
<p>text 라는 State 타입 property 를 선언한다는 건 이해했는데, 
<code>remember</code> 과 <code>mutableStateOf()</code> 는 뭘까?</p>
<h3 id="remember">remember</h3>
<p>위의 OutlinedTextField 를 기반으로 이야기해보겠다.
OutlinedTextField 는 글을 입력할 수 있는 Composable UI 이다.</p>
<p>비슷한 기능인 EditText 에서는 입력한 글에 대한 상태를 관리해줄 필요가 없지만, 
OutlinedTextField 에서는 상태를 관리해주어야 한다. 
과연 OutlinedTextField 에서는 글에 대한 상태를 어떻게 불러올 수 있을까?</p>
<p>이는 Composable UI 내에서 가지고 있는 메모리 덕분에 가능하다. 
실제 공식문서에도 아래의 내용이 있다.</p>
<blockquote>
<p>Composable functions can store a single object in memory by using the remember composable. </p>
</blockquote>
<p>single object 라는 말이 모호하지만 확실한 건 Composable 함수는 메모리 내에 single object 를 저장할 수 있다는 것이고, 이는 remember composable 을 통해 가능하다는 것이다. </p>
<p>결론을 정리하면 우린 이렇게 이야기할 수 있다.</p>
<blockquote>
<p>remember 을 통해 우리는 Composable UI 에 정보를 상태값을 저장하고 불러올 수 있다.</p>
</blockquote>
<p>참고) 별개의 이야기로 remember 도 composable 이다. 실제 구현부를 보면 아래와 같다.</p>
<pre><code class="language-kotlin">@Composable
inline fun &lt;T&gt; remember(calculation: @DisallowComposableCalls () -&gt; T): T =
    currentComposer.cache(false, calculation)</code></pre>
<p>이말인 즉슨 remember 를 Composable 함수 외에서는 못쓴다는 것이다.</p>
<h3 id="mutablestateof">mutableStateOf()</h3>
<p>remember 을 통해 Composable UI 에 값을 저장한다는 걸 알았으니, 이건 쉽게 이해될 것이다.
변경가능한(mutable) 상태 타입 인스턴스를 만든다는 말이고 처음에 들어가는 인자는 초기값이다.</p>
<p>그런데 궁금하다 왜 직접 클래스 호출 방식(ex. <code>MutableState()</code>) 은 사용하지 않을까?
이는 실제 구현부를 보면 답이 나온다.</p>
<pre><code class="language-kotlin">@Stable
interface MutableState&lt;T&gt; : State&lt;T&gt; {
    override var value: T
    operator fun component1(): T
    operator fun component2(): (T) -&gt; Unit
}</code></pre>
<p>공식문서에서는 value 만 있으며, 이외 operator 함수는 아래 의미를 가진다고 한다. </p>
<ul>
<li>component1() : mutableState 내부의 값을 나타냄 (as like getter)</li>
<li>component2() : 값이 들어왔을 때 값을 세팅하는 람다 식 (as like setter)</li>
</ul>
<p>이미 interface 로 구현되어 있으므로 당연히 만드는 게 불가능하고, 
MutableState 를 상속받는 다른 자식 클래스들도 있는 걸 보면 (ex. SnapshotMutableState 등), 
직접 인스턴스화하여 구현하는 방식을 지양하기 위해 이렇게 호출하는 것 같다.</p>
<h3 id="2-value--textvalue">2. value = text.value</h3>
<p>말 그대로 state.value 를 OutlinedTextField 에 넣어 상태 값과 UI 를 연결시키는 것이다.
state 값이 변경된 경우의 결과는 3번에서 다룬다.</p>
<h3 id="3-textvalue--it">3. text.value = it</h3>
<p>상태의 값에 새로운 값을 주입하는 것이다.
state 타입의 값이 변경되면, 해당 state 에 연결된 Composable UI 의 recomposition 이 시작된다.</p>
<h2 id="stateful-vs-stateless">StateFul vs Stateless</h2>
<p>메모리를 사용해 객체를 저장하기 때문에, Composable UI 의 내부 상태 값은 State 에 의해 관리된다.
이에 대한 단점도 존재한다. 해당 Composable UI 를 호출하는 곳에서 State 변경을 할 수 없기에 
재사용성이 떨어지고, 테스트를 하기에도 어려운 측면이 있다.</p>
<p>이런 단점을 극복하기 위해 State Hoisting pattern 을 사용하여 상태를 갖지 않는 Composable 로 변경이 가능하다고 한다.</p>
<h2 id="state-hoisting">State hoisting</h2>
<p>말 그대로 상태를 관리하는 곳을 위(ex. caller) 로 끌어올린 느낌인데, 
이는 앞서 말한 viewModel 이 그 예가 될 수 있다.</p>
<p>실제 장점은 아래와 같다고 한다.</p>
<ol>
<li>Single source of truth
상태를 복제하지 않고 하나의 포인트에서만 상태를 관리한다.</li>
<li>Encapsulated
stateful composable 만 상태를 수정할 수 있으므로, 캡슐화가 되어 있다.</li>
<li>Sharable
여러 composable 에서 참고할 수 있다.</li>
<li>Interceptable
상태가 바뀌는걸 caller 부분에서 제어할 수 있다.</li>
<li>Decoupled
state는 어디에도 저장될 수 있으며, Viewmodel 같은 곳으로 옮겨져서 처리할 수 있다.
state가 hoisting 되면 composable과 state는 의존관계가 없어진다.</li>
</ol>
<p>아래는 실제 구글 공식문서의 예이다.</p>
<pre><code class="language-kotlin">@Composable
fun HelloScreen() {
    var name by rememberSaveable { mutableStateOf(&quot;&quot;) }

    HelloContent(name = name, onNameChange = { name = it })
}

@Composable
fun HelloContent(name: String, onNameChange: (String) -&gt; Unit) {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = &quot;Hello, $name&quot;,
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.h5
        )
        OutlinedTextField(
            value = name,
            onValueChange = onNameChange,
            label = { Text(&quot;Name&quot;) }
        )
    }
}</code></pre>
<p>HelloScreen (일종의 호출부) 에서 HelloContent 를 다루고 있다.</p>
<p>HelloContent 의 동작(event) 가 HelloScreen 으로 전파되고
HelloScreen 은 state 의 변경에 맞춰 호출 순서대로 상태변경 내역을 반영해나간다.
(name 을 관리하는 HelloContent 에서 HelloScreen 으로 내려간다.)</p>
<p>아래 그림이 위 설명을 표현한 도식이다.
단방향 데이터 흐름도 지키면서, 분리가 되므로
보다 코드 관리는 용이해질 것이다.</p>
<p><img src="https://images.velog.io/images/ricky_0_k/post/5eeb53c2-aafc-441d-b506-4ba8dead3324/image.png" alt=""></p>
<p>주의사항도 일부 있다. 어찌보면 당연한 이야기여서 언급만 하고 넘어간다.</p>
<ul>
<li>최대한 낮은 공통 부모로 호이스팅되어야 한다.</li>
<li>최대한 높은 자식에 상태가 변경될 수 있어야 한다.</li>
<li>동일한 이벤트에 대한 응답으로 두 상태가 변경되면 함께 호이스팅 되어야 한다.</li>
</ul>
<h2 id="remembersaveable">rememberSaveable</h2>
<p>compose 를 사용하면서 이 이야기가 나올 수 있다.</p>
<pre><code>상태 변경 되거나, 앱이 중간에 죽어서 다시 키는 경운 어떻게 해야하나요</code></pre><p>이럴 때는 rememberSaveable 를 사용해야 한다.
번들에 추가할 수 없는 데이터를 저장하는 경우 Parcelize, MapSaver 등을 사용해야한다.</p>
<p>내가 만든 앱은 portrait 고정이어서 이 작업은 하지 않았지만 
ViewModel 을 활용하면 쉽게 해결되므로 이런 것도 있구나 하고 넘어갔다.</p>
<ol start="2">
<li>LiveData, Flow, RxJava2 와 연동된다.
위 코드에서 언급했었지만 <code>observeAsState()</code> 를 통해,
Composable 에 Observable 형 변수를 연결시킬 수 있다.</li>
</ol>
<h1 id="compose-lifecycle">Compose LifeCycle</h1>
<p>아까 우리는 State hoisting 을 활용해 데이터를 바꿔주고 recomposition 을 해준다고 했다.
한번 이를 어떻게 하는지 깊게 한번 확인해보려 한다.</p>
<h2 id="기본-시나리오">기본 시나리오</h2>
<p><img src="https://images.velog.io/images/ricky_0_k/post/a42bb940-205a-402a-8871-f87083df538d/image.png" alt=""></p>
<p>먼저 최상위에 데이터를 제공해주었을 때의 시나리오이다. (처음 실행할때도 마찬가지이다.)
최상위에 호출된 Composable 함수를 실행하여 최상위 UI 를 그리고, 
그 내부에 선언되어 있는 다른 Composable 함수를 호출하면서 UI 를 그린다.
경우에 따라 계속 데이터를 전달하면서 UI 가 그려질 수 있다.</p>
<p><img src="https://images.velog.io/images/ricky_0_k/post/c4eeb192-d0d6-4f20-9658-a0fb5949f934/image.png" alt=""></p>
<p>위는 Composable 내에서 이벤트가 생겼을 때의 시나리오이다.
변경된 내용에 따라 state 가 연결되어있는 경우, <code>데이터 제공해주었을 때의 시나리오</code>가 동작하기도 한다.</p>
<p>위 내용을 도합하여 정리하면 Compose 의 lifecycle 은 아래와 같이 정리할 수 있다.</p>
<ol>
<li>Composition 시작</li>
<li>최소 0번 이상 재구성</li>
<li>Composition 종료 
<img src="https://images.velog.io/images/ricky_0_k/post/0c1d4a36-25c2-481b-9bd2-1dfeda51b32e/image.png" alt=""></li>
</ol>
<p>Composable 은 여러 번 호출되면 Composition 에 여러 인스턴스가 배치되며
각 호출에는 Composition 에서 고유한 수명주기가 있다.</p>
<pre><code class="language-kotlin">@Composable
fun MyComposable() {
    Column {
        Text(&quot;Hello&quot;)
        Text(&quot;World&quot;)
    }
}</code></pre>
<p>실제 위 코드는 아래와 같은 수명주기를 가진다. 색이 다른 건 다른 인스턴스라는 표시이다.
<img src="https://images.velog.io/images/ricky_0_k/post/f9cf5d9c-f4e9-4214-bf79-a1b0545f0188/image.png" alt=""></p>
<h2 id="call-site">call site</h2>
<ol>
<li><p>Composition 내에서 Composable 인스턴스를 구분하는 <code>식별자</code>이다.
실제 Compose 컴파일러는 각 call site 를 고유한 것으로 간주하고, 
여러번 호출하면 여러 인스턴스가 생성된다.</p>
</li>
<li><p><code>Composable 이 호출되는 소스 코드 위치</code>를 이야기한다.
이는 Composition 하는 위치에도 영향을 미쳐, UI 트리에도 영향을 끼친다.</p>
</li>
</ol>
<p>어찌보면 위 내용이 충돌되어 보이기도 하는데 
실제 recomposition 할 때에는, 어떤 Composable 이 호출되었는지 여부를 식별하고 
입력이 변경되지 않은 경우 그 Composable 은 Recomposition 을 하지 않는다.</p>
<p>아래 코드를 보자</p>
<pre><code class="language-kotlin">@Composable
fun LoginScreen(showError: Boolean) {
    if (showError) {
        LoginError()
    }
    LoginInput() // This call site affects where LoginInput is placed in Composition
}</code></pre>
<p>showError 가 state 변수내 값인 경우, false 라면 
LoginScreen -&gt; LoginInput 이 순차적으로 composition 될 것이다.</p>
<p>그럼 여기서 true 로 바뀌면 어떻게 될까?</p>
<p>recomposition 단계에서 LoginScreen, LoginInput 은 새로 그려지지 않고
LoginError 는 기존에 없었으므로 새로 그려지게 된다.</p>
<p><img src="https://images.velog.io/images/ricky_0_k/post/937e4575-f38c-4bd8-a2df-bd484a2d913c/image.png" alt=""></p>
<p>아래와 같이 같은 call site 에서 여러번 호출할 때에는, call site 와 함께 <code>실행 순서</code>가 사용된다.</p>
<pre><code class="language-kotlin">@Composable
fun MoviesScreen(movies: List&lt;Movie&gt;) {
    Column {
        for (movie in movies) {
            // MovieOverview composables are placed in Composition given its
            // index position in the for loop
            MovieOverview(movie)
        }
    }
}</code></pre>
<p>새로운 movie 가 리스트에 추가된다면, 아래와 같이 
새롭게 추가된 항목에 대해서만 composition 이 발생하고 나머지는 그대로 재사용할 것이다.
<img src="https://images.velog.io/images/ricky_0_k/post/ef2d5f6e-658e-49ec-ab7e-aa43d45c737a/image.png" alt=""></p>
<p>목록의 상단이나 중간에 추가하거나, 항목을 제거 또는 재정렬하여 영화목록이 변경되면 
모든 MovieView 에서 recomposition 이 발생한다.</p>
<pre><code class="language-kotlin">@Composable
fun MovieOverview(movie: Movie) {
    Column {
        // Side effect explained later in the docs. If MovieOverview
        // recomposes, while fetching the image is in progress,
        // it is cancelled and restarted.
        val image = loadNetworkImage(movie.url)
        MovieHeader(image)

        /* ... */
    }
}</code></pre>
<p>만약 이런 코드에서 새로운 값이 추가되거나 맨 위에 값이 추가되면 아래와 같이 전부 새로 그려진다.
<img src="https://images.velog.io/images/ricky_0_k/post/27ab35e4-df8e-487c-a399-de183bf6f06a/image.png" alt=""></p>
<p>그러면 이런 경우를 없애려면 어떻게 해야할까? key 를 활용해 unique 값을 직접 명세할 수 있다.
key의 값은 global 에 유니크한 값일 필요는 없다. call site에서 Composable 호출시에만 유니크하면 된다. </p>
<pre><code>@Composable
fun MoviesScreen(movies: List&lt;Movie&gt;) {
    Column {
        for (movie in movies) {
            key(movie.id) { // Unique ID for this movie
                MovieOverview(movie)
            }
        }
    }
}</code></pre><p>위 예제처럼 매 movie 는 movies 사이에서 유니크한 key가 필요하다. 
key 를 앱의 다른 위치에 있는 다른 Composable 과 공유해도 괜찮다.
이렇게 되면 리스트에서 요소가 변경 시 Compose는 알아채고 재사용이 가능해진다.</p>
<p><img src="https://images.velog.io/images/ricky_0_k/post/436b487c-4971-4caa-b519-293e9b28eda8/image.png" alt=""></p>
<p>참고) 몇몇 Composable은 key composable을 내부에 지원하고 있다. 
ex. LazyColumn : items DSL 내부에 특정 custom key를 받을 수 있다. </p>
<pre><code>@Composable
fun MoviesScreen(movies: List&lt;Movie&gt;) {
    LazyColumn {
        items(movies, key = { movie -&gt; movie.id }) { movie -&gt;
            MovieOverview(movie)
        }
    }
}</code></pre><p>위 코드가 그 예제이다.</p>
<h2 id="recomposition-skip">Recomposition skip</h2>
<p>Composable 이 Composition 에 이미 있고 모든 입력이 안정적이면서 변하지 않았다면 
RecomPosition 을 건너뛸 수 있다.</p>
<p>안정적인 타입은 아래 내용을 준수해야 한다.</p>
<ol>
<li>두 인스턴스의 equals 결과가 동일한 두 인스턴스는 항상 동일하다</li>
<li>타입의 public 속성이 변경되었을 시 Composition 에 알린다.</li>
<li>모든 public 속성 타입 또한 안정적이다.</li>
</ol>
<p>Compose 컴파일러가 안정적인 것으로 취급하는 (위 내용을 준수하는) 몇가지 중요한 공통 타입이 있다.</p>
<ol>
<li>모든 주요 value 타입 : Boolean, Int, Long, Float, Char 등...</li>
<li>문자열(Strings)</li>
<li>모든 함수 타입(람다)</li>
</ol>
<p>변경할 수 없는 타입은 절대 변경할 수 없으므로 
Composition 에 변경사항을 알리지 않아도 되므로 준수하기가 더 쉽다.</p>
<p><strong>MutableState</strong>
만약 값이 MutableState 에 유지되고 있다면, 
Compose가 State의 <code>.value</code> 속성의 변경사항에 대해 알림을 받을 것이기 때문에 
State Object는 전반적으로 안정적이라고 간주된다.</p>
<p>Composable에 파라메터로 전달되는 모든 타입이 안정적일 때, 
파라메터 값은 UI 트리의 Composable 위치에 기반하여 동일한 값인지 비교된다. 
모든 값이 이전 호출에서의 값과 변경된 것이 없다면 Recomposition 은 skip 된다.
값 비교는 equals() 를 사용한다.</p>
<p>interface 는 비안정적이라고 간주한다. 
변경할 수 있는 public 속성을 가지고 있고 구현 변경이 안되므로 안정적이지 않다.</p>
<h3 id="stable">@Stable</h3>
<p>Compose가 해당 타입을 안정적으로 처리하도록 하려면 <code>@Stable</code> 어노테이션으로 표시한다.</p>
<pre><code class="language-kotlin">// Marking the type as stable to favor skipping and smart recompositions.
@Stable
interface UiState&lt;T : Result&lt;T&gt;&gt; {
    val value: T?
    val exception: Throwable?

    val hasError: Boolean
        get() = exception != null
}</code></pre>
<ol>
<li>UiState는 interface이기 때문에 Compose는 원칙적으로는 안정적이지 않은 타입</li>
<li>하지만 @Stable 어노테이션을 추가함으로써 Compose 에 이 타입이 안정적이라는 것을 알려줌</li>
<li>Compose 는 smart recomposition 을 선호하게 됨</li>
</ol>
<p>-&gt; interface 가 파라메터 타입으로 사용될 시, Compose는 이 interface 의 모든 구현을 안정적으로 간주하고 처리</p>
<h1 id="결론">결론</h1>
<p>State 를 정리하면 Composable UI 가 가지고 있는 상태값이고,
Lifecycle 은 Composition, Recomposition, exit 로 정리될 수 있다.</p>
<p>다시 돌아보니 일부 내용을 놓치고 (ex. keys) 개발했던 내용도 있었던 것 같다. 
이는 리펙터링 대상으로 자연스럽게 넣어 놓아야겠다.</p>
<p>State 와 Compose Lifecycle 을 다뤄보았으니 이제 다음 Step 인 
메인 화면 (학점을 아름답게 볼 수 있는 화면) 을 다뤄보려 한다.</p>
<p>여기에서는 xml 내에 Compose 를 넣어서 처리하는 방식을 사용해봤었는데
호환성에 대해서도 이야기가 나올 것 같다.</p>
<p>참고</p>
<ol>
<li><a href="https://developer.android.com/jetpack/compose/state">State and Jetpack Compose</a></li>
<li><a href="https://developer.android.com/jetpack/compose/mental-model">Thinking in Compose</a></li>
<li><a href="https://developer.android.com/jetpack/compose/lifecycle">Lifecycle of composables</a></li>
<li><a href="https://growup-lee.tistory.com/entry/Android-Compose-Lifecycle">[Android] Compose Lifecycle(안드로이드 개발자 사이트 번역)</a></li>
<li><a href="https://tourspace.tistory.com/410">4. 상태관리 - hoisting, mutableState, remember, rememberSaveable, Parcelize, MapSaver, ListSaver</a></li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[Compose 무작정 맛보기 [1. 초기 설정 및 쉬운 화면 작업]]]></title>
            <link>https://velog.io/@ricky_0_k/Compose-%EB%AC%B4%EC%9E%91%EC%A0%95-%EB%A7%9B%EB%B3%B4%EA%B8%B0-1.%EA%B8%B0%EB%B3%B8%EC%A0%90%EC%88%98-%EC%9E%85%EB%A0%A5-%EC%A1%B8%EC%97%85%ED%95%99%EC%A0%90-%EC%9E%85%EB%A0%A5</link>
            <guid>https://velog.io/@ricky_0_k/Compose-%EB%AC%B4%EC%9E%91%EC%A0%95-%EB%A7%9B%EB%B3%B4%EA%B8%B0-1.%EA%B8%B0%EB%B3%B8%EC%A0%90%EC%88%98-%EC%9E%85%EB%A0%A5-%EC%A1%B8%EC%97%85%ED%95%99%EC%A0%90-%EC%9E%85%EB%A0%A5</guid>
            <pubDate>Mon, 31 Jan 2022 17:05:00 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p>일전에 런칭하기 전에 내용들을 올리겠다고 했었는데 
포스팅을 올려놓고 생각해보니 아닌 것 같아 <code>런칭하고 포스팅</code>을 하기로 하였다. </p>
<h1 id="앱-소개-및-개인-회고">앱 소개 및 개인 회고(?)</h1>
<p>서론의 말인 즉슨, 앱 재런칭을 완료했다.
내가 리펙터링하여 재런칭한 앱은 <a href="https://play.google.com/store/apps/details?id=recipe.yapp.kr.graderecipe">학점 레시피</a> 라는 아름답게 내 학점을 보여주는 어플이다.</p>
<p><img src="https://images.velog.io/images/ricky_0_k/post/e4f37743-1052-44de-bbde-f24ae24c61af/image.png" alt=""></p>
<center><font color=#999999><b>2018년의 나</b>는 되게 닭살 돋는 말들을 많이 적어놓았다....</font></center>


<p>리펙터링하면서 개선하고 싶은 게 계속 나오면서 조금 지지부진해지고 있었는데
1월 말에 <strong>코로나</strong>에 걸리면서 (...) 중간 터닝 포인트를 찍지 않으면 안되겠다는 생각이 들었다.
그래서 일부 개선 작업은 뒤로 미루고 런칭부터 완료했다.</p>
<p>내가 세웠던 개발 목표를 기준으로 평가 및 회고(?) 해보면 아래와 같다.</p>
<p><strong>목표 1. 다각도로 Compose 사용해보기 ⭕</strong>
이 목표는 일단 성공했다. 참고로 이 프로젝트는 100% Compose 는 아니다.
다양한 시나리오 경험을 위해, 처음부터 100% Compose 생각은 없었다.
일부 화면(크롭화면, 통계 Fragment, Main 화면 전체 틀) 은 
기존 View 와 혼합해 사용하는 시나리오 경험을 위해 일부만 Compose 로 전환했고 
그 이외에는 전부 Compose 를 활용했어서 이에 대해 아쉬움은 없다.</p>
<p><strong>목표 2. 개인적으로 공부한 클린아키텍처 적용 ⭕</strong>
멀티 모듈을 적용하는 클린 아키텍처를 적용해보았다.
<code>data</code>, <code>presentation</code>, <code>domain</code>, <code>app</code> 으로 구성했으며 
로컬 DB (Room) 만 사용하기 때문에 그렇게 복잡하지는 않았다.</p>
<p><strong>목표 3. DI, 테스트 코드 적용 ❌</strong>
DI 와 테스트 코드는 적용하지 못했다. 
Compose 와 연계한 DI 연동 및 테스트 환경 구성을 생각했었는데 이는 아쉬움이 남는다.</p>
<h1 id="초기설정">초기설정</h1>
<p>2018년에 코틀린과 친해진다고 이것저것 실험했던 것들이 많았고
일정도 조금 급했어서 빠르게 런칭하고 간간히 유지보수만 했던 앱이었다보니 코드가 난장판이었다.</p>
<p>일련의 예로 <code>의존성 관리</code>를 이야기해보면..</p>
<p>gradle 이나 라이브러리들도 옛날 것이었고 사용하지 않는 라이브러리도 있어 
의존성 삭제, 버전 업데이트 등 정리가 필요했다.</p>
<p><img src="https://images.velog.io/images/ricky_0_k/post/778c7746-2a94-4a8f-a37f-0fc61f160cc8/image.png" alt=""></p>
<p>왼쪽은 과거이고, 오른쪽 것이 현재(최종)이다. 이런 식으로 gradle 부터 차근차근 정리해나갔다.</p>
<p>이후 코드 수정 작업도 진행했다.
Compose 를 위해 MVVM 을 써야 했기에 이에 대한 초기 base 작업도 newBase 로 따로 두어 정리했다.
<img src="https://images.velog.io/images/ricky_0_k/post/8892dad6-73d4-4b1a-b5c0-3247c690b614/image.png" alt=""></p>
<p>이런 식으로 기존 base 를 그대로 두고 newBase 를 따로 만들어 작업을 했다.</p>
<p>저 구조가 최종은 아니다. 지금은 저 패키지 구조에서도 변경 및 데이터를 추가한 상태이고
저 사진의 newBase 자체도, 지금은 이름을 <code>base</code> 로 바꾸고 <code>presentation 모듈</code>로 옮긴 상태이다.</p>
<h1 id="compose-적용-및-basecomposeactivity-구축">Compose 적용 및 BaseComposeActivity 구축</h1>
<p>이윽고 Compose 라이브러리 적용을 위한 초기 작업을 시작했다.</p>
<p><img src="https://images.velog.io/images/ricky_0_k/post/7618b6c9-82e5-432c-86d3-bf3283691d7a/image.png" alt=""></p>
<p>먼저 Compose 관련 gradle 설정을 해주었는데 의외로 간단했다. 
buildFeatures 코드 수정, 일부 설정 추가, 라이브러리만 일부 추가하여 설정을 완료했다.</p>
<p>중간에 kotlin 도 1.6.0 을 못쓴다고 해서 다시 버전을 낮추기도 했다.
Compose 의존성 라이브러리의 경우 <a href="https://developer.android.com/codelabs/jetpack-compose-migration?hl=en#3">Compose Migration 코드랩</a> 내용을 보고 가져왔다.</p>
<p>이윽고 Compose 를 사용하는 BaseComposeActivity 도 따로 만들게 되었다.
나머지 코드는 제외하고 핵심 코드만 언급해보려 한다.</p>
<pre><code class="language-kotlin">abstract class BaseComposeActivity : AppCompatActivity() {
    abstract var composable: @Composable () -&gt; Unit

    abstract val viewModel: BaseViewModel?

    // ...


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent { composable() }
        subscribeUI()
    }

    // ...
}</code></pre>
<p>Compose 관련하여 이런 식으로 구현하였다. 
<code>subscribeUI()</code> 는 ViewModel observe 로직을 모아둔 함수이다.</p>
<p>BaseComposeActivity 를 상속 받는 함수는 아래와 같이 사용하게 만들었다.</p>
<pre><code class="language-kotlin">class ComposeActivity : BaseComposeActivity() {
    override var composable: @Composable () -&gt; Unit = { ComposeView(viewModel) 

    override val viewModel: ComposeViewModel by lazy {
        ViewModelProvider(this)[ComposeViewModel::class.java]
    }

    // ...
}
</code></pre>
<p><code>ComposeViewModel</code> 은 <code>MainViewModel</code> 을 상속받는 클래스이며, 
MVVM 의 ViewModel 로 생각하면 된다.</p>
<h1 id="initactivity-리펙터링">InitActivity 리펙터링</h1>
<p>먼저 이 화면을 리펙터링했다. </p>
<img src="https://images.velog.io/images/ricky_0_k/post/e8003a16-468c-4fce-b690-e5fa9d0fcbcc/image.png" width="300">

<p>비교적 간단한 화면이었고 이 Activity 가 스플래시 역할도 같이하고 있어 그걸 분리 작업도 같이 했다.</p>
<h2 id="1-스플래시-화면-코드-작성">1. 스플래시 화면 코드 작성</h2>
<p>먼저 Compose 연습 겸 스플래시를 분리하여 코드를 작성하였다.
여기서는 비교적 코드가 짧아 전체 코드를 언급하려 한다.</p>
<h3 id="launchactivitykt">LaunchActivity.kt</h3>
<p>아래는 최종 코드이며 패키지 및 기능 개선으로 몇 번의 commit 을 거쳐 만들어졌다.</p>
<pre><code class="language-kotlin">class LaunchActivity : BaseComposeActivity() {
    override var composable: @Composable () -&gt; Unit = { LaunchView(viewModel) }

    override val viewModel: LaunchViewModel by lazy {
        ViewModelProvider(this, viewModelFactory)[LaunchViewModel::class.java]
    }

    /** [UserRepository] 를 ViewModel 에 넘겨주어야 하여 만들게 된 [ViewModelProvider.Factory] */
    private val viewModelFactory: LaunchViewModelFactory by lazy {
        LaunchViewModelFactory(Modules.userRepository)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        lifecycleScope.launch {
            delay(3000)
            viewModel.checkValueSaved()  // 1
        }

        // 3
        viewModel.activityAction.removeObservers(this)
        viewModel.activityAction.observe(this, {
            val intent = Intent(this, it.activityClass)
            intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
            intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
            intent.putExtra(Const.EXTRA_FROM_LAUNCH, true)
            startActivity(intent)
            finish()
        })
    }
    // ...
}

/** 스플래시 화면 ViewModelFactory */
class LaunchViewModelFactory(
    private val userRepository: UserRepository
) : ViewModelProvider.Factory {
    override fun &lt;T : ViewModel&gt; create(modelClass: Class&lt;T&gt;): T {
        return if (modelClass.isAssignableFrom(LaunchViewModel::class.java)) {
            LaunchViewModel(userRepository) as T
        } else {
            throw IllegalArgumentException()
        }
    }
}

// ...

// 2
sealed class ActivityAction(val activityClass: Class&lt;out AppCompatActivity&gt;?) {
    /** 백 버튼 액션 */
    object BackPress : ActivityAction(null)

    /** 현재 화면 종료 액션 */
    object Finish : ActivityAction(null)

    /** 액티비티 이동 액션 */
    class GoActivity(activityClass: Class&lt;out AppCompatActivity&gt;?) :
        ActivityAction(activityClass = activityClass)

    /** 액티비티 이동 후 이전 화면 종료 액션 */
    class GoActivityAndFinish(activityClass: Class&lt;out AppCompatActivity&gt;?) :
        ActivityAction(activityClass = activityClass)
}</code></pre>
<ol>
<li><p>기본 점수를 설정했는지를 ViewModel 에서 확인(<code>viewModel.checkValueSaved()</code>) 하여 
설정 했을 경우 2번 로직의 <code>observe { ... }</code> 코드가 실행된다.</p>
</li>
<li><p>Class 타입 변수를 가지는 ActivityAction sealed class 를 두어
간단한 액션만 있을 경우 BaseComposeActivity 의 <code>observe { ... }</code> 를 따르게 두었다.</p>
</li>
<li><p>이 화면에서는 flag 를 걸어주어야 하는 액션이 있어 기존 observe 를 무효화한 후 새로 등록하였다.</p>
</li>
</ol>
<h3 id="launchviewkt">LaunchView.kt</h3>
<p>이제 Compose 로 만든 View 를 확인해보려 한다. 
스플래시만 있어 비교적 간단한 작업이었다.</p>
<pre><code class="language-kotlin">package ...

import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import recipe.yapp.kr.graderecipe.presentation.R
import kotlin.math.*

@Composable
fun LaunchView(vm: LaunchViewModel? = null) {
    Column(
        modifier = Modifier    // 1
            .fillMaxSize()
            .gradientBackground(
                listOf(
                    // 2
                    colorResource(id = R.color.launch_gradient_start_color),
                    colorResource(id = R.color.launch_gradient_end_color)
                ), 180f
            )
            .padding(20.dp),
        horizontalAlignment = Alignment.CenterHorizontally,    // 3
        verticalArrangement = Arrangement.Center               // 4
    ) {
        // 5
        Column(
            modifier = Modifier
                .fillMaxSize()
                .border(0.4.dp, colorResource(id = R.color.white)),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center
        ) {
            Image(
                painter = painterResource(R.drawable.splash_image),
                contentDescription = stringResource(id = R.string.iv_launch_image_content_description),
            )

            Spacer(modifier = Modifier.height(40.dp))

            Text(
                text = stringResource(id = R.string.tv_launch_title),
                color = colorResource(id = R.color.white),
                letterSpacing = 0.16.sp,
                fontSize = 12.7.sp
            )

            Spacer(modifier = Modifier.height(5.dp))

            Text(
                text = stringResource(id = R.string.tv_launch_sub_title),
                color = colorResource(id = R.color.white),
                fontSize = 25.3.sp
            )
        }
    }
}

// 6
@Preview
@Composable
fun LaunchViewPreview() {
    LaunchView()
}</code></pre>
<ol>
<li><p><code>Modifier</code> 라는 것을 활용해서 xml 에서 쓰던 왠만한 기능들을 적용할 수 있었다.
<code>background</code>, <code>padding</code>, <code>border</code>, <code>WRAP_CONTENT</code>, <code>MATCH_PARENT</code> 등의 설정 등을 할 수 있었다.
<code>gradientBackground</code> 은 내가 임의로 만든 Modifier 기반 커스텀 확장함수이다.
그라데이션 배경 적용이 필요하여 만들어 작업하였다.</p>
</li>
<li><p>계속 보이겠지만 <code>color</code>, <code>drawable</code>, <code>string</code> 등 <code>xml resource</code> 를 이런 패턴으로 가져올 수 있다.</p>
</li>
<li><p><code>horizontalAlignment</code> : 자식 레이아웃의 수평 gravity 설정</p>
</li>
<li><p><code>verticalArrangement</code> : 자식 레이아웃의 수직 gravity 설정</p>
</li>
<li><p><strong>orientation</strong> 이 <code>vertical 인 LinearLayout</code> 을 생각하면 된다. 
(Flutter 의 <code>Column Widget</code> 과 똑같다.)
이 중괄호 안에 언급된 Composable 들이 <code>수직 정렬</code>된다.</p>
</li>
<li><p>Compose 에서는 이 <code>PreView</code> 어노테이션을 가진 Composable 를 따로 선언해주어야 
xml 에서 흔히 볼 수 있었던 <code>preview 형태</code>를 볼 수 있다.
<img src="https://images.velog.io/images/ricky_0_k/post/0d4db627-9c63-481e-a76a-fe8938e7dcb5/image.png" alt=""></p>
<p> 왼쪽은 <code>___View.kt</code> 파일에서 PreView 가 없을 때 나오는 화면이며
 6번과 같이 PreView 를 구현해놓은 경우 우측과 같은 화면을 볼 수 있다.</p>
</li>
</ol>
<h2 id="2-initactivity-리펙터링">2. InitActivity 리펙터링</h2>
<p>스플래시를 제외한 화면에서 리펙터링을 진행했다.</p>
<h3 id="initactivitykt">InitActivity.kt</h3>
<p>대략적인 구조는 위의 <code>LaunchActivity.kt</code> 와 비슷하다.</p>
<h3 id="initviewkt">InitView.kt</h3>
<p>여기에서는 <code>ConstraintLayout</code> 와 비슷한 기능 활용이 필요하여 아래 라이브러리를 추가하였다.</p>
<pre><code class="language-kotlin">implementation &quot;androidx.constraintlayout:constraintlayout-compose:1.0.0-rc02&quot;</code></pre>
<p>전체 코드는 아래와 같다.</p>
<pre><code class="language-kotlin">// 1
private const val ll_init_bottom = &quot;ll_init_bottom&quot;
private const val ll_init_top = &quot;ll_init_top&quot;

private const val tv_init_bottom_description = &quot;tv_init_bottom_description&quot;

/** 기본 점수 설정 화면 View */
@Composable
fun InitView(vm: InitViewModel? = null) {
    BoxWithConstraints() {
        // 2
        val constraints = getInitConstraints()

        ConstraintLayout(constraints) {
            InitTopView(vm)
            InitBottomView(vm)
        }
    }
}

// 2
/** 기본 점수 설정 화면 View 의 ConstraintSet */
private fun getInitConstraints(): ConstraintSet {
    return ConstraintSet {
        val topLayout = createRefFor(ll_init_top)
        val bottomLayout = createRefFor(ll_init_bottom)
        constrain(bottomLayout) {
            bottom.linkTo(parent.bottom)
        }
        constrain(topLayout) { top.linkTo(parent.top) }
    }
}

// 3
/** 기본 점수 설정 화면 상단 View */
@Composable
fun InitTopView(vm: InitViewModel? = null) {
    // val scoreInfo = vm?.score?.observeAsState()
    // 6
    val enablePosition = vm?.enablePosition?.observeAsState()

    Column(
        modifier = Modifier
            .layoutId(ll_init_top)
            .fillMaxSize()
            .background(color = colorResource(id = R.color.white)),
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Toolbar(
            { vm?.let { BackButton(it) } },
            R.string.tv_init_toolbar_title
        )
        Spacer(modifier = Modifier.height(80.dp))
        Text(
            text = stringResource(id = R.string.tv_init_title),
            fontFamily = nanumSquareFamily,
            color = colorResource(id = R.color.inputTextColor),
            fontWeight = FontWeight.Bold,
            fontSize = 26.sp,
        )
        Spacer(modifier = Modifier.height(26.dp))
        Text(
            textAlign = TextAlign.Center,
            text = stringResource(id = R.string.tv_init_description),
            color = colorResource(id = R.color.grayTextColor),
            fontWeight = FontWeight.Normal,
            fontSize = 15.sp,
        )
        Spacer(modifier = Modifier.height(45.dp))
        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.Center
        ) {
            GetScoreButton(
                vm, ScoreInfo.SCORE_4_3, enablePosition?.value == 0
            )

            Spacer(modifier = Modifier.width(24.dp))

            GetScoreButton(
                vm, ScoreInfo.SCORE_4_5, enablePosition?.value == 1
            )
        }
    }
}

// 7
/** 기본 점수 설정 화면 하단 View */
@Composable
fun InitBottomView(vm: InitViewModel? = null) {
    Column(
        modifier = Modifier.layoutId(ll_init_bottom),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(
            modifier = Modifier
                .layoutId(tv_init_bottom_description),
            text = stringResource(id = R.string.tv_init_bottom_description),
            color = colorResource(id = R.color.grayTextColor),
            textAlign = TextAlign.Center
        )
        Spacer(modifier = Modifier.height(48.dp))
        TextButton(
            modifier = Modifier
                .fillMaxWidth()
                .height(48.dp),
            onClick = { vm?.clickNextButton() },
            colors = ButtonDefaults.buttonColors(
                backgroundColor = colorResource(id = R.color.colorTheme)
            )
        ) {
            Text(
                text = stringResource(id = R.string.btn_init_next),
                color = colorResource(id = R.color.white),
                fontSize = 12.sp,
            )
        }
    }
}

// 4
/** 기본 점수 설정 화면 버튼 아이템 View */
@Composable
fun GetScoreButton(vm: InitViewModel?, scoreInfo: ScoreInfo, enabled: Boolean) = Column(
    modifier = Modifier
        .width(144.dp)
        .height(144.dp)
        .background(
            colorResource(
                id = if (enabled) R.color.colorTheme
                else R.color.grayBrightColor
            ),
            CircleShape
        )
        // 5
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) { vm?.clickScoreButton(scoreInfo) },
    horizontalAlignment = Alignment.CenterHorizontally,
    verticalArrangement = Arrangement.Center
) {
    Text(
        text = &quot;${scoreInfo.score}&quot;,
        color = colorResource(
            id = if (enabled) R.color.white
            else R.color.grayTextColor
        ),
        fontWeight = FontWeight.Normal,
        fontFamily = bebasneueFamily,
        fontSize = 54.4.sp,
    )

    Spacer(modifier = Modifier.height(10.dp))

    Text(
        text = stringResource(id = R.string.tv_init_score_item_sub_title),
        color = colorResource(
            id = if (enabled) R.color.white
            else R.color.grayTextColor
        ),
        fontWeight = FontWeight.Normal,
        fontFamily = bebasneueFamily,
        fontSize = 16.9.sp,
    )
}

@Preview
@Composable
fun InitViewPreview() {
    InitView()
}</code></pre>
<ol>
<li><p>레이아웃 id 를 한 곳에 모아 전역 상수 형태로 두었다.
레이아웃 id 설정 역시 <code>Modifier.layoutId(___)</code> 형태로 사용이 가능하다.</p>
</li>
<li><p>InitTopView(<code>ll_init_top</code>), InitBottomView(<code>ll_init_bottom</code>) 을 
ConstraintSet 을 통해 연결하고, ConstraintLayout 내에 각 Composable 들을 선언하였다.
기존 kt 코드에서 ConstraintSet 을 설정하던 것과 비슷했던 것 같고, id 를 generate 해주는 작업은 없어서 편했던 것 같다.
id 위치를 이상하게 두면 어떻게 될지 (크래시가 날지, 이상하게 보여질지 등) 는 해보지 않았다.</p>
<img src="https://images.velog.io/images/ricky_0_k/post/03a2f47a-1da5-46c0-b08a-7787b946ba26/image.png" width="150">

<p>참고로 <code>InitTopView</code> 는 위 사진에서 빨간색, <code>InitBottomView</code> 는 위 사진에서 녹색이다.</p>
</li>
<li><p>상단 View 에 대한 코드이다
<code>BackButton</code>(백버튼) 과 <code>ToolBar</code>(상단 ActionBar) 의 경우 구글링을 통해 커스텀하여 만들었다. 
4.3, 4.5 동그라미 버튼이 동일한 기능이어서 <code>GetScoreButton</code> 으로 커스텀하였고 
이는 4번 로직을 보면 확인할 수 있다.</p>
</li>
<li><p>위에서 언급했다시피 동그라미 모양의 선택 버튼 Composable 이다.</p>
<p>background 의 경우 두번째 인자로 모양을 받는다. 
그래서 예전과 다르게 모양 설정이 매우 쉽다.</p>
<pre><code class="language-kotlin">fun Modifier.background(
   color: Color,
   shape: Shape = RectangleShape
)</code></pre>
<p>위와 같이 구현되어 있으며 이는 compose 의존성(이하 라이브러리)에서 지원해주는 함수이다.</p>
</li>
<li><p>clickable 설정 코드이며 ripple 등도 설정이 가능하다 (여기선 ripple 을 없앴다.)
ripple 과 더불어 <code>remember</code> 이라는 것도 있는데 이는 상태와 관련하여 중요한 내용이다.
여기서는 state 를 크게 다루는 내용이 없고 이 내용으로 한 포스트 분량이 나오기 때문에 
다음 포스트에서 설명 에정이다.</p>
</li>
<li><p>이 코드는 ViewModel 의 LiveData 를 state 형태로 변환하여 저장하고 있으면서
LiveData 내 값이 변할 시 인지하고 바뀐 상태 값을 기반으로 
<code>Composable 를 새로 그려주는 작업</code> (이하 ReComposition) 을 할 수 있게 된다.</p>
<p><code>vm?.enablePosition</code> 은 각 4.3, 4.5 동그라미 버튼 중 
어떤 걸 선택했는지를 나타내는 LiveData 이며 클릭 시 버튼색을 바꿔주어야 하기에
state 형태로 변환하여 ReComposition 을 할 수 있도록 하였다.</p>
<p>ReComposition 은 Compose LifeCycle 와도 관련이 있는데
이 내용도 State 와 연결하여 다음 포스트에서 다루려 한다.</p>
</li>
<li><p>하단 View 에 대한 코드이다. 
별다른 큰 특징은 없고 TextButton 에 onClick 을 통해 클릭 이벤트를 넣었다.</p>
</li>
</ol>
<p>난 여기까지 해서 InitActivity 의 Compose 변환 작업을 끝냈다.</p>
<h1 id="graduationactivity-리펙터링">GraduationActivity 리펙터링</h1>
<p>다음으로 이 화면을 리펙터링했다. </p>
<img src="https://images.velog.io/images/ricky_0_k/post/f1396fb0-ab0e-4b81-bcc2-f6930f26abab/image.png" width="300">

<p>일부 텍스트 내용이 바뀌고, 입력 텍스트가 생긴 것 이외에는 별다른 작업이 없었다.</p>
<h3 id="graduationactivitykt">GraduationActivity.kt</h3>
<p>대략적인 구조와 로직이 위의 <code>InitActivity.kt</code> 와 흡사하다.</p>
<h3 id="graduationviewkt">GraduationView.kt</h3>
<p>위에서 말했다시피 대략적인 구조는 <code>InitView.kt</code> 와 비슷했다.
<code>TextField</code> 부분만 언급하려 한다.</p>
<pre><code class="language-kotlin">
// 4
val graduationInput = vm?.graduationInput?.observeAsState()
    ?: remember { mutableStateOf(&quot;&quot;) }

...

Row(
    horizontalArrangement = Arrangement.Center,
    verticalAlignment = Alignment.CenterVertically
) {
    // 1
    Column(modifier = Modifier.width(112.dp)) {
        TextField(
            // 2
            label = null,
            // 3
            colors = TextFieldDefaults.textFieldColors(
                backgroundColor = colorResource(id = R.color.transparent),
                textColor = colorResource(id = R.color.themeTextColor),
                focusedIndicatorColor = colorResource(id = R.color.transparent),
                unfocusedIndicatorColor = colorResource(id = R.color.transparent),
                cursorColor = colorResource(id = R.color.themeTextColor),
            ),
            // 4
            value = graduationInput.value!!,
            textStyle = TextStyle(fontSize = 42.7.sp),
            // 5
            placeholder = {
                Text(
                    text = &quot;140&quot;,
                    fontSize = 42.7.sp,
                    color = colorResource(id = R.color.inputHintColor)
                )
            },
            // 6
            onValueChange = {
                if (it.length in 0..3) vm?.inputText(it)
            },
        )
        Spacer(
            modifier = Modifier
                .fillMaxWidth()
                .height(1.dp)
                .background(color = colorResource(id = R.color.inputHintColor))
        )
    }
    Text(
        modifier = Modifier.padding(top = 6.dp),
        text = stringResource(id = R.string.tv_graduation_input_suffix),
        fontSize = 28.7.sp,
    )
}</code></pre>
<ol>
<li><p>해당 입력 뷰는 먼저 <code>Row</code> 을 활용해 <code>140+밑줄</code>, <code>점</code> 을 <strong>수평 정렬</strong> 한다.
<code>140+밑줄</code>은 <code>Column</code> 을 활용해 <strong>수직 정렬</strong> 한다.</p>
</li>
<li><p>사진을 통해 이야기하는 게 좋을 것 같다. 난 label 이 필요 없어 null 로 두었다.
<img src="https://images.velog.io/images/ricky_0_k/post/0f8052a7-f020-4149-8a53-18268b3ba452/image.png" alt=""></p>
</li>
<li><p>colors 를 통해 다양한 시나리오의 색을 설정할 수 있었다.
<code>배경색</code>, <code>텍스트색</code>, <code>커서색</code> 등을 설정할 수 있었다.</p>
</li>
<li><p>이 역시 입력된 텍스트 값을 LiveData 로 기억하고 있어서 이렇게 처리했었다.
이 코드에서 <code>remember { mutableStateOf(&quot;&quot;) }</code> 가 활용될 일은 없다. 
vm 이 null 일 때, PreView 에서 일부 Composable 만 그려지는 걸 피하기 위해 추가한 코드이다.</p>
</li>
<li><p>기존 xml 에서 hint 를 생각하면 된다. 
Compose 에서는 hint 텍스트 하나의 Composable 로 처리한다.</p>
</li>
<li><p>외부 입력 (키보드 등) 으로 텍스트가 바뀔 때의 동작을 명세하는 곳이다.
여기에서는 입력된 텍스트 값(<code>vm?.graduationInput</code>)을 
갱신해주는 작업 (<code>vm?.inputText()</code>) 을 실행한다.</p>
<p>4번에서 보다시피 <code>vm?.graduationInput</code> 이 state 형태로 변환되어 있으므로
<code>vm?.inputText()</code> 로 인해 값의 상태가 바뀐 것을 인지하고 Composable 를 새로 그려주게 된다.
새로 그려준다는 말이 거창해보이지만 여기서는 텍스트 값만 새로 그려준다.</p>
</li>
</ol>
<h1 id="마무리">마무리</h1>
<p>여기까지 하여 초기 설정을 완료하고, 2개의 화면에 Compose 를 적용하고 리펙터링을 할 수 있었다.</p>
<p>여기까지 작업하면서 Compose 에 대해 느꼈던 점은 
<code>정말 Flutter 같다</code>는 것과, 기존에 라이브러리를 활용하거나 커스텀 함수를 만들며 힘들게 한 것들을 <code>쉽게 작업한다</code> 는 느낌이 들었다. 그것도 <code>Kotlin 언어</code>를 사용해서 말이다.
신비하면서도 Compose 의 장점을 확실히 느낄 수 있었다.</p>
<p>하지만 Compose 도 러닝 커브가 없는 건 아니었다. 
<code>TextField</code> 에서는 Hint 의 차이점에 대해서 쉽게 이야기했지만, 개발 처음에는 xml 과 상이하다보니 헤멘 영역이었다. 심지어 <code>TextField</code> 는 일부 한계점이 있는데, 이는 학점 입력 화면(InputActivity)에서 다뤄볼 예정이다.
그리고 State 또한 한 없이 어렵게 들어가면 정말 어려워지는 영역이었다. 
이에 대해서도 뒤 리펙터링 포스트에서 차차 다뤄볼 예정이다.</p>
<p>본래 MainActivity 리펙터링을 바로 이야기하려 했으나 
일전에 State 와 Composable 의 LifeCycle 이야기가 일부 나왔어서 
그 내용을 먼저 정리하여 포스팅하고, 리펙터링에 대한 이야기를 다시 진행해보려 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Compose 무작정 맛보기 [0.시작]]]></title>
            <link>https://velog.io/@ricky_0_k/Compose-%EB%AC%B4%EC%9E%91%EC%A0%95-%EB%A7%9B%EB%B3%B4%EA%B8%B0-0.%EC%8B%9C%EC%9E%91</link>
            <guid>https://velog.io/@ricky_0_k/Compose-%EB%AC%B4%EC%9E%91%EC%A0%95-%EB%A7%9B%EB%B3%B4%EA%B8%B0-0.%EC%8B%9C%EC%9E%91</guid>
            <pubDate>Wed, 12 Jan 2022 17:23:14 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p>작년부터 조금조금 하던 <code>Compose 적응 내역</code>들을 차근차근 포스팅해보려 한다.
사실 런칭 신청까지 완료하고 포스팅을 하려고 했으나, 작업하면서 좌충우돌이 너무 컸다.</p>
<p>메인 기능들을 다루기 시작하면서 작업시간이 오래 걸렸고 그만큼 포스트에 많은 글이 써질 것 같았다. 
<del>(내가)</del> 스압을 싫어하므로 시리즈 포스트 형태로 작성해보기로 마음 먹었다.
<img src="https://ww.namu.la/s/9d8dced730918edf1fcb025deeba5de017c528e940486c3e1e1be5432195c0face50e285f89a5be671288f24ea3bcf014fa48c9f7d720d7ddc579f16f2eb0cb89db24064393e543d7e7bb4a2da016c3ab7e58a0a12f9fe7d085c38fda8544873" alt=""></p>
<h1 id="compose-맛보기">Compose 맛보기</h1>
<p>xml 로만 Android 화면을 작성해왔던 나에게, Compose 는 처음보는 무서운 녀석의 느낌이었다.
하지만 Kotlin 으로 작성하는 UI 이기 때문에 무섭지만은 않겠지하고 공부방법을 생각해보게 되었다.
<img src="https://images.velog.io/images/ricky_0_k/post/16533d01-44fc-44f6-a8a7-6ba4a2aff295/image.png" alt=""></p>
<div style="text-align: center;"><span style="color:#999999;">하다보면 이렇게 되겠지 ㅇㅅㅇa</span></div>


<h2 id="공부방법-선택하기">공부방법 선택하기</h2>
<p>필자는 과거에 새로운 언어 혹은 개념을 공부하기 위해 아래 두가지 방법을 사용했었다.</p>
<ol>
<li><code>무작정 경험</code>을 해보는 방식</li>
<li><code>코드랩을 통해 차근차근 접근</code>하는 방식 </li>
</ol>
<p>두 가지 방법을 효과를 보았기에 어떤 방식으로 할지 고민이 있었고 고민 끝에 <code>무작정 경험</code>을 선택했다.
그리고 과거의 MVP 프로젝트를 리펙터링 하는 방식을 선택하게 되었다.</p>
<h2 id="선택한-이유">선택한 이유</h2>
<p>사실 이 선택을 하면서 정답이라고 생각이 들진 않았다.
Compose 관련 코드랩이 잘 되어 있어 코드랩으로 차근차근 해보는 게 정답이라고 느꼈기 때문이다.
하지만 나름의 이유는 있었다.</p>
<h3 id="1-왜-mvvm-에-맞는지-몸소-느껴보기">1. 왜 MVVM 에 맞는지 몸소 느껴보기</h3>
<p>필자는 예전 Compose 관련 컴퍼런스에서 MVP 보다는 MVVM 이 Compose 에 어울린다고 들은 적이 있었다.
당시에 머리로는 끄덕끄덕했지만 실제 체감까지는 하지 못했다.</p>
<p>그러던 도중 예전의 <code>MVP 로 구현된 안드로이드 프로젝트</code>가 눈에 보였다.
어짜피 Kotlin 이어서 언어의 제약도 덜할 것 같았고 당장의 궁금함을 해소하고 싶어 이 방식을 선택했다.</p>
<h3 id="2-언젠가-마주하게-될-리펙터링">2. 언젠가 마주하게 될 리펙터링</h3>
<p>무작정 하되 프로젝트 리펙터링 방식을 선택한 이유도 나름 있었다.</p>
<p>Compose 는 기존 프로젝트에 섞어서 사용할 수 있다.
xml 에서 Compose 를 사용할 수 있고, Compose 에서도 xml view 를 사용할 수 있다.</p>
<p>만약 Compose 를 배운다면 이 경험도 꽤 중요할거란 생각이 들어 
프로젝트 리펙터링으로 Compose 를 다뤄보려 했다.</p>
<p><img src="https://images.velog.io/images/ricky_0_k/post/8fc954ba-0ede-40bf-a9e7-a803359a2d35/image.png" alt=""></p>
<p>물론 무작정하는 만큼 한번에 완벽하겐 불가능할 것이다. (위와 같이 되면 기적이다)
하지만 난 리펙터링을 한 번으로 생각하지 않는다. </p>
<p><code>무작정 리펙터링</code>, <code>개념보고 느낀바대로 리펙터링</code> 등 여러번 리펙터링하며
Compose 를 공부하기 위한 충실한 실험쥐 프로젝트로 꾸준히 활용하려 한다.</p>
<h3 id="3-무작정의-좋은-이면-살리기">3. 무작정의 좋은 이면 살리기</h3>
<p>사실 무작정은 위험할 수 있다. 무작정 해본 지식이 잘못된 기본기로 둔갑할 수 있기 때문이다.
<img src="https://images.velog.io/images/ricky_0_k/post/f48d5783-6cca-4f36-89d1-65974cfe4c5c/2175b59ecb9c2521e593ef969728a110.gif" alt="">   </p>
<div style="text-align: center;"><span style="color:#999999;">이렇게 생각없이 무작정했다가 망할수도 있다</span></div>


<p>하지만 무작정 해본 이후 개념을 바라보는 시간을 가진다면
무작정 경험은 <code>개념 공부의 디딤돌</code>이 될 수 있다. 
애로사항만큼 기억에 깊게 남는 게 없기 때문이다. </p>
<p>필자의 경우 무작정 검색으로 작성한 코드로 애로사항을 직접 느끼고
그 이후 개념을 돌아볼 때, <code>실제 사례들을 연계</code>하며 개념을 습득한 사례가 제법 있었기 때문에 
오히려 개념 공부하고 실제 어떻게 쓰일지 예상하는 데 도움이 되었었다.</p>
<p>당장 Compose 를 실무에 써야하는 상황이라면 경험보다 개념 공부가 우선이겠지만,
길게 공부해도 문제 없는 상황이므로, 이번엔 이렇게 Compose 개념을 익혀보려 했다.</p>
<h3 id="정리">정리</h3>
<p>그 외에도 <code>언어의 제약이 덜함</code> 등 다양한 명분이 있었고 
나름의 고민 끝에 <strong>무작정 경험</strong> 으로 Compose 공부를 시작하게 되었다.</p>
<h1 id="프로젝트-선택하기">프로젝트 선택하기</h1>
<p>4년전(...) 에 Kotlin 언어 공부용으로 개발했던 앱을 리펙터링 진행해보려 한다.
앱은 <code>학점을 입력하는 앱</code>이며 큰 기능은 아래와 같다.</p>
<ol>
<li>기본점수 설정 화면</li>
<li>졸업학점 입력 화면</li>
<li>학점을 아름답게 볼 수 있는 화면</li>
<li>학점 및 과목 입력 화면</li>
<li>학점 통계 화면</li>
<li>이미지 크롭 화면</li>
<li>설정 화면</li>
</ol>
<p>그리고 현재 앱을 돌아봤을 때 개발 스펙은 이렇다.</p>
<ol>
<li>MVP 아키텍처</li>
<li>MPAndroidChart</li>
<li>Custom ProgressBar</li>
<li>Room</li>
<li>Deprecated 코드 다수 존재 (AsyncTask 등..)</li>
<li>거대한 Application class 코드</li>
</ol>
<p>어째 스펙을 적어야하는데 문제가 더 돋보이는 듯한 느낌은 느낌일뿐이다 ㅇㅅㅇa
이거 이외에도 4년전엔 몰랐던 개념들을 적용하기 위해 추가적인 작업들을 더 하게 될 것 같다.</p>
<p>본래 사진을 넣을까했으나 삭제된 앱이고
마음 속에서 아직 공개하지 말라는 마음의 소리가 있어
각 포스팅마다 스크린샷을 드문드문 넣어놓으려 한다.</p>
<h1 id="현재-진행상황">현재 진행상황</h1>
<p>현재 4번까지 진행을 완료한 상태이다. (5~60 %)
4번의 경우 개선 아이디어가 생겨, 일부 UX 도 같이 개선하고 있다.</p>
<p>위에 언급했던 명분은 어느정도 맞아들어가고 있다. 
하지만 일부 아쉽게 된 내용들도 있다.</p>
<ol>
<li><p>xml 에 <code>ComposeView</code> 도 써보고, Compose 에 <code>xml 용 View</code> 도 넣어보고 있다.
그러면서 <code>Base</code> 도 같이 작성했으며, 어떻게 호환을 시킬지에 대해 나만의 생각을 정리해보고 있다.</p>
</li>
<li><p>뒤 포스트에서 적겠지만 <code>state 관리 방법</code>으로 인해 제대로 애로사항을 겪는 중이다.
뒤에 내가 처리한 방법을 언급하면서, 이 방법이 맞을지에 대해서도 이야기를 들어볼 수 있으면 좋겠다.
(viewer 가 거의 없어 현실성까진 모르겠다만 ㅇㅅㅇa <del>아무말이든 다 환영</del>)</p>
</li>
<li><p>뒤로 갈수록 개발 속도가 느려져, 100% 체감하지 못한 내용들도 일부 생기고 있다.
MVP 에 Compose 를 적용못한다는 건 몸으론 이해했는데
실제 MVP 에 Compose 를 적용해보진 못해 설정 화면 리펙터링 때 이 부분을 다뤄보려 한다.</p>
</li>
</ol>
<p>잘못된 기본기가 정립되는 걸 막기 위해, 
포스팅과 코드 내용들을 통해 과거의 나를 되돌아보고 
최대한 내가 느꼈던 바들과 깨달은 바들을 수두룩빽빽히 정리해보려 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2021년에 정리해보는 회고]]></title>
            <link>https://velog.io/@ricky_0_k/2021%EB%85%84%EC%97%90-%EC%A0%95%EB%A6%AC%ED%95%B4%EB%B3%B4%EB%8A%94-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@ricky_0_k/2021%EB%85%84%EC%97%90-%EC%A0%95%EB%A6%AC%ED%95%B4%EB%B3%B4%EB%8A%94-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Thu, 30 Dec 2021 11:48:31 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p>어느덧 2021년도 끝나간다. </p>
<p>간단하게 작년이랑 올해를 비교해보면</p>
<p>작년엔 코로나 1년차라 머뭇거린 게 많았고, 
성과보단 과정이 많았던 기억이지만</p>
<p>올해는 새로운 성과물도 좀 있었던 것 같다.</p>
<hr>
<h1 id="1-회사">1. 회사</h1>
<p>작년엔 CI / CD 개선, 테스트 코드 작성, 언어 및 기능 리펙터링 등 내부 개선에 주력했던 기억이 더 크다.
하지만 올해는 전면적으로 개선한 기능들도 있고 서비스 차원에서 새롭게 추가된 기능들도 많았다.</p>
<p>내가 회사에서 작업한 내용을 보이는 문서에 적는 건 처음이라 그런지 이미지는 올리기 부담스러웠다.
이건 공개 회고를 생각하면서 어쩔 수 없다는 생각이 들었다.. ㅠ</p>
<h2 id="1-유저-개선">1. 유저 개선</h2>
<p><code>유지보수</code>에 대해서 깊게 생각했던 스프린트였다.</p>
<p>새롭게 추가되는 기능, 마이그레이션, 회원이 연관된 다양한 화면의 동작여부도 체크하며 
그만큼 시간이 더 걸렸고, 그만큼 많은 유관자들과 커뮤니케이션들을 했던 기억이다. </p>
<p>다양한 고민을 했고 시간도 오래 걸렸던 스프린트였지만 
서비스에 진심인 사람들은 어떻게 서비스를 생각하는지, 
어느 시기와 차원까지 고민하는지에 대해서도 많이 알게 되었던 것 같다.</p>
<p>기술적으로는 LiveData 를 도입하고 코루틴 기능 일부 개선한 게 전부였던 것 같다. 
그리고 회원이니만큼 테스트 코드 작성에 진심을 더하고, 에러 처리 정책도 새로 정하고 반영하면서 
커머스 초기 틀을 일부 잡았던 기억이다.</p>
<h2 id="2-상단탭">2. 상단탭</h2>
<p>메인 화면의 구조를 완전히 바꾸게 되면서 오랜만에 <code>Fragment</code> 와 <code>ViewPager</code> 를 깊게 바라봤던 시기였다.</p>
<p>개발 도중 이슈를 발견하여 수정하면서 혼란을 겪을 때도 있었지만 유관자들과 유연하게 해결할 수 있었고, 만약 우리가 느슨하게 개발한다면 <code>배포로 인해 새로운 서비스 버전 관리 정책이 생길수도 있겠다</code>는 교훈을 얻었던 스프린트였다.</p>
<p>이후엔 새로고침 주기를 개선하고, 어떤 데이터를 상단탭에 두는 게 좋을지도 
AB 테스트를 통해 적용시키면서 앱을 긍정적으로 개선하는 재미를 느끼기도 했다.</p>
<p>이유는 모르겠지만 상단탭 배포 이후로 앱 사용자수가 늘었던 걸 보며 재밌는 걸 개발하고 개선했다는 생각이 든다.</p>
<p>A/B 테스트에 감명이 깊어, 관련 패스트캠퍼스 강연을 외부의 가호를 받아 결제하기도 했는데
엄청 예전에 중간까지만 봤었어서 늦어도 1월까지는 다 보려고 한다.</p>
<h2 id="3-커머스-개발">3. 커머스 개발</h2>
<p>유저 개선은 커머스의 토대를 잡는 시발점이었다.
유저 개선 이후 주문 / 결제 / 클레임 개발에 참여했다.</p>
<p>상단탭 개발 시기에 킥오프가 되어서
처음엔 서브 기능인 배송지, 계좌 관리 기능을 먼저 개발하고, 
이후로 클레임 파트를 주력으로 맡아 개발을 했었다. </p>
<p>기술보다는 <code>Flow</code> 를 고민하는데 더 많은 노력을 했고, 
과정 도중 필요한 유틸 기능들도 추가하며 개발을 완료할 수 있었다.</p>
<p>기본 기능은 개발했지만 아직 커머스 개발이 다 끝난 건 아니다. 
이후 리뷰 개발(완료) 와 다른 개발에도 참여하고 있다. 
(리뷰의 경우 이미지 업로드 쪽만 support 를 했다.)</p>
<p>지금도 기능을 개발하며 유관자와 다양한 이야기를 하는 만큼, 앞으로도 많은 걸 느끼며 커머스를 개발할 것 같다.</p>
<blockquote>
<p>🔥 느낀점 : 섬세한 개발이 필요한 커머스(?)</p>
<p>주문 / 결제 / 클레임 개발을 완료하고 느낀 점이 있다면, 
커머스는 과정, 유저를 다각도로 고려한 <code>섬세</code>한 개발이 필요하고, 
고려한 정도에 따라 서비스의 미래에 영향을 끼친다는 것이었다. </p>
<hr>
<p>아래와 같이 다양하게 고려하고 이해할 내용이 많았다.</p>
<ol>
<li><p>과정
앱을 이용하는 사용자가 물품을 주문하고 받기까지엔 <code>많은 과정</code>들이 있는데
그 현황을 사용자에게 보이고 액션을 취할 수 있도록 해야했다.</p>
<p>특히 클레임에서, 사용자에게 보여주고 사용자가 <code>행동</code>하는 <code>다양한 시나리오</code>가 있었고
이를 고려 및 문제를 해결하기 위해 백앤드의 데이터가 어떻게 관리되는지도 알게 되었다.     </p>
</li>
</ol>
<ol start="2">
<li><p>유저 타겟
그리고 커머스는 사용자가 한 파트만 있는게 아니었다. </p>
<p><code>구매자, 판매자가 모두 서비스의 사용자</code>이기 때문에 니즈가 다른 두 파트를 모두 만족시켜야 하고, 그만큼 많은 시간과 인력이 필요하다는 걸 느꼈다.</p>
</li>
</ol>
<p>개발 속도를 높이기 위해 오히려 <code>섬세한 개발의 필요성</code>을 느꼈다.</p>
<hr>
<p>그리고 개발을 할 수록 이전 개발을 더 들먹이고 마주하며
어떻게 개발하겠다가 아닌 <code>이전 내용을 이렇게 참고하여 수정</code>한다는 이야기를 많이 하고 있다.</p>
<p>한번에 모든 커머스 기능을 만들기 어려운만큼 모두가 단계적으로 개발을 진행하는데
어느 순간 나도 <code>수정</code>의 차원에서 이야기를 하고 있었다. </p>
<p>내가 작성한 코드들을 다시 마주하면서, 내가 앞전에 얼마나 섬세하게 개발했느냐에 따라 
새로운 기능들을 붙이기가 어려워질수도 있겠다는 생각을 하게 되었다. </p>
<hr>
<p>작성하다보니 다른 도메인도 마찬가지 일 것 같긴하다. </p>
<p>하지만 커머스 개발에 참여하면서 위의 내용들을 깊게 느낀건 사실이었기에
나는 커머스 개발의 키워드를 <code>섬세</code>로 정리하게 되었다.</p>
</blockquote>
<h2 id="4-이미지-로직-2차-정리">4. 이미지 로직 2차 정리</h2>
<p>사실 작년에 이미지 로직을 한번 정리한 적이 있었다. </p>
<p>작년엔 파편화 되어 있는 코드를 합치고, deprecated 된 코드들을 정리하는 게 주였다면, 
이번엔 <code>로직이 제대로 동작하고 있는지</code>에 대한 정리였다. </p>
<p>리뷰 기능으로 인해 이미지 로직이 메이저 기능으로 바뀌면서 전반적인 기능 점검이 필요했고
리뷰 화면에서 쓰이는 업로드 방식을 추가하면서 제대로 사진은 가져오는지, 스케일링은 제대로 처리하는지, 임시 이미지는 지워주는지 등을 점검하고 개선했다. </p>
<p>점검하고 개선하면서 <code>bitmap 관련 개념</code>도 다시 돌아보는 계기가 되었다.</p>
<p>개인적으로는 시행착오가 컸어서 아쉬웠었다. 
처음엔 간단하게만 생각했어서 갤러리 이미지 업로드만 제공하는 등 일부 혼동이 있었고, 업로드 속도 성능으로 인해 업로드 방식을 자주 수정하면서 예상보다 많은 소요가 있었다. </p>
<p>기획서를 더 쫀쫀하게 바라보면서 물어보고, 내 상황을 공유하면서 다른 동료들의 진행사항도 확인하는 방법을 좀 더 터득해야겠다는 생각을 하면서 개발을 마무리했다.</p>
<h2 id="5-결론">5. 결론</h2>
<p>난 항상 회사에 대해 소개하면 <code>다 같이 재미있게 일하고 더욱 성장할 수 있는 회사</code> 라고 생각하고 이야기해왔다.<br>지금도 그 생각엔 변함이 없다.</p>
<p>이번 년도는 특히 더 그랬다. 
많은 사람들과 커머스 장기 개발을 같이 하고, 팀 차원에서도 작년에 비해 같이 논의하고 
개발하거나 페어로 개발을 했던 에피소드들도 더 생겼어서 더욱 그랬던 것 같다.</p>
<p>그리고 실수가 있더라도 모두가 1순위로 그 문제를 빠르게 해결하려 노력할 뿐이다. 
경우에 따라 책임론이 필요할 수 있겠지만 서로에 대한 비난보다는 빠른 문제해결과 독려가 먼저라는 건 
메이커에게 심적으로 안정감을 주면서 재발 방지의 의지를 더욱 돋아준다고 생각이 든다.
개인적으로는 <code>실수</code>는 <code>없애야 하는 것이 아닌 관리이다</code> 라는 말을 이해하고 실천하고 있다는 생각도 든다.</p>
<p>위에 이렇게 좋았던 점을 이야기한만큼 코로나가 더더욱 아쉽게 느껴진다. 
예전엔 많았던 야외 활동들도 코로나 때문에 싸그리 없어졌다고 하는 데 
&quot;코로나가 풀리면 생기겠지” 하는 희망을 가져본다.</p>
<hr>
<h1 id="2-동아리">2. 동아리</h1>
<p>작년과 두드러진 차이가 있다면 마음을 바꿔 거의 2년만에 동아리 활동을 한 것이다. 
그것도 새로운 동아리 활동인데 우려되는 면도 많았지만 얻은 건 매우 많았다.</p>
<p>참고로 넥스터즈의 동아리 활동 시작이 12월 말이었던지라 보일 수 없는 작년 회고에서 약간 언급하긴 했다. 
그 때는 “첫 동아리 활동에 대한 느낌 이야기”와 ”Gather 재미있다 뿌에엥” 이 전부였는데
이번 년도 회고에서는 (조금 딱딱하게) 런칭한 서비스에 대한 회고 이야기만 나오게 될 것 같다.</p>
<h2 id="1-nexters-18th-winepick">1. Nexters 18th. WINEPICK</h2>
<p>위에서 언급했던 “Gather 재미있다 뿌에엥” 이 바로 이 팀이었다. </p>
<p><img src="https://images.velog.io/images/ricky_0_k/post/be84be13-2db8-4a5e-bedd-ca9045500bc7/image.png" alt=""></p>
<p>과정은 위 사진으로만 언급하고 결실만 이야기해보려 한다. 
(여기서 내가 누구였는지도 기억이 가물가물하다.)</p>
<h3 id="결실"><strong>결실</strong></h3>
<p>결국 <code>안드로이드 리드</code>를 맡았고 <a href="https://play.google.com/store/apps/details?id=kr.co.nexters.winepick"><code>런칭</code></a>까지 완료했다.</p>
<p><img src="https://images.velog.io/images/ricky_0_k/post/7cb4f2fe-f389-4920-a69e-6cd5568924c1/image.png" alt=""></p>
<p>그런데 이것도 개발에 나름 에피소드가 있었다.</p>
<p>1~2월엔 치아 치료와 더불어, 회원 작업이 피크를 찍고 있을 때여서 신경을 깊게 못 쓰기도 했고, 
2월 이후엔 동아리 활동이 끝나고 강제성이 없어져 기울기가 최하인 수준으로 개발되고 있었다. 
그런데 단톡방에서 생일 축하해주는 거에 리마인드 되서 다시 기울기를 버닝하고 런칭까지 갈 수 있었다.</p>
<blockquote>
<p>뜻 밖의 교훈(?) : 프로젝트를 소생시키려면 같은 팀원들의 생일을 적극 축하해줍시다(?) ㅇㅅㅇa</p>
</blockquote>
<p>프로젝트를 돌아보면 일단 개발 난이도 자체는 어렵지 않았다. 
기능이 많진 않았고 flow 도 복잡하진 않았어서, <code>단순 기능 구현</code>에만 신경을 쓰면 되었다.</p>
<p>동아리에서 오랜만에 협업을 하면서 안드로이드를 개발했었는데, 같이 개발했던 안드로이드 개발자 분들의 열정이 엄청났어서 오히려 <code>내가 배우기도 했던 프로젝트</code>였다. 뭔가를 각자 맡으면 그 것보다 더 해와서 놀랄 때가 많았다. 
내가 Hilt 를 적용할 수 있던 것도, 개발자 분이 앱 전체에 Koin 을 적용시켜와서 동기부여가 된 것이 컸다.</p>
<p>치열하게 코드리뷰를 하진 못했지만 온라인인 상황에서도 최선의 안드로이드 협업을 하며 같이 개발했기에, 
이는 다음 기수 활동을 바로 해야겠다는 마음으로 직결되었다. </p>
<p>그리고 이와 함께 <code>바쁘고 정신없는 여름</code>이 시작되었다.</p>
<h2 id="2-nexters-19th-basket">2. Nexters 19th. BASKET</h2>
<p>WINEPICK 앱 개발을 끝내고 아쉬운 게 있었다면 <code>런칭하고 끝</code>이었다는 것이었다. 
실제 지금 WINEPICK 은 앱 사용보다는 앱 소개용이나 github 기록용, github actions 처음 적용한 썰 푼다의 소재로만 활용되고 있다.</p>
<p>그런데 WINEPICK 뿐만 아니라 매번 동아리 프로젝트를 하면서 그 갈증을 느껴왔었고, 
다음 기수에서는 이걸 해소해보고 싶었다. 설사 나 혼자 안드로이드를 개발하게 될 지라도..</p>
<p>그렇게 들어간 팀이 BASKET 팀이었다.</p>
<h3 id="1-defi">1. Defi</h3>
<p>사실 Defi 에 대해 아는 바가 1도 없었다. 
하지만 발표를 들었을 때 이 서비스는 런칭하고 운영까지 가능하다는 강한 확신이 들었고, 마침 코인 생태계를 이해하고 싶은 니즈가 있던 시기이기도 해서 과감하게 1지망으로 썼는데.. 운 좋게도 되었다. </p>
<p>일단 내가 만든 산출물(자식)을 개발자(부모)가 이해 못하면 안되니까 나름의 노력을 했던 것 같다. 
매달 일정 금액씩 <code>Defi 에 소비</code>하면서 손실 생각없이 무작정 도전해보고, 초기 Defi 서비스에 <code>치고 빠지기</code>(?) 도 해보면서 접해보았다. (그리고 코인 징징이분들은 실존한다는 것도 깨닫게 되었다...) 
그리고 Defi 개념에 대해서 일부 이해가 안되는 부분도 있긴해서, 이전 동아리 사람이 PM을 맡으면서 만들어진 <code>Defi 스터디</code>에 참여하여 다른 직군의 사람들과 같이 이야기 해보기도 했다. </p>
<p><img src="https://images.velog.io/images/ricky_0_k/post/e8ad0456-d905-44ed-8e9e-c47200456c90/image.png" alt=""></p>
<p>멱살잡고 이끌어주신 PM 님께 감사하단 말도 같이 드려본다.
이런 노력들 덕분인지, 아직까지도 Defi 는 모르겠지만 그래도 얼추의 이야기는 이해하게 된 것 같다. </p>
<h3 id="2-cicd">2. CI/CD</h3>
<p>앞서 WINEPICK 이 “github actions 처음 적용한 썰 푼다” 였다면, BASKET 는 <code>그동안의 지식을 집대성한 썰 푼다</code> 였다. dev / real 서버 분리, 이에 따른 브랜치 처리 및 apk 배포, 유닛 테스트 코드 작성, 할 수 있는 flow 다 때려넣기 등 할 수 있는 건 다 했던 것 같다. 
(단 플레이스토어 바로 업로드, App Distribution 연동은 제외...)</p>
<p><img src="https://images.velog.io/images/ricky_0_k/post/7fafa7ef-34a6-480d-a800-d3901575fd4c/image.png" alt=""></p>
<p>Velog 에 github actions 내용을 정리할 수 있던 것도 여기서의 시행착오 덕분이지 않나 싶다.</p>
<h3 id="3-기타">3. 기타</h3>
<p><code>Hilt 를 다른 방식으로 적용</code>해보고, MpAndroidChart 이용해서 <code>주식형 차트 레이아웃</code> 만들고, 
공부하고 싶던 라이브러리 사용해보고 (Gson(?), DataStore 등) 해보고 싶은 건 다 했던 것 같다. </p>
<p>실제 exception 처리나 테스트 코드의 경우에는, 이 프로젝트에서 써먹었던 것들을 회사에 일부 써먹기도 했다.</p>
<h3 id="4-결실">4. 결실</h3>
<p>엄청 바쁜 여름과, 개인사정 등으로 iOS 보다는 늦었지만 <a href="https://play.google.com/store/apps/details?id=finance.ccbc.basket"><code>런칭</code></a>까지 맛볼 수 있었다.</p>
<p><img src="https://images.velog.io/images/ricky_0_k/post/593f2260-47cf-4a59-8d04-814630b02587/image.png" alt=""></p>
<p>아직 안드로이드 다운로드 수는 별로이지만, 
운영 오픈채팅방도 있고 그 방에 나름 코인쪽에서 저명한 활동을 하는 사람도 있다고 해서 
처음 목표는 일부 이루지 않았을까하는 생각이 든다. </p>
<p>위에서 살짝 언급했지만, PM 님 이외에도 Defi린이에게 Defi 이야기하거나 설명해주신다고 일부 고생하신 분들이 정말 많았는데 이 자리를 빌어 감사의 말씀을 다시 드려본다.</p>
<h2 id="3-스터디">3. 스터디</h2>
<p>상반기, 하반기 때 모두 넥스터즈에서 생긴 스터디에 참여했었다. 
모두 성과가 있는 스터디였고, 그만큼 일부는 아쉬웠던 스터디도 있었다.
참여했던 스터디들을 추려 회고해보려 한다.  스크린샷은 지금 하고 있는 것만 올리려 한다.</p>
<h3 id="1-알고리즘-스터디-done">1. 알고리즘 스터디 (<del>DONE</del>)</h3>
<p>당시 쉬운 알고리즘 문제도 못 풀었던 것에 쇼킹하여 바로 <code>알고리즘 스터디</code>에 참여했었다.</p>
<p>내가 생각하지 못했던 사람들의 풀이 방법 이야기도 듣고
어떤 면을 고려하면서 알고리즘을 짜야하는지를 다시 상기할 수 있어 좋았다.</p>
<p>하지만 알고리즘 자체가 지속성이 필요한 스터디인데, 지금은 끝나서 아쉬운 마음이 있다.</p>
<h3 id="2-크로스-플랫폼-스터디-done">2. 크로스 플랫폼 스터디 (<del>DONE</del>)</h3>
<p><code>사실상 Flutter 스터디</code>였다.</p>
<p>당시에 스터디원들의 이야기를 듣고 쿡북을 한번 돌아보는 방향으로 진행되었었는데 
놓쳤던 내용을 찾거나 이렇게 코드를 작성할수도 있구나를 느끼면서 <strong>내면의 정리</strong>도 할 수 있었다.</p>
<p>사실상 그 다음 step 이라거나 동기부여에 대해서도 생각했어야 했는데, 
이 부분은 내가 소홀히 했어서 개인적으론 아쉬움이 컸던 스터디였다.</p>
<h3 id="3-graphql-스터디-done">3. GraphQL 스터디 (<del>DONE</del>)</h3>
<p>짧고 굵게 GraphQL 을 이해했던 스터디였다. 
<code>graphQL 이 무엇인지 큰틀을 알 수 있었던 스터디</code>였다.</p>
<p>지금 사내에서 graphQL 이야기가 나오고 있는데 
백앤드 개발자와 말이 통하면서 잘 참여했다고 느낀 스터디였다.</p>
<h3 id="4-책-읽기-스터디-doing"><strong>4. 책 읽기 스터디 (DOING)</strong></h3>
<p>이 스터디는 각자의 브랜치를 따고, 읽은 <code>책 내용을 마크다운에 정리</code>하여, 마스터 브랜치로 PR 날리는 방식이다. </p>
<p>기타 자세한 설명은 <a href="https://github.com/Nexters/READ-BOOK">이 링크</a> 와 아래 사진을 통해, 레파지토리의 역사(?)와 연륜(???) 을 직접 보는 걸 추천한다 
(<code>#대놓고_앞광고</code> <del><code>#보고_Star는_기본인거_아시죠?</code></del> <code>#아_어뷰징인가_취소</code>)</p>
<p><img src="https://images.velog.io/images/ricky_0_k/post/90efde3e-e4ae-441a-8a10-54d24dabd67d/image.png" alt=""></p>
<p>참고로 위키 정리 및 merge, 벌금 수급 및 분배(?)는 모 프론트앤드 개발자 분께서 총괄하여 진행해주시고 있다. (이 자리를 빌어 그 분께도 정말 감사하다는 말씀을 전합니다.)</p>
<p>회고하면서 내가 작성했던 방식도 돌아보려 한다.</p>
<ol>
<li><p>Notion → Markdown</p>
<p>난 책을 읽고 Notion 에 정리를 하고 있어서, notion 내용을 markdown 에 넣고 PR 을 날리는 편이다.
이렇게 하니 내용을 옮기면서 복습이 되어 기억에 좀 오래 남아 이렇게 하고 있다. 
이렇게 하다보니 이미지를 다시 markdown 에 넣어야 하는 수고로움은 있긴하다.
(그랬는데도 까먹는 건 안 비밀 <code>ㄱ-</code> )</p>
</li>
<li><p>고해성사(?)</p>
<p>고해성사(?)를 하자면, 사실... 회사에서 읽고있는 책을 스터디에 언급한 적도 제법 있었다.
(어찌되었든 그 주에 책을 읽은거긴 하니까 괜찮지 않을까...하는 적반하장식 행복회로를 가져본다. ㅇㅅㅇa)
그런데 개인적으론 오히려 복습이 2중으로 되어 기억에 좀 오래 남았던 것 같다. 
TMI로 저 Repository 에서 소프트스킬 책이 그 예이다.</p>
</li>
<li><p>망각</p>
<p>위에서 <code>ㄱ-</code> 과 함께 언급했었지만 읽은 지 오래된 경우는 책 내용을 까먹는 경우도 있다.
이럴 땐 어떻게 다시 <code>리마인드</code>하면 좋을 지 고민해보고 있다.</p>
</li>
</ol>
<h3 id="5-기술공유-스터디-">5. 기술공유 스터디 (?)</h3>
<p>자신이 조사할(공부할) 기술을 노션에 공유한뒤 <code>자신이 조사하거나 공부한 내용을 발표</code>하는 방식이었다.</p>
<p>코동프 스터디와 더불어 후술할 기본기에 대해 많은 생각을 하게 되었던 스터디였다.</p>
<p>일정이 정신이 없었어서 중간에 빠지고 9월에 다시 참여하겠다고 했었지만 
생각보다 9월 이후엔 다른 것과도 엮여 바빴고, 그러다 보니 연말까지 와버렸다.</p>
<p>조금 여유가 생긴 현 상태에서 돌아보면... 지금도 하고 있었으면 하는 스터디이다.</p>
<h3 id="6-코동프-스터디-pending">6. 코동프 스터디 (PENDING)</h3>
<p><code>코틀린 동시성 프로그래밍 책을 읽는 스터디</code>였다. </p>
<p>그런데 사실상 19기 안드로이드 개발자가 대부분이었어서 안드로이드 스터디 방 느낌도 있었다.
책 자체는 코루틴을 이야기하는 책이었고, 난 중간부터 참여했다.</p>
<p><img src="https://images.velog.io/images/ricky_0_k/post/566f8e0d-2b71-4b22-b1bb-644b2b30adaa/image.png" alt=""></p>
<p>이 책으로 알게 모르게 놓쳤던 기본기나 기능들을 보고 실제 내 작업에도 활용할 수 있어 좋았다.
코동프 책읽기가 끝나고는 코딩 인터뷰 완전 분석 책을 일부 스터디원들과 같이 봤었다.</p>
<p>지금은 각자의 일정이 바빠 잠시 중단된 상태이지만, 
이 스터디 덕분에 안드로이드 개발자분들과 이야기를 할 수 있어 좋았던 스터디였다.</p>
<h2 id="4-결론">4. 결론</h2>
<p>일전 회고에서 언급했었지만, 사실 넥스터즈 활동에 기대도 있었지만 걱정이 더 컸다.
앞서 언급했다시피 오랜만의 동아리 활동이고 개인적 차원으로 우려되는 내용들도 많았었기 때문이다.</p>
<p>하지만 지금 돌아보면 <code>재밌게 활동했던 것 같다.</code>
동아리의 의무는 다하면서 내가 하고 싶은 건 다해봤고, 활동으로 얻은 게 많았어서 개인적으로는 만족했다. 
더불어 이것 저것 저지르고 보고, 수습하는 성향도 아직 버리지 못했다는 걸 알 수 있었다.</p>
<p>하지만 온라인이어서 아쉬웠고, 언젠가는 오프라인으로 한 번은 참여를 시도하지 않을까 생각된다.
(온라인으로 참여 시도하지 않겠다는 말은 물론 아니다.)</p>
<hr>
<h1 id="3-블로그">3. 블로그</h1>
<p>말로만 이야기했던 블로그를 다시 살려보게 되었다.</p>
<h2 id="1-medium-velog">1. <del>Medium</del> Velog</h2>
<p>예전의 github.io 를 살리기엔 작성하는 노력에 비해 관리 소요가 클 것 같아 새로운 서비스를 찾기로 했다.
그렇게 찾은 게 Medium 과 Velog 였고, 지금은 <code>Velog</code> 를 적극적으로 활용해보고 있다.
Velog 를 사용하게 된데에는 몇 가지 이유가 있었다.</p>
<p><img src="https://images.velog.io/images/ricky_0_k/post/09ce84e6-60e6-433d-8e64-faaaec413c97/image.png" alt=""></p>
<p>첫번째로 위와 같이 <code>시리즈로 목록 관리가 쉽다</code>는 게 두드러진 이유였다.</p>
<p>여러 포스트에 걸쳐 글을 써야할 경우엔 시리즈가 좋은 기능이라 생각되어 
Velog 로 장문이나 시리즈 형태의 글들을 써보려 노력하는 중이다.</p>
<p><img src="https://images.velog.io/images/ricky_0_k/post/2416b273-630e-4403-8317-948a95170de1/image.png" alt=""></p>
<p>두 번째는 Velog 에서는 이렇게 <code>다단 형태</code>로 볼 수 있어서 좋았다. 
내용을 쓰다가 갑자기 헷갈릴 때 우측을 통해 이전 내용을 보면서 흐름을 맞출 때가 많았다.
그런데 바로 스타일이 적용되는 에디터도 나름의 장점이 있어, 이건 내 개인적 성향이라 생각이 든다.</p>
<p>이렇게 지금은 Velog 를 적극 활용하고 있고 당분간은 계속 그럴 것 같다.</p>
<h2 id="2-작성하는-내용">2. 작성하는 내용</h2>
<p>아직까진 <code>개인이 공부한 내용</code>을 이야기하는 것만 하고 있다.
때로는 컨퍼런스 내용 정리한 걸 올려 보기도 한다. (공개된 컨퍼런스만)</p>
<p>별개 내용이다만 작성한 글들을 돌아보면서 꼰대 성향으로 작성이 의심되는 내용도 있어 
“아 나도 이제 그렇게 되어가는구나...” 하면서 탄산수를 먹으며 자아반성(?)을 하기도 한다.</p>
<h2 id="3-플랜">3. 플랜</h2>
<p><del>별 계획없다.</del> 후술하겠지만 <code>영속성을 유지하는 것</code>이 앞으로의 플랜이다.</p>
<p>계획이 생겼다. 개발 말고도 다양한 글을 써보려 하고, 그렇게 한 달에 몇 개까지 쓸지 내부적으로 목표를 정했다. 
1년 동안 노력해보려 한다.</p>
<hr>
<h1 id="4-독서">4. 독서</h1>
<p>올해 두드러졌던 게 <code>독서의 양</code>이였다. </p>
<p>더불어 같이 느끼는 건 책은 너무나도 읽을 게 많다.. </p>
<p><img src="https://images.velog.io/images/ricky_0_k/post/1ff3a69b-a9cd-4f75-b352-0d83fc3ad2b0/image.png" alt=""></p>
<p>참고로 위 사진은 과대 광고이다. 
여기서 4~5권 정도는 작년에 읽은 책들인데 캡처하기 귀찮아 이렇게 두었다 ㅇㅅㅇ...</p>
<p>책을 읽으면서 개발에 직접 도움이 되었던 사례도 있었어서 보람을 많이 느꼈다.
블로그 작성을 하면서 <code>읽고 싶은 글</code> 이 어떤건지도 체득할 수 있으면 좋을 것 같아 
독서하는 습관은 계속 유지하려 한다.</p>
<p>앞서 책읽기 스터디에서 언급했지만, 읽은지 오래된 책들은 내용이 드문드문 기억난다.
이럴 때 어떻게 다시 <code>리마인드</code>하면 좋을지를 고민해보고 있다.</p>
<hr>
<h1 id="5-기타">5. 기타</h1>
<p>기타 활동 내용들을 짤막하게 언급하려 한다.</p>
<h2 id="1-flutter-앱-런칭">1. Flutter 앱 런칭</h2>
<p>개인적으로 런칭한 Flutter 앱이 생겼다.
그러나 프로토타입 형태의 앱이고, 완성도도 내 기준으로 높은 편은 아니기에 회고에 언급하고 싶진 않다.</p>
<p>하지만 런칭을 하면서 Flutter 의 경우 어떤 프로세스를 거쳐 런칭하는지는 확실히 알 수 있었다.</p>
<p>예상외의 에피소드도 좀 있었다. 개발을 하면서 iOS 가 먼저 런칭이 되었고
구글은 검수가 빡세졌고, 개발자 이름으로 앱 런칭 거부를 시키기도 한다. 
(<del>#창씨개명</del> <del>#이럴꺼면_validation을_걸어놨어야지;;</del>)</p>
<p>이 건으로 Flutter 런칭도 안해보고 이론만 말하는 <code>이상론자라는 말은 피할 수 있지 않을까</code> 싶다.</p>
<h2 id="2-compose-적용하기">2. Compose 적용하기</h2>
<p>(드디어 말로만 했었던) 이전 프로젝트 Compose 전환작업을 해보고 있다.
지금은 내려간 앱이어서 이름은 공개할 수 없고 내년 회고에 언급할 수 있는 앱이 되었으면 좋겠다.
전환을 완료하고 관련된 포스팅을 Velog 에 올릴 수도 있다 ㅇㅅㅇ</p>
<h2 id="3-놀자판">3. 놀자판</h2>
<p>365일 24시간 개발만 했다면 성장을 많이 했겠지만, 태생이 그런 걸 극혐하고 삭막하다고 생각하는 편이다.</p>
<p>캘리그라피, 클라이밍 등 새롭게 경험해본 것들도 제법 있지만, 
일부는 <code>난 이걸 경험했다</code> 하고 끝나버린 것도 있다.. (캘리그라피 안녕...)</p>
<p>무언가를 기점으로 작년에 싸그리 정리했던 인스타는 여전히 잘 살아 있다. </p>
<p>내가 직관하면 패배한다는 징크스는 여전히 깨지지 않고 있다. 이 정도면 토템수준이다. 
(<del>#직관하자마자_4연패_실화냐</del>)</p>
<p>일부 가린 내용도 있는데, 전자기기들을 항상 마주하는 우리들의 시력은 소중하니까 <del>_</del></p>
<p><img src="https://images.velog.io/images/ricky_0_k/post/e696a511-0f74-4416-96a7-2bf128c24396/image.png" alt=""></p>
<p>사진 상 언급되지 않은 내용으로, 요즘 다시 더치 커피를 내리기 시작했다. </p>
<p>만들어진 더치커피는 카페인 중독자들에게 흡수됨과 동시에 회사에서 실험 용도로 적극적으로 활용되고 있다.</p>
<hr>
<h1 id="총평-올해-느꼈던-점"><del>총평</del> 올해 느꼈던 점</h1>
<p>작년엔 코로나 1년차이기도 하고, 상반기에 약간 번아웃 기운이 있었어서 열정이 왔다갔다할 때가 많았다.
그래서 개발, 자기 관리에 대해서 성장이 더뎠다고 느꼈고, <strong>부정적인 개인 총평</strong>을 남겼다.</p>
<p>올해는 과연 어땠을까? 올해는 작년과 다르게 
평가보단 <code>올해 임팩트있게 느낀 점</code> 들을 주저리주저리 적어보려 한다.</p>
<h2 id="1-난-왜-이-기능을-개발할까">1. 난 왜 이 기능을 개발할까?</h2>
<p>아래는 올해 읽었던 <code>함께 자라기</code> 책에 있던 내용이었는데, 이번 년도에 특히 공감되었다.</p>
<blockquote>
<p>📚 스펙대로 코드를 만드는 사람은 컴퓨터화 할 수 있는 영역이지만
<code>사용자의 요구사항을 분석</code>하고 그에 대한 <code>솔루션을 설계하는 사람</code>은 대체가 힘들다</p>
</blockquote>
<p>더불어 이 책의 후기 중 아래 질문을 던지는 후기가 있었다.</p>
<blockquote>
<p>❓ 만약 기능을 획기적으로 개발했더라도, 그 기능으로 유저 30% 가 떨어져나갔다면 좋은 개발일까?</p>
</blockquote>
<p>돌아보면서 잘 개발하는 것도 중요하지만, 왜 개발하는지 생각하고, 
이 개발로 파생될 이슈 생각도 중요하게 생각할 필요를 느꼈다.</p>
<p>막말로 안 좋은 사용성으로 인해 최악의 시나리오로 이 기능이 없어진다면....? 
이건 귀책을 넘어 <code>담당자들의 노력 및 성과가 한 순간에 없어진다</code> 는 것이니 말이다.</p>
<p>말은 쉽지만 아직은 어렵다. 
나는 아직까지 Why 보다는 기획서나 원하는 방향대로 안 되는 경우에 대한 캐치 및 논의가 더 쉽다.
올해에는 Why 를 좀 더 생각할 수 있는 방법을 찾아보고 싶다.</p>
<h2 id="2-기본기">2. 기본기</h2>
<p>올해는 안드로이드 기본기에 대해 고민했던 게 많았다.</p>
<p>매번 미뤄왔던 <code>안드로이드 프로그래밍 Next Step</code> 을 읽으면서 느낀바가 더 많아졌다. (아직 다 보지는 못했다)
과거의 내용들도 있지만 <code>개발을 보는 과정</code>, <code>어디까지 Android 를 깊게 분석했는지</code>를 감탄하면서 보고 있다.</p>
<p>과연 난 누군가가 Android, Kotlin/Java 특정 내용을 물어본다면 저자처럼 깊고 슬기롭게 답변할 수 있을까?
아직까진 모르겠고 자신감 또한 없다. 막연하지만 2022년엔 이 의문을 없애보려 한다.
최소 개인적으로라도 고민을 해소하는 걸 생각하고 있고, 사람들과 같이 하는 것도 생각해보고 있다.</p>
<h2 id="3-배려">3. 배려</h2>
<p>타 직군 지인에게 들었던 “<code>같이 작업하기 편한 어느 개발자</code>” 의 이야기가 있다.</p>
<blockquote>
<p>💬 정확한 커뮤니케이션이 되는 것도 좋지만, 우선적으로 그 개발자는 타 파트도 고려해서 이야기를 하는 편이다. 단편의 예로 회의에서 자신의 영역뿐만 아니라 <code>다양한 파트의 시간과 범위도 고려</code>하고, 자신이 아는 차원에서 <code>다른 파트의 애로 및 우려사항을 먼저 언급</code>하고 자신의 이야기를 한다. 
이런 면이 그 개발자와 일하기에 편하다고 느끼는 큰 이유인 것 같다.</p>
</blockquote>
<p>정리하면 이렇고, 처음 들었을 땐 별 생각이 없었지만 집에 가면서 난 어떤 개발자였는지를 생각해보게 되었다.</p>
<p>혹시 나의 영역만을 생각하고 상대를 옳아매거나 강제로 따르게 만들지는 않았을지, 
내 마음대로 판단해서 타 파트의 고려 없이 개발하진 않았을지 
(+ 맥주와 함께) 돌아보는 시간을 갖게 되었다.</p>
<hr>
<h1 id="2022-plan">2022 Plan</h1>
<p>느낀점 끄적임은 이 정도가 좋을 것 같다. 이젠 내년의 목표를 정리해보려 한다.</p>
<h2 id="1-영속성-유지하기">1. 영속성 유지하기</h2>
<p>올해 내가 했던 것들 중 확실히 효과를 본 것들이 있었다. </p>
<p>일부는 게을러서 유지를 못한 것도 있었지만, 영속성을 유지하면 성장에 도움이 될 꺼라 생각한다. </p>
<pre><code>독서, 블로깅, 개인 스크럼</code></pre><p>2022 년에는 1월부터 12월까지 최소 위 3개를 꾸준히 유지하려 한다.</p>
<p>각 카테고리의 최소 목표는 생각한 바가 있지만 이는 내년 회고에 언급할 예정이다 :) 
(안 정해서 그런거 아닙니다 ㅇㅅㅇ)</p>
<h2 id="2-동적인-활동-늘리기">2. 동적인 활동 늘리기</h2>
<p>진짜 “<code>코로나</code>” 는 단어 3음절 만으로도, 사람 속의 천불의 샘을 솟아버리게 한다.</p>
<p>집돌이, 침대벌레가 되다보니 세상 무력해지는 것이 느껴져, 내년엔 동적인 활동 늘리기를 목표로 잡아보려 한다.</p>
<p>일단 최소 목표는 클라이밍이다. 
(갑자기 생각한 건 아닙니다 ㅇㅅㅇ)</p>
<h2 id="3-느낀점으로만-만들지-않고-성과를-거두기">3. 느낀점으로만 만들지 않고 성과를 거두기</h2>
<p>단순히 느끼고 발전하는 게 없다면, 쓸데없는 느낌이지 않을까 싶다.</p>
<p><code>올해 느꼈던 점</code>에 대한 Action 을 하는 것을 생각해보고 있다. 더욱 쫀쫀해지기, 1+ 스터딩 등 아직은 막연하다.</p>
<p>이걸 지켰는지 못 지켰는지 여부는 내년 회고의 내용으로 결판이 나지 않을까 싶다.
(새벽에 쓴 거 아닙니다 ㅇㅅㅇ)</p>
<hr>
<h1 id="마무리">마무리</h1>
<h2 id="1-부족함을-바라보는-방법">1. 부족함을 바라보는 방법</h2>
<p>(원래는 <code>부족함의 늪</code>이었으나, 다시 정리하면서 새로 느낀 점이 있어 제목, 내용을 수정했다)</p>
<p>2020년 회고까진 <code>부족함</code>을 <code>채움과 개선의 대상</code>으로만 바라보고 
<code>생기면 안되는 요소</code>라 생각하여, 단순 무지성으로 없애려던 게 컸다.</p>
<p>그런데 올해부턴 이를 다른 관점으로 바라보게 된 것 같다.
올해는 무언가의 노력으로 <code>부족함을 채우면</code>
채운 부분을 통해 <code>내가 몰랐던 부족함을 찾았을 때</code> 가 많았기 때문이다.</p>
<p>일일히 찾은 걸 설명하긴 어렵지만 
<code>찾은 부족함들</code>을 채우고, 또 만들기를 반복하는 2021년의 나를 보고
앞으로는 <code>부족함을 적극적으로 찾고 마주하자</code> 는 생각을 갖게 되었다.</p>
<p>두려움 없이 더한 부족함을 <code>마주하는 방법</code>과 <code>자신감</code> 을 익히고 <code>대처 하는 경험</code> 을 쌓아,
<code>부족함의 선순환</code> 을 일으키는 게 더 중요할 거라 생각이 들었다.</p>
<p>더불어 진짜 없앨 것은 <code>피하거나 감추기</code>, <code>알면서도 냅두기</code> 라 생각이 들었다.
이로 인해 <code>자기합리화</code>와 <code>나태의 늪</code>에 빠지는 것이 진정한 문제라 생각이 들었고
올해는 <code>부족함</code> 을 <code>다른 의미</code>로 바라보며 나 자신을 속이지 않으려 한다.</p>
<p>회고 작성한 후 며칠만에 이런 생각들과 의지가 생기는 걸 보니
<code>1년의 나 자신의 모든 것을 가림 없이 돌아보는 시간</code> 인 회고는 
<code>죽이되든 밥이되든 꼭 해야한다</code>는 생각이 더 크게 든다.</p>
<h2 id="2-감사">2. 감사</h2>
<p>항상 그렇지만 올해도 많은 사람들 덕분에, 부족함을 채우거나 찾으며 감사함을 느꼈던 한 해였다. </p>
<p>종류는 다양하다. 검색해서 나오는 <code>글이나 영상</code>이기도 했고, <code>개발/실무적 조언</code>이기도 했고, <code>사적인 이야기</code>를 하며 짐을 덜기도 했다.</p>
<p>뭉뚱그려 많은 분께 감사하다는 말과 함께, 내가 받은 도움을 <code>다른 사람에게 넘겨줄 수 있는 사람</code>이 될 수 있도록 발전하고 노력하고 싶다.</p>
<h2 id="3-작년과는-사뭇다른-회고">3. 작년과는 사뭇다른 회고</h2>
<p>한 일을 정리한 건 똑같았지만 다른 방식으로 작성했다보니
확실히 작년과는 다른 느낌의 회고가 나왔다.</p>
<p>먼저 <code>첫 번째 차이</code>는
자가 평가 대신 이번 년도의 경험이나 책을 읽으면서 얻은 <code>느낀점을 정리</code>한 것이었다.</p>
<p>이걸 돌아보기 위해 1월부터 12월까지를 쫙 생각해보고 그 중에 임팩트 있는 걸 뽑았는데
그 과정에서 1년동안 내가 어떤 생각을 하며 살았는지, 내가 어떤 value 을 생각했는지
정리가 더 잘 된 것 같았다. 그리고 앞서 언급한대로 이를 통해 <code>내가 모르던 부족함</code>을 찾기도 했다.</p>
<p>그리고 이번엔 회고 <code>공개를 생각</code>하고 <code>회고 모임에 참여한 것</code>도 이전과 다른 <code>두번째 차이</code> 였다. 
그러다 보니 다른 사람들이 보는 것을 생각하고 여러번 검수하면서 쓸데없는 말들이 줄었다.
그리고 검수하고 수정하면서, 나를 좀 더 깊게 분석하고 목표를 더욱 곱씹게 된 것 같다.</p>
<p>지금에서도 좋았는데 <code>이번 차이</code>로 인해 2022년의 내가 긍정적으로 발전했다면
<code>느낀 점</code>들을 먼저 정리하고, <code>같은 목표를 가진 사람들</code>과 같이 <code>보일 수 있는 형태</code>로 
회고를 <code>100%</code> <del>풀 파워</del> 로 작성하지 않을까 싶다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[의식의 흐름으로 "선언형", "선언형 UI" 개념 재정리하기]]></title>
            <link>https://velog.io/@ricky_0_k/%EC%9D%98%EC%8B%9D%EC%9D%98-%ED%9D%90%EB%A6%84%EC%9C%BC%EB%A1%9C-%EC%84%A0%EC%96%B8%ED%98%95-%EC%84%A0%EC%96%B8%ED%98%95-UI-%EA%B0%9C%EB%85%90-%EC%9E%AC%EC%A0%95%EB%A6%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ricky_0_k/%EC%9D%98%EC%8B%9D%EC%9D%98-%ED%9D%90%EB%A6%84%EC%9C%BC%EB%A1%9C-%EC%84%A0%EC%96%B8%ED%98%95-%EC%84%A0%EC%96%B8%ED%98%95-UI-%EA%B0%9C%EB%85%90-%EC%9E%AC%EC%A0%95%EB%A6%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 28 Dec 2021 10:27:55 GMT</pubDate>
            <description><![CDATA[<p>난 예전에 선언적 UI 패턴을 이렇게 이야기한 적이 있었다.</p>
<blockquote>
<p>선언적 UI 패턴은 개발자가 화면 구성을 확인하는 데 <code>직관성을 제공</code>하는 것은 물론이고, 
<code>오로지 View 의 상태에 대해서만 프로그래밍 할 수 있도록</code> 돕습니다.</p>
</blockquote>
<p>2년전에 이야기한 내용이었고, 지금도 처음 보는 사람에게 이렇게 설명이 가능할지 고민이 생겼다.</p>
<p>그리고 고민한 결과 아래의 아쉬운 점이 느껴졌다.</p>
<ol>
<li><p>직관적이라는 게 <code>what</code> 을 이야기하는 것 같은데 확 와닿지 않는 느낌이었다.</p>
</li>
<li><p>일부 잘못 해석될 수 있는 내용도 있었다. <code>오로지 View 상태에 대해서만 프로그래밍 할 수 있다</code>는 걸 보니, 
당시의 나는 한 곳에서 해당 View 에 대해 프로그래밍할 수 있다는 걸 이야기하고 싶었던 것 같다.</p>
<p>그런 차원에서 보면 <code>오로지 한 포인트에서 View 의 상태에 대해 프로그래밍 할 수 있도록</code> 이 더 어울리는 말이었다.</p>
</li>
</ol>
<p>그 외에도 나사가 부분부분 빠져 보였어서 다시 정리가 필요해보였다.</p>
<p>선언형, 선언형 UI 개념을 내 기억에 정리하면서 
임펙트 있고 길게 기억하기 위해 이 포스트를 작성하게 되었다.</p>
<h1 id="선언형">선언형?</h1>
<p>선언형을 처음 듣는 사람에게 이걸 설명하려면 어떻게 해야할지부터 고민해보았다.</p>
<h2 id="1-위키-보기">1. 위키 보기</h2>
<p>일단은 <a href="https://ko.wikipedia.org/wiki/%EC%84%A0%EC%96%B8%ED%98%95_%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D">진리의 위키백과(?)</a>를 참고했고 아래의 문구를 보았다.</p>
<blockquote>
<p>한 정의에 따르면, 프로그램이 어떤 방법으로 해야 하는지를 나타내기보다 무엇과 같은지를 설명하는 경우에 &quot;선언형&quot;이라고 한다. 예를 들어, 웹 페이지는 선언형인데 웹페이지는 제목, 글꼴, 본문, 그림과 같이 <code>&quot;무엇&quot;이 나타나야하는지를 묘사</code>하는 것이지 &quot;어떤 방법으로&quot; 컴퓨터 화면에 페이지를 나타내야 하는지를 묘사하는 것이 아니기 때문이다. 이것은 전통적인 포트란과 C, 자바와 같은 명령형 프로그래밍 언어와는 다른 접근방식인데, 명령형 프로그래밍 언어는 프로그래머가 실행될 알고리즘을 명시해주어야 하는 것이다. 간단히 말하여, 명령형 프로그램은 알고리즘을 명시하고 목표는 명시하지 않는 데 반해 선언형 프로그램은 <code>목표를 명시하고 알고리즘을 명시하지 않는 것</code>이다.</p>
</blockquote>
<p>별로 글자도 안 봤는데 벌써부터 머리가 아파 핵심으로 보이는 내용을 블록 처리해 정리해보았다.</p>
<ol>
<li><p><code>&quot;무엇&quot;이 나타나야하는지를 묘사</code></p>
<p>이 내용은 제법 봤었다. 
어떻게(how) 대신 무엇을(what)을 이야기하는 게 <code>선언형</code> 이라는 글을 많이 보았었다.</p>
</li>
<li><p><code>목표를 명시하고 알고리즘을 명시하지 않는 것</code> </p>
<p><code>what</code>의 연장선의 내용이라 생각이 들었다.
알고리즘이라는 것이 과정이라 생각하기 때문에 맞는 말이라 생각이 들었다.</p>
</li>
</ol>
<h2 id="2-사례-보기">2. 사례 보기</h2>
<p>어떤 차이가 있는지는 얼추 이해했고 사례도 보고 싶었다.</p>
<h3 id="1-flutter-공식-내용-사례">1. Flutter 공식 내용 사례</h3>
<p>이런 갈증에 맞춰 <a href="https://docs.flutter.dev/get-started/flutter-for/declarative">Flutter 공식 문서 내용</a>에 사례가 있었고 제법 도움이 되었다.
사례 내용은 아래와 같다.
<img src="https://images.velog.io/images/ricky_0_k/post/11234bdd-6f2d-43a1-ad69-bb7ea9023afd/image.png" alt=""></p>
<p>난 개인적으로 이 사례가 공감이 많이 되었다. 
실제 <a href="https://medium.com/@kimdohun0104/%EC%82%AC%EB%9E%8C%EB%93%A4%EC%9D%80-%EC%99%9C-%EC%84%A0%EC%96%B8%ED%98%95-ui%EC%97%90-%EC%97%B4%EA%B4%91%ED%95%A0%EA%B9%8C-1440d03f4e49">타 블로그</a>에서도 이 사례를 언급하기도 한다.</p>
<p>실제 동작의 차원에서 위 코드를 바라보며, 블록친 1,2번을 보충설명하면 아래와 같이 정리가 가능하다.</p>
<ol>
<li><p><code>&quot;무엇&quot;이 나타나야하는지를 묘사</code>
위의 코드(명령형)은 b가 어떻게 만들어지는지를 이야기하지만
아래의 코드(선언형)은 ViewB 가 <code>무엇</code> 인지를 설명한다. </p>
</li>
<li><p><code>목표를 명시하고 알고리즘을 명시하지 않는 것</code> 
위의 방식(명령형)은 b가 만들어지는 <code>일련의 과정이 명시</code>되어 있다.
아래의 방식(선언형)은 ViewB 를 설명하는 <code>목표</code>에만 치중하고 있다.</p>
</li>
</ol>
<h3 id="2-집-주소-설명-사례">2. 집 주소 설명 사례</h3>
<p>그 외에 <a href="https://codechaser.tistory.com/81">집주소를 설명하는 방식을 다룬 사례</a>도 있었는데 그 사례도 제법 괜찮았다. 
이 사례를 좀 내 마음대로 만들어보았다.</p>
<ol>
<li><p>명령형 방식
사거리에서 길 건너 300m 직진 후 좌회전하고, 
150m 쯤 가서 우회전하고, 
10m 쯤 직진해 수입맥주 4캔을 사고(..?), 
50m 더 가서 빨간집 대문 앞으로 가면 우리집에 도착합니다.</p>
</li>
<li><p>선언형 방식
xx시 xx구 xx길 xxxx 입니다.</p>
</li>
</ol>
<p>두 가지 방식 모두 우리 집에 대해 이야기하지만, 이야기하는 방식과 목표는 다르다.</p>
<h2 id="3-정리">3. 정리</h2>
<p>공식문서들과 타 블로그들을 보고나서 난 선언형에 대해 이렇게 다시 정리했다.</p>
<blockquote>
<p>선언형은 <code>무엇</code> 에 대한 이야기를 주로 다루며, 
과정 나열보다는 이게 무엇인지를 한 줄에 설명하려 노력한다.</p>
</blockquote>
<h1 id="선언형-ui-패턴">선언형 UI 패턴</h1>
<p>자 이제 본론까지 왔다. <code>선언형 UI 에 대해 설명해보세요</code> 라고 하면 난 어떻게 말할 수 있을까?</p>
<p>&quot;요즘 개발자들이 즐겨보는 UI 패턴이라고 하며, 크게는 열광도 한다고 한다.&quot; </p>
<p>만약 내가 위와 같이 결론과 상황만 이야기하면, 지식수준에 대해 재평가 또는 매장(?)을 당할수도 있을 거라 생각된다.</p>
<p>난 아직 묻히고 싶지 않고, 재평가도 당하고 싶지 않기 때문에 
&quot;이게 무엇무엇이고, 타 패턴에 비해 두드러지는 장점과 단점은 무엇이다.&quot; 
를 이야기할 수 있을 정도로만 다뤄보려 한다.</p>
<h2 id="무엇">무엇?</h2>
<p>다시 공부할겸 일부 블로그들을 검색해보았다.</p>
<ol>
<li><p>첫 번째 정의에 입각해보기
나는 앞서 선언형이 무엇인지를 분석했었다. 
이를 상기하여 나는 아래와 같이 말을 만들어보았다.</p>
<blockquote>
<p>과정보다는 <code>무엇</code> 에 대한 이야기를 다루는 UI 패턴</p>
</blockquote>
<p>평가 : 별로..
적고나서 보니 너무 이론적이다. 내가 보기엔 별로인 것 같다.
&quot;치킨은 닭으로 만들어지고, 닭으로는 치킨을 만든다.&quot; 같이 의미없는 말의 집합체 같아 보인다.
선언형의 다른 특징도 확인해보아야 겠다.</p>
</li>
<li><p>두 번째 정의에 입각해보기
두 번째 정의를 활용해 말을 다시 만들어보았다.</p>
<blockquote>
<p>과정 나열보다는 이게 무슨 UI 인지를 한 줄에 설명하려 노력하는 패턴</p>
</blockquote>
<p>평가 : SoSo
그나마 좀 나아보인다. 실제 Flutter 공식 문서 사례와도 일부 매치되기도 한다. 
말을 좀더 다듬어 봐야겠다.</p>
</li>
<li><p>다듬어 보기
<code>비교</code>는 의미 없으니 빼고, <code>설명하려 노력하는</code> 도 최대한 줄여 써봐야겠다.</p>
<blockquote>
<p>이게 무슨 UI 인지 한 번에 알 수 있는 패턴이다.</p>
</blockquote>
<p>평가 : 정의는 나름 된 듯
한 문장으로 표현은 된 것 같다.</p>
</li>
<li><p>만드는 방식도 덧붙이고 멋드러지게 만들어보기
한 문장으로 표현을 최대한 했지만 뭔가 아쉽긴 하다.
만드는 방식도 좀 언급하고, 멋드러지게 표현하면 좋을 것 같다는 생각이 들었다.</p>
<blockquote>
<p>F(State) = View.</p>
</blockquote>
<p>검색해보니 만드는 방식을 공식으로 표현한 게 있었다.
공식에 대한 설명도 추가해야겠다고 느꼈다.</p>
<blockquote>
<p>F(State) = View. 즉 상태를 가지는 함수를 호출하는 방식으로 UI 를 만드는 형태이다.</p>
</blockquote>
<p>평가 : 나름 Ok
확실히 어떤 방식으로 만드는지도 설명되니 좋은 것 같다.
자 그러면 위에서 언급했던 사례도 덧붙여 최종 정리를 해봐야겠다.</p>
</li>
<li><p>최종 정리</p>
<blockquote>
<p>이게 무슨 UI 인지 한 번에 알 수 있는 패턴이다.
F(State) = View. 즉 상태를 가지는 함수를 호출하는 방식으로 UI 를 만드는 형태이다.
예를 들면 명령형 형태로 주소는 .......</p>
</blockquote>
<p>평가 : 매장은 면할 수 있겠다.
이 정도면 <code>무엇</code>인지와 더불어, 어떤 방식으로 UI 를 만드는지도 표현이 가능해보인다.
사례도 더해졌으니 나름 공부는 했다고 평가받지 않을까 싶다.</p>
</li>
</ol>
<h2 id="장점">장점</h2>
<p>선언형 UI 패턴을 통해 얻는 장점은 뭘까? 
난 먼저 정의를 다시 한 번 돌아보았다.</p>
<h3 id="1-이게-무슨-ui-인지-한-번에-알-수-있는-패턴이다-직관적">1. 이게 무슨 UI 인지 한 번에 알 수 있는 패턴이다. (직관적)</h3>
<p>정의대로 이게 무슨 UI 인지 직관적으로 알 수 있는 패턴이다.
실제 개발 사례를 생각해보아야겠다.</p>
<ol>
<li><p>안드로이드 네이티브에서 가장 일반적인 UI 를 만드는 과정</p>
<p>일단 제일 크리티컬 한 건 <code>관리포인트가 여러개</code> 인거다. </p>
<p>물론 Kotlin 으로만 UI 를 작성 할 수도 있겠지만 그건 입코딩이다.
보통은 xml 과 Kotlin 을 같이 작성하는 게 편하다.</p>
<p>그 이외에도 문자열이나 색상은 <code>strings.xml</code>, <code>colors.xml</code> 에 설정해야하고, 
경우에 따라 <code>theme.xml</code> 이나 <code>styles.xml</code> 에 추가 작업을 해야하고 등등..... </p>
<p>고려할 게 많다.</p>
</li>
<li><p>안드로이드 네이티브의 선언형 UI 사례 (Jetpack Compose, Flutter 등)</p>
<p>xml, Kotlin 으로 화면 코드가 분리하는 건 선언형 개념에 위배된 내용일 것이다.
선언형 UI 는 무슨 UI 인지를 <code>한번에 알아야한다</code> 고 했다.
그런데 화면에 대한 내용이 분리가 되어있다고 하면 원론적인 차원에서 <code>모순</code>일테니 말이다.</p>
<p>그럼 선언형 UI 의 개념을 따르는 사례가 있을까?
위에서 언급한 Jetpack Compose, Flutter 이다.</p>
<p>Jetpack Compose 는 <code>Kotlin</code> 으로만 작성하고, Flutter 는 <code>Dart</code> 로만 UI를 작성한다.
물론 여기에서도 문자열 등 리소스는 따로 관리해야 하지만, 
애니메이션이나 추가 설정은 코드 차원에서 처리가 가능하다.</p>
<p>한 곳에서 UI 에 대한 정보를 다 알 수 있으니 첫 번째 장점 설명은 이걸로 끝!</p>
</li>
</ol>
<blockquote>
<p>정리</p>
<p>하나의 포인트에서 화면 구성, 이벤트 내용, 애니메이션 등 UI 의 모든 것을 알 수 있다.
이로 인해 UI 수정이 필요할 때, 해당 UI 에 직관적인 접근이 가능해진다. </p>
</blockquote>
<h3 id="2-재사용성-효율성">2. 재사용성 (효율성)</h3>
<p>왜 효율적인지를 파악하기 위해, 위의 <code>일반 UI 사례</code>와 <code>선언형 UI 사례</code>를 다시 가져와 보았다.</p>
<ol>
<li><p>안드로이드 네이티브에서 가장 일반적인 UI 사례</p>
<p>xml 과 Kotlin 을 같이 사용하는 경우,
xml 은 보통 화면단을, Kotlin 은 일부 로직 쪽을 담당한다.</p>
<p>여기서 재사용성 관련하여 질문을 하나 던져본다.</p>
<ol>
<li>오로지 화면(<code>xml</code>) 만 따로 재사용할 수 있을까? </li>
<li>아니면 일부 로직(<code>Kotlin</code>) 만 따로 재사용할 수 있을까?</li>
<li>아니면 그 둘(<code>xml</code>, <code>Kotlin</code>)을 모두 재사용할 수 있을까?</li>
</ol>
<p>3개 모두 가능하다. 
둘을 모두 재사용하는 경우는, Dialog 나 CustomView 등의 사례로 보면 알 수 있다.
다만 이 경우에는 <code>xml</code>, <code>Kotlin</code> 영역을 동시에 고려하면서 작업이 필요할 것 같다.</p>
</li>
<li><p>안드로이드 네이티브의 선언형 UI 사례
<strong>1. Jetpack Compose</strong>
Jetpack Compose 는 그냥 UI 를 호출만 해주면 된다.
예를 들면 아래와 같이 말이다.</p>
<pre><code class="language-kotlin">override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)

   setContent {
      GraduationView(viewModel)  // UI 호출을 통한 생성
   }
}</code></pre>
<p><code>GraduationView</code> 하나만 만들고 매개변수를 고려해서 
어느 곳에서든 호출하게 만들면 쉽게 재사용이 가능하다.</p>
<p>Kotlin, xml 의 경우 재사용 가능하도록 만들어주는 <code>인위적 작업이 필요</code>했다면
Jetpack Compose 에서는 <code>기본적으로 제공해주는 기능</code>이다.</p>
<p><strong>2. Flutter</strong> 
Flutter 는 함수형태로 반환도 가능하다. 
이런식으로 만들고 이전 방식과 선언형 방식을 혼용할수도 있다. 
아래는 선언형 방식만을 사용해본 예이다.</p>
<pre><code class="language-dart">class LoadingWidget extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
      return const Center(
         child: CircularProgressIndicator(),
      );
   }
}</code></pre>
</li>
</ol>
<p>이렇게 2가지 사례를 보았다.</p>
<p>물론 개발자의 역량에 따라 달라질 순 있다. 
하지만 선언형 UI 를 사용하면 재사용성 차원에서 아래 이득을 볼 수 있을 것 같다.</p>
<ol>
<li>인위적 작업 없이 <code>기본 호출</code>만으로 재사용을 할 수 있다</li>
<li>하나의 영역만 바라보고 관리가 가능하므로 (2 -&gt; 1)
유지보수 작업 효율성에도 조금 더 나은 느낌이다. </li>
</ol>
<blockquote>
<p>정리</p>
<p>선언형 UI 패턴은 개발자의 인위적 작업 없이 <code>기본 호출만으로 UI 를 재사용</code>할 수 있다.
UI 를 수정하려면 하나의 영역만 확인 후 수정하면 되니, 작업 효율성도 올라가는 느낌이다.</p>
</blockquote>
<h3 id="3-단방향-흐름으로-인한-sideeffect-제거">3. 단방향 흐름으로 인한 SideEffect 제거</h3>
<p>이건 Jetpack Compose 에서 더욱 체감할 수 있는 요소이다.
JetPack Compose 에서 UI 는 <code>Composable 어노테이션을 가지는 함수</code> 를 통해 만들 수 있다.</p>
<pre><code class="language-kotlin">@Composable
fun GraduateView(){
    // ...
    Column() {
        GraduateTextView(),
        GraduateTextView()
    }
}

@Composable
fun GraduateTextView(){
    // ...
}</code></pre>
<p>위와 같이 Composable 함수를 선언하고 조합하여 커스텀 UI 를 만들 수 있다.
(후술하겠지만 Column 도 Composable 함수이다)</p>
<p>여기서 주목할 내용은 <code>Composable 함수들은 전부 반환값이 없다</code>.
이건 단방향의 중요한 단추이다. 실제 아래 예시를 보자.</p>
<pre><code class="language-kotlin">@Composable
inline fun Column(
    modifier: Modifier = Modifier,
    verticalArrangement: Arrangement.Vertical = Arrangement.Top,
    horizontalAlignment: Alignment.Horizontal = Alignment.Start,
    content: @Composable ColumnScope.() -&gt; Unit
) {
    val measurePolicy = columnMeasurePolicy(verticalArrangement, horizontalAlignment)
    Layout(
        content = { ColumnScopeInstance.content() },
        measurePolicy = measurePolicy,
        modifier = modifier
    )
}</code></pre>
<p>이 코드는 위에서 썼던 Column 의 정의 내용이다.
안드로이드 UI 에서 orientation 을 <code>vertial</code> 로 갖는 <code>LinearLayout</code> 을 생각하면 된다. 
(= 수직 정렬 레이아웃)
실제 이 코드는 보다시피 반환값이 없다. 다른 내장 UI 함수(Text, TextField 등)도 마찬가지이다.</p>
<p>반환값이 없다는 건 아래 내용을 의미한다.</p>
<ol>
<li><p>UI 를 호출하는 영역(ex. <code>Activity</code>)에서, UI 내의 로직(ex. <code>Composable 함수 내부</code>)에 직접 접근할 수 없다.
ex. <code>Activity</code> 위치에서 호출한 Compose UI 내의 로직에 접근할 수 없다.</p>
</li>
<li><p>UI 내(ex. <code>Composable 함수 내부</code>)에서, UI 를 호출하는 영역(ex. <code>Activity</code>) 의 로직을 직접 실행할 수 없다.</p>
<p>물론 인위적으로 매개변수를 주입하여 접근할수록 만들 순 있다</p>
<p>ex. Compose UI 에서 호출한 Activity 의 로직에 접근할 수 없다. </p>
<pre><code>단 매개변수를 통해 간접적인 접근은 가능할 수는 있다 
(ex. Activity 또는 ViewModel 을 매개변수로 주입)</code></pre></li>
</ol>
<p>정리하면 <code>호출한 영역 -&gt; 호출된 영역</code>으로 향하는 단방향 흐름으로 설명될 수 있다.</p>
<h4 id="단방향-흐름의-장점">단방향 흐름의 장점</h4>
<p>만약 양방향 흐름으로 서로와 맞물려 있는 로직을 수정할 경우, 각 파일들을 왔다갔다하면서 수정해야한다.
깊게 들어가면 클린 아키텍처에서 나쁜 사례로 언급했던 <code>순환의 지옥</code>을 맛볼 수 있다.</p>
<p>단방향에서는 서로 맞물리는 케이스가 비교적 적다. </p>
<p>이 덕분에 단방향 흐름에서는 특정 UI 를 수정해야 할 시, 해당 UI 코드만 신경쓰고 수정하면 된다.</p>
<p>만약 호출부에서 넘겨주는 매개변수로 서로 맞물리는 케이스가 있을 경우에는
넘겨주는 값과 로직이 제대로 UI 코드에 반영되는지만 신경쓰면 된다.</p>
<p>참고 : 위에서 말했듯이 Jetpack Compose 는 특히 단방향적 성향이 강해 (반환값이 없음)
아키텍처 상에서 MVP 구조에서는 사용이 힘들고, MVVM 구조에서는 사용이 용이한 편이다.</p>
<blockquote>
<p>정리</p>
<p>선언형 UI 패턴은 대체로 단방향 흐름의 성향이 강하다.
이럴 경우 호출된 영역과, 호출한 영역이 서로의 로직을 침범할 수 있는 방법이 없다.</p>
<p>이는 각 영역의 독립성을 높여주어, 서로를 참조하면서 발생되는 SideEffect 차단에 도움이 된다.</p>
</blockquote>
<h2 id="단점">단점</h2>
<p>특징에서부터 단점을 생각해보려 한다. 
한 곳에서 UI 의 모든 것을 볼 수 있는 선언형 UI 에서 비롯되는 단점은 무엇일까?</p>
<h3 id="1-거대화된-단위-코드">1. 거대화된 단위 코드</h3>
<p>신경써야 할 내용이 하나로 합쳐진다고, 신경쓰는 개수까지 줄어드는 건 아니다.
모든 내용이 한 파일에 있어 UI 하나가 거대한 코드 단위일 것이다.</p>
<p>한 곳에서 모든 걸 설명하지만 그만큼의 코드는 많아지기에 
Trade Off 현상 (= 얻는 게 있으면 잃는 것도 생기는 현상) 으로 안고가야 할 내용이다.</p>
<blockquote>
<p>정리</p>
<p>한 곳에서 UI 의 모든 것들이 정의되어 있기에 거대화 되어 있는 코드를 보게 된다.</p>
</blockquote>
<h3 id="2-재사용성의-어두운-면">2. 재사용성의 어두운 면</h3>
<p>위에서 장점으로 재사용성(효율성)을 이야기했다.
하지만 이는 장점이면서도, 그만큼 개발자가 <code>재사용성이 높게 만들어야 하는 것</code>이다.</p>
<p>만약 개발자가 재사용 없이, 각 화면마다 일일히 UI 를 선언해준다면 어떨까?
동일한 기능의 코드가 산발적으로 퍼져 오히려 코드 라인이 획기적으로 늘어난 것을 확인할 수 있다.</p>
<p>그렇다고 무조건 중복 코드를 합치는 게 능사는 아니다.
한 곳에 두고 온갖 조건문을 두는 것보다 차라리 분리시키는 게 더 나을수도 있다.</p>
<p>이런 판단이 필요하기에 많은 경험을 통해 
본인만의 재사용성 활용 스킬을 높이는 게 필요하다고 생각한다.</p>
<blockquote>
<p>정리</p>
<p>재사용성은 개발자가 얼마나 활용하느냐에 따라 잘 쓰일 수 있다.
이에 대한 경험 없이 마구잡이로 사용하면 오히려 중복 코드가 많아져 유지보수가 힘들어질 수 있다.</p>
</blockquote>
<h1 id="선언형은-명령형의-추상화">선언형은 명령형의 추상화</h1>
<p>선언형은 명령형 프로그래밍을 추상화했다고도 이야기한다.
사실이다. 실제 아래의 코드도 선언형 프로그래밍의 예 중 하나이다.</p>
<pre><code class="language-javascript">function declarative(arr) {
    return arr.reduce((acc,v)=&gt;v+acc,0);
}</code></pre>
<p>선언형 프로그래밍을 활용해 배열내의 값들을 모아 합을 구하는 함수로써
코틀린에서도 이런 선언형의 조합을 <code>chaining 형태</code>로 계속 이어 사용하기도 한다.</p>
<pre><code class="language-kotlin">listOf(1,2,3,4,5).filter { it &lt; 3 }.first()</code></pre>
<p>위의 예도 선언형 프로그래밍이라 볼 수 있고 
<code>filter()</code>, <code>first()</code> 가 어떻게 구현되어 있는지 확인하면 
명령형으로 코드가 작성되어 있는 걸 확인할 수 있다.</p>
<p>과정 없이 단순 호출만을 하기 때문에 이런 프로그래밍 방식을 싫어하는 사람들도 있을꺼라 생각이 든다.
하지만 개인적으로는 <code>나의 코드를 상대방이 직관적으로 보고 이해하는 게 더 중요하다고 생각하기 때문에</code>
그런 차원에서 가독성에 큰 도움을 주는 선언형 프로그래밍에 긍정적이다. </p>
<h1 id="결론">결론</h1>
<p>의식의 흐름대로 정리하면서 내 머리속에 선언형 프로그래밍과 선언형 UI 에 대해 다시 정리할 수 있었다.
다시 선언형을 공부하면서 앞으로 대세가 될 거라는 내용에 굉장히 공감했다.</p>
<p>하지만 단점에서 언급한 <code>재사용성의 이면</code> 과 <code>명령형을 추상화</code> 때문에 개인적으로 우려되는 점도 있다.</p>
<p>어디까지 재활용 범위를 둘 것인지는 오롯이 개발자의 몫이기에 
그 경험치에 따라 <code>최고의 코드</code> 또는 <code>안티 패턴의 코드</code>가 될 수 있다고 본다.
시행착오를 겪으면서 많이 접해보고 자신만의 방식을 찾아내야 할 것으로 생각한다.</p>
<p>그리고 명령형이 추상화 되어 있다고 그 로직을 알 필요가 없다는 말은 아니다. 
추상화된 명령형 로직이 어떻게 동작하는지 확인해야 예상치 못한 성능 이슈를 해소할 수 있다.
편하다고 막 사용했다가 서비스에 치명상을 줄 수 있기 때문에
이 역시 시행착오를 겪으면서 해당 선언형 함수가 어떻게 동작하는지도 숙지할 필요가 있다고 생각한다.</p>
<p>선언형 UI 는 개발자의 노력에 따라 숙련도가 늘어나는 영역이라 생각이 들었다.</p>
<p>많이 사용하고 자신만의 깨달음을 얻는다면 <code>요즘 개발자들이 즐겨보는 UI 패턴</code> 을 
능수능란하게 사용할 수 있지 않을까 생각하면서 이 포스트를 마무리한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Github Actions 으로 Android CI/CD 구축한 썰 만들어보기 [4. CI - 이론 및 예시]]]></title>
            <link>https://velog.io/@ricky_0_k/Github-Actions-%EC%9C%BC%EB%A1%9C-Android-CICD-%EA%B5%AC%EC%B6%95%ED%95%9C-%EC%8D%B0-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EA%B8%B0-4.-CI-%EC%9D%B4%EB%A1%A0-%EC%98%88%EC%8B%9C</link>
            <guid>https://velog.io/@ricky_0_k/Github-Actions-%EC%9C%BC%EB%A1%9C-Android-CICD-%EA%B5%AC%EC%B6%95%ED%95%9C-%EC%8D%B0-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EA%B8%B0-4.-CI-%EC%9D%B4%EB%A1%A0-%EC%98%88%EC%8B%9C</guid>
            <pubDate>Sun, 19 Dec 2021 09:32:01 GMT</pubDate>
            <description><![CDATA[<p>무엇이든 직접 해봐야 이해가 될 것이라 생각이 든다. 이제 CI 를 직접 구축해보려 한다.
여기서는 아래 내용들을 순차적으로 다뤄볼 예정이다.</p>
<ol>
<li><p>먼저 예시에 언급되었던 명령어인 <code>./gradlew build</code> 의 한계를 분석해보려 한다.</p>
</li>
<li><p>맨 처음에 CI 사용 목적에 대해 간단히 이야기했을 때 아래 3가지를 이야기했었다.</p>
<blockquote>
<ol>
<li>배포할 브랜치에서 <strong>프로젝트 빌드가 잘 되는지</strong>,</li>
<li>컨벤션 및 <strong>코드스타일 규칙을 지켰는지</strong></li>
<li>로직 오류 검증을 위한 <strong>테스트 코드를 실행하고 결과를 확인</strong>해주는지</li>
</ol>
</blockquote>
<p>1~3번의 경우 어떤 명령어들을 활용할 수 있는지, 내가 했던 경험에 기반하여 간단한 예시코드도 언급하려 한다. 
2번의 경우 추가로 <code>코드스타일 관련 도구들</code>도 짤막하게 언급할 예정이다.</p>
</li>
</ol>
<h1 id="1-gradlew-build-명령어-깊게-바라보기">1. gradlew build 명령어 깊게 바라보기</h1>
<p>우리는 이전에 <code>./gradlew build</code> 를 통해 빌드가 잘 되는지를 간접적으로 확인할 수 있었다.
<a href="https://github.com/riflockle7/github-actions-example/runs/4278796180?check_suite_focus=true">실제 android sdk version 으로 인해 빌드 실패</a>를 확인 후 수정하여 프로젝트의 영속성을 유지시킬 수 있었다.</p>
<p>그런데 이 명령어에 대해 자세하게 알 필요가 있다.
아래 내용을 통해 <code>gradlew build</code> 명령어에 대해 깊게 분석해보자.</p>
<h2 id="1-gradlew-build-명령어의-이면">1. gradlew build 명령어의 이면</h2>
<p>먼저 뭘 실행하는지를 알아보자.
<code>./gradlew build</code> 를 실행하게 되면 아래의 task 들이 실행된다. </p>
<pre><code>&gt; Task :app:preBuild UP-TO-DATE
&gt; Task :app:preDebugBuild UP-TO-DATE
&gt; Task :app:compileDebugAidl NO-SOURCE
&gt; Task :app:compileDebugRenderscript NO-SOURCE
&gt; Task :app:generateDebugBuildConfig UP-TO-DATE
&gt; Task :app:checkDebugAarMetadata UP-TO-DATE
&gt; Task :app:generateDebugResValues UP-TO-DATE
&gt; Task :app:generateDebugResources UP-TO-DATE
&gt; Task :app:mergeDebugResources UP-TO-DATE
&gt; Task :app:createDebugCompatibleScreenManifests UP-TO-DATE
&gt; Task :app:extractDeepLinksDebug UP-TO-DATE
&gt; Task :app:processDebugMainManifest UP-TO-DATE
&gt; Task :app:processDebugManifest UP-TO-DATE
&gt; Task :app:processDebugManifestForPackage UP-TO-DATE
&gt; Task :app:processDebugResources UP-TO-DATE
&gt; Task :app:compileDebugKotlin UP-TO-DATE
&gt; Task :app:javaPreCompileDebug UP-TO-DATE
&gt; Task :app:compileDebugJavaWithJavac UP-TO-DATE
&gt; Task :app:compileDebugSources UP-TO-DATE
&gt; Task :app:mergeDebugNativeDebugMetadata NO-SOURCE
&gt; Task :app:mergeDebugShaders UP-TO-DATE
&gt; Task :app:compileDebugShaders NO-SOURCE
&gt; Task :app:generateDebugAssets UP-TO-DATE
&gt; Task :app:mergeDebugAssets UP-TO-DATE
&gt; Task :app:compressDebugAssets UP-TO-DATE
&gt; Task :app:processDebugJavaRes NO-SOURCE
&gt; Task :app:mergeDebugJavaResource UP-TO-DATE
&gt; Task :app:checkDebugDuplicateClasses UP-TO-DATE
&gt; Task :app:desugarDebugFileDependencies UP-TO-DATE
&gt; Task :app:mergeExtDexDebug UP-TO-DATE
&gt; Task :app:mergeLibDexDebug UP-TO-DATE
&gt; Task :app:dexBuilderDebug
&gt; Task :app:mergeProjectDexDebug UP-TO-DATE
&gt; Task :app:mergeDebugJniLibFolders UP-TO-DATE
&gt; Task :app:mergeDebugNativeLibs NO-SOURCE
&gt; Task :app:stripDebugDebugSymbols NO-SOURCE
&gt; Task :app:validateSigningDebug UP-TO-DATE
&gt; Task :app:writeDebugAppMetadata UP-TO-DATE
&gt; Task :app:writeDebugSigningConfigVersions UP-TO-DATE
&gt; Task :app:preReleaseBuild UP-TO-DATE
&gt; Task :app:compileReleaseAidl NO-SOURCE
&gt; Task :app:compileReleaseRenderscript NO-SOURCE
&gt; Task :app:generateReleaseBuildConfig UP-TO-DATE
&gt; Task :app:checkReleaseAarMetadata UP-TO-DATE
&gt; Task :app:generateReleaseResValues UP-TO-DATE
&gt; Task :app:generateReleaseResources UP-TO-DATE
&gt; Task :app:mergeReleaseResources UP-TO-DATE
&gt; Task :app:createReleaseCompatibleScreenManifests UP-TO-DATE
&gt; Task :app:extractDeepLinksRelease UP-TO-DATE
&gt; Task :app:processReleaseMainManifest UP-TO-DATE
&gt; Task :app:processReleaseManifest UP-TO-DATE
&gt; Task :app:processReleaseManifestForPackage UP-TO-DATE
&gt; Task :app:processReleaseResources UP-TO-DATE
&gt; Task :app:compileReleaseKotlin UP-TO-DATE
&gt; Task :app:javaPreCompileRelease UP-TO-DATE
&gt; Task :app:writeReleaseApplicationId UP-TO-DATE
&gt; Task :app:analyticsRecordingRelease
&gt; Task :app:compileReleaseJavaWithJavac UP-TO-DATE
&gt; Task :app:compileReleaseSources UP-TO-DATE
&gt; Task :app:extractProguardFiles UP-TO-DATE
&gt; Task :app:bundleReleaseClasses UP-TO-DATE
&gt; Task :app:mergeReleaseJniLibFolders UP-TO-DATE
&gt; Task :app:mergeReleaseNativeLibs NO-SOURCE
&gt; Task :app:stripReleaseDebugSymbols NO-SOURCE
&gt; Task :app:extractReleaseNativeSymbolTables NO-SOURCE
&gt; Task :app:mergeReleaseNativeDebugMetadata NO-SOURCE
&gt; Task :app:checkReleaseDuplicateClasses UP-TO-DATE
&gt; Task :app:dexBuilderRelease
&gt; Task :app:desugarReleaseFileDependencies UP-TO-DATE
&gt; Task :app:mergeExtDexRelease UP-TO-DATE
&gt; Task :app:mergeDexRelease UP-TO-DATE
&gt; Task :app:mergeReleaseArtProfile UP-TO-DATE
&gt; Task :app:compileReleaseArtProfile UP-TO-DATE
&gt; Task :app:mergeReleaseShaders UP-TO-DATE
&gt; Task :app:compileReleaseShaders NO-SOURCE
&gt; Task :app:generateReleaseAssets UP-TO-DATE
&gt; Task :app:mergeReleaseAssets UP-TO-DATE
&gt; Task :app:compressReleaseAssets UP-TO-DATE
&gt; Task :app:processReleaseJavaRes NO-SOURCE
&gt; Task :app:mergeReleaseJavaResource UP-TO-DATE
&gt; Task :app:optimizeReleaseResources UP-TO-DATE
&gt; Task :app:collectReleaseDependencies UP-TO-DATE
&gt; Task :app:sdkReleaseDependencyData UP-TO-DATE
&gt; Task :app:writeReleaseAppMetadata UP-TO-DATE
&gt; Task :app:writeReleaseSigningConfigVersions UP-TO-DATE
&gt; Task :app:bundleDebugClasses UP-TO-DATE
&gt; Task :app:packageDebug
&gt; Task :app:assembleDebug
&gt; Task :app:compileDebugUnitTestKotlin UP-TO-DATE
&gt; Task :app:preDebugUnitTestBuild UP-TO-DATE
&gt; Task :app:javaPreCompileDebugUnitTest UP-TO-DATE
&gt; Task :app:compileDebugUnitTestJavaWithJavac NO-SOURCE
&gt; Task :app:processDebugUnitTestJavaRes NO-SOURCE
&gt; Task :app:lintVitalAnalyzeRelease
&gt; Task :app:testDebugUnitTest
&gt; Task :app:lintVitalRelease SKIPPED
&gt; Task :app:compileReleaseUnitTestKotlin UP-TO-DATE
&gt; Task :app:preReleaseUnitTestBuild UP-TO-DATE
&gt; Task :app:javaPreCompileReleaseUnitTest UP-TO-DATE
&gt; Task :app:compileReleaseUnitTestJavaWithJavac NO-SOURCE
&gt; Task :app:processReleaseUnitTestJavaRes NO-SOURCE
&gt; Task :app:testReleaseUnitTest
&gt; Task :app:test
&gt; Task :app:packageRelease
&gt; Task :app:assembleRelease
&gt; Task :app:assemble
&gt; Task :app:lintAnalyzeDebug</code></pre><p><code>정말 많지만</code> 필요한 task 들(unitTest, assembleRelease 등)도 눈에 보인다.
그래도 너무 많기에 좀 더 자세하게 들여다보면 우리는 일부 규칙성을 찾을 수 있다.</p>
<h3 id="1-규칙성-찾기">1. 규칙성 찾기</h3>
<p><code>variant</code> 종류들인 <strong>debug</strong> 와 <strong>release</strong> 로 분류하여 확인해보자.</p>
<p><img src="https://images.velog.io/images/ricky_0_k/post/0734f6ac-e2ad-4e1d-b33c-5a4043e2fd98/image.png" alt=""></p>
<p>맨 위에는 variant 이름에 겹치지 않은 task 들을 적은 것이고
그 아래로 <code>왼쪽엔 debug</code>, <code>오른쪽엔 release</code> 키워드로 정리한 내용이다.
<code>app:compile</code> 를 블록처리한 부분을 기준으로 좌우 차이를 찾아보면 우리는 아래 내용을 알 수 있다.</p>
<blockquote>
<p>build 명령어로 실행된 task 중 일부는, variant 만 다르고 명령어의 동작은 동일하구나.</p>
</blockquote>
<h3 id="2-실제-실행-가능한-task-개수로-분석하기">2. 실제 실행 가능한 Task 개수로 분석하기</h3>
<p>다음은 실행 가능한 task 개수를 기준으로 분석해보자. 
아래 내용을 참고해보면 gradlew build 명령어가 더 많은 task 를 실행하는 것을 확인할 수 있다.
<img src="https://images.velog.io/images/ricky_0_k/post/0ac1fce1-86bd-43c0-8770-4fbccefeeea3/image.png" alt=""></p>
<p>정확한 연산은 힘들지만 <code>26개</code>, <code>27개</code> 에 비해 
<code>73개</code> 라는 task 숫자가 보통 숫자는 아니라 느낄 수 있다.</p>
<h3 id="3-결론--gradlew-build-명령어가-무조건-정답은-아니다">3. 결론 : gradlew build 명령어가 무조건 정답은 아니다.</h3>
<p>위 내용들로 보아, 시간적 차원으로는 <code>./gradlew build</code> 가 정답이 아님을 알 수 있다.</p>
<p><code>variant 개수</code>만큼 <code>많은 task</code> 들을 실행하기 때문에 그 만큼 많은 시간이 걸린다.</p>
<p>그리고 지금은 테스트 코드도 없고, 코드의 규모도 작기에 속도의 차이가 안 느껴지는 것이지 
<code>프로젝트가 커질수록 시간도 늘어나기 때문에</code> 우리는 <code>./gradlew build</code> 를 대체해야 할 시기를 마주하게 될 수도 있다.</p>
<h1 id="2-첫-접근을-위한-예시">2. 첫 접근을 위한 예시</h1>
<p>위를 통해 gradlew build 명령어의 한계는 알 수 있었지만, 이 명령어가 CI 의 전부는 아니다.
막상 CI 에서 어떤 것이 필요할지, 그리고 gradlew build 명령어의 거대한 로직 중 어떤 것만을 뽑아 실행시킬지 정해야 한다.</p>
<p>개발자 자신이 마음대로 CI 를 만들 수 있는만큼 첫 접근을 위한 <code>사용 예시</code>가 필요하지 않을까하여, 
필자가 github actions CI 코드에서 사용했던 명령어들을 순차적으로 아래에 적어볼까한다.</p>
<h3 id="1-github-actions-내에서-얻을-수-있는-데이터-로깅">1. github actions 내에서 얻을 수 있는 데이터 로깅</h3>
<p>github actions 에서는 여러 값들이 내장 되어 있고 이를 통해 확인할 수 있는 내용들이 있다.
이에 대한 자세한 내용은 github actions 공식문서에서 확인할 수 있다.</p>
<p>때로는 어떤 event 인지에 따라 (ex. push, pull request) 볼 수 있는 내용이 달라질 때가 있다.
이에 필자는 브랜치, 태그 이름에 기반한 작업을 하거나, 디렉터리 확인 등을 위해 
아래와 같이 github actions 에서 볼 수 있는 모든 항목들에 대한 로깅을 했었다.</p>
<pre><code class="language-yaml"># 실행할 job 설정
jobs:
  # CI 작업
  integration:
    runs-on: ubuntu-18.04
    steps:
      # 로깅1. github context
      - name: 로깅1. GitHub context 확인
        env:
          GITHUB_CONTEXT: ${{ toJson(github) }}
        run: echo &quot;$GITHUB_CONTEXT&quot;
        if: always()

      # 로깅2. job context
      - name: 로깅2. job context 확인
        env:
          JOB_CONTEXT: ${{ toJson(job) }}
        run: echo &quot;$JOB_CONTEXT&quot;
        if: always()

      # 로깅3. steps context
      - name: 로깅3. steps context 확인
        env:
          STEPS_CONTEXT: ${{ toJson(steps) }}
        run: echo &quot;$STEPS_CONTEXT&quot;
        if: always()

      # 로깅4. runner context
      - name: 로깅4. runner context 확인
        env:
          RUNNER_CONTEXT: ${{ toJson(runner) }}
        run: echo &quot;$RUNNER_CONTEXT&quot;
        if: always()

      # 로깅5. strategy context
      - name: 로깅5. strategy context 확인
        env:
          STRATEGY_CONTEXT: ${{ toJson(strategy) }}
        run: echo &quot;$STRATEGY_CONTEXT&quot;
        if: always()

      # 로깅6. matrix context
      - name: 로깅6. matrix context 확인
        env:
          MATRIX_CONTEXT: ${{ toJson(matrix) }}
        run: echo &quot;$MATRIX_CONTEXT&quot;
        if: always()</code></pre>
<p>context 라고도 지칭하는 이 값들은 json 형태로 되어 있고, echo 를 통해 아래와 같이 확인이 가능하다.
전체 내용을 보여줄 수 없어 일부 내용만 캡처했다.
<img src="https://images.velog.io/images/ricky_0_k/post/08342ff9-8609-4b8e-9da8-c63087d04731/image.png" alt=""></p>
<p>이 내용으로 각 event 를 구분하고, 그에 따라 필요한 값들을 뽑아올지 확인하여 코드를 작성할 수 있다.</p>
<h3 id="2-현재-시간-가져오기-with-get-current-time-plugin">2. 현재 시간 가져오기 (with Get Current Time Plugin)</h3>
<p>이 내용은 CI 에 영향을 주진 않는 코드이다. 
하지만 <code>github actions 플러그인 활용법</code>과 관련된 이야기이기도 하여 사용 예만 언급하고 넘어가려 한다.</p>
<p>필자의 경우 CI 를 통해 테스트를 완료하거나, CD 를 통해 apk 를 배포할 때 
슬랙과 연동하여 결과를 슬랙에 전달하는 로직을 넣었었다.
이 때 CI/CD 를 시작한 시간을 가져오기 위해 <code>Get Current Time</code> 이라는 플러그인을 사용했다. 
<a href="https://github.com/marketplace/actions/get-current-time">이 링크</a>로 들어가면 관련 내용을 자세히 볼 수 있고, 안드로이드 의존성을 implement 하여 사용하는 듯한 느낌을 얻을 수 있을 것이다.</p>
<pre><code class="language-yaml">jobs:
  # CI 작업
  integration:
    runs-on: ubuntu-18.04
    steps:
      .
      .
      .
      # 현재 시간 설정 및 출력 내용 확인
      # [Get Current Time](https://github.com/marketplace/actions/get-current-time)
      # 1. 현재 시간 설정하기
      - name: 1. 현재 시간 설정하기
        uses: 1466587594/get-current-time@v1
        id: current-time
        with:
          format: YYYY.MM.DD_LT
          utcOffset: &quot;+09:00&quot;
        if: always()

      # 2. 현재 시간 내용 확인
      - name: 2. 현재 시간 내용 확인
        env:
          TIME: &quot;${{ steps.current-time.outputs.time }}&quot;
          F_TIME: &quot;${{ steps.current-time.outputs.formattedTime }}&quot;
        run: echo $TIME $F_TIME
        if: always()</code></pre>
<p>필자는 이런 식으로 현재 시간을 체크하였다. 
한번 위에 언급한 <code>플러그인 링크</code>를 직접 보고 작성해보는 것도 추천한다.</p>
<h3 id="3-압축-파일-해독">3. 압축 파일 해독</h3>
<p>github Repository 에 직접 올리기에 꺼림칙한 일부 파일이 있을 것이다. <code>google-service.json</code>, <code>kakao-strings.xml</code>, <code>키스토어 파일</code> 등이 그 예가 될 수 있다. (왜 꺼림칙한지는 따로 이야기하지 않겠다.)
보안을 필요로 하지만 빌드나 배포 등을 위해서는 반드시 repository 내에서 인식할 수 있어야 한다.</p>
<p>그러면 어떻게 해야할까? 필자는 <a href="https://sys09270883.github.io/ci/cd/78/">비밀번호를 통한 파일 압축 방법을 언급한 링크</a>를 통해 고민을 해소했다. </p>
<p>비밀번호의 경우에는 github 에서 제공하는 <code>SECRET</code> 를 활용했다. 
github 내에서는 <code>SECRETS</code> 라는 key-value 형태로 값을 저장할 수 있는 기능이 있다. 해당 기능을 활용하면 간단한 문자열을 저장할 수 있다. 아래는 실제 저장한 예이다.</p>
<p><img src="https://images.velog.io/images/ricky_0_k/post/7051dd14-c12c-4002-a6fe-b8eb238396a6/image.png" alt=""></p>
<p>주의할 점이 있다면 SECRETS 에 한 번 저장한 이후 그 값을 다시 확인할 수 있는 길은 없다. 
자기자신은 이 값을 기억하고 있어야 할 것이다.</p>
<p>위 내용들을 종합하여 필자는 아래와 같이 작성했었다.</p>
<pre><code class="language-yaml">jobs:
  # CI 작업
  integration:
    runs-on: ubuntu-18.04
    steps:
      .
      .
      .
      # 압축 파일 해독 [Github action에 .gitignore 파일 포함하기](https://sys09270883.github.io/ci/cd/78/)
      # 1. services.tar gpg 해독
      - name: 1. Decrypt services.tar
        run: gpg --quiet --batch --yes --always-trust --decrypt --passphrase=&quot;$SERVICE_TAR_PASSWORD&quot; --output services.tar services.tar.gpg
        env:
          SERVICE_TAR_PASSWORD: ${{ secrets.SERVICE_TAR_PASSWORD }}

      # 2. services.tar 압축 풀기 (지정되었던 경로로 자동으로 파일이 만들어짐)
      - name: 2. services.tar 압축 풀기
        run: tar xvf services.tar</code></pre>
<p>여러 개의 파일을 압축할 경우 각 파일이 그 위치를 기억하고 있다.
그러므로 일일히 하나씩 명령어를 작성할 필요는 없다. 
본인의 입맛에 맞춰 사용하면 좋을 듯하다.</p>
<h3 id="4-의존성-라이브러리-다운로드-및-확인">4. 의존성 라이브러리 다운로드 및 확인</h3>
<p>우리는 작업하면서 많은 안드로이드 의존성 라이브러리를 사용한다. (retrofit, glide 등)
그러면서 라이브러리가 충돌되는 case 가 발생할 수 있고 이를 CI 상에서도 확인할 수 있다.</p>
<pre><code class="language-yaml">jobs:
  # CI 작업
  integration:
    runs-on: ubuntu-18.04
    steps:
      .
      .
      .
      # 의존성 라이브러리 다운로드 및 확인
      - name: 의존성 라이브러리 다운로드 및 확인
        run: ./gradlew androidDependencies</code></pre>
<p>직접 의존성 라이브러리들을 설치하고 관련 내용을 아래와 같이 확인할 수 있다.</p>
<p><img src="https://images.velog.io/images/ricky_0_k/post/5fa6b479-6048-4e55-8bd6-bce41d414fa7/image.png" alt=""></p>
<p>이 또한 variant 에 따라 여러번 실행되므로 이로 인한 시간을 줄이고 싶다면 <a href="https://stackoverflow.com/questions/49006280/filter-androiddependencies-by-debug-or-release">그에 대한 방법</a>을 찾아 처리할 수 있다.</p>
<h3 id="5-프로젝트-build">5. 프로젝트 build</h3>
<p>다시 build 를 맞이하게 되었지만, 이젠 다른 build 를 사용해야 할 것이다.
우리는 <code>특정 variant 에 맞는 build</code> 를 원하므로 이에 맞춰 자유롭게 명령어를 작성해주면 된다. </p>
<p>Android Studio 와 비슷하게 처리해주려면 아래와 같이 작성하면 된다. 
다른 variant 로 처리하고 싶을 경우, assemble 뒤에 다른 variant 를 적어주면 된다.</p>
<pre><code class="language-yaml">jobs:
  # CI 작업
  integration:
    runs-on: ubuntu-18.04
    steps:
      .
      .
      .
      # 프로젝트 빌드
      - name: 프로젝트 빌드
        run: ./gradlew assembleDebug</code></pre>
<h3 id="6-코드스타일-확인">6. 코드스타일 확인</h3>
<p>각 언어마다 코드 작성 스타일 가이드가 존재한다. 
안드로이드 스타일 가이드의 경우 <a href="https://developer.android.com/kotlin/style-guide?hl=ko">안드로이드 공식 문서 링크</a>, <a href="https://kotlinlang.org/docs/coding-conventions.html">코틀린 공식 문서 링크</a> 등을 통해 확인할 수 있다.</p>
<p>물론 숙지하고 코드를 작성하겠지만 우리는 사람인만큼 실수할 수 있고 스타일 가이드를 놓칠 수 있다.
이를 개발자 대신 체크해주거나, 고쳐주는 기능이 있다면 더할나위없이 편할 것이다. </p>
<p>안드로이드에서는 크게 3가지 도구가 있다.</p>
<h4 id="1-ktlint">1. ktlint</h4>
<p>안드로이드 공식 문서, 코틀린 공식 문서에서 언급한 스타일 가이드를 확인해주는 도구이다. 
틀릴 경우 안내(<code>ktlintCheck</code>)해주거나, 직접 수정(<code>ktlintFormat</code>)도 해준다.
<a href="https://github.com/pinterest/ktlint/blob/master/README.md#-with-intellij-idea">github link</a>를 통해 프로젝트에 적용이 가능하며, 이를 github actions 에 언급하여 CI 도중에 스타일 테스트를 할 수 있다. </p>
<pre><code class="language-yaml">jobs:
  # CI 작업
  integration:
    runs-on: ubuntu-18.04
    steps:
      .
      .
      .
      # 코틀린 스타일 테스트
      - name: 코틀린 스타일 테스트
        run: ./gradlew ktlintCheck</code></pre>
<h4 id="2-detekt">2. detekt</h4>
<p>코틀린 정적 코드 분석 도구이다.
코드 순환 검사, 코드의 복잡성 등 전반적인 코드의 품질을 검사해주는 도구이며 <a href="https://github.com/detekt/detekt#quick-start-">github link</a>를 참고하여 적용할 수 있다.
이 역시 CI 도중에 스타일 테스트를 할 수 있다.</p>
<pre><code class="language-yaml">jobs:
  # CI 작업
  integration:
    runs-on: ubuntu-18.04
    steps:
      .
      .
      .
      # kotlin style 테스트
      - name: kotlin style 테스트
        run: ./gradlew detekt</code></pre>
<h4 id="3-android-lint">3. android lint</h4>
<p>프로젝트의 구조 및 불필요한 코드가 있는지 확인하고 체크해주는 도구이다.
기본으로 제공해주며 <a href="https://developer.android.com/studio/write/lint">안드로이드 공식 문서</a>에서 확인할 수 있다.</p>
<pre><code class="language-yaml">jobs:
  # CI 작업
  integration:
    runs-on: ubuntu-18.04
    steps:
      .
      .
      .
      # android lint 테스트
      - name: android lint 테스트
        run: ./gradlew lint</code></pre>
<p>특정 variant 에 대해서만 실행시켜주는 것도 가능하다. (ex. <code>./gradlew lintDebug</code>)</p>
<p>위와 같이 안드로이드에서는 크게 3가지 코드 스타일 테스트가 있으며 
이들을 적절히 활용하여 코드의 품질을 사전에 체크하고 향상시킬 수 있다.</p>
<h3 id="7-테스트">7. 테스트</h3>
<p>다양한 테스트 (UnitTest, UITest) 를 통해 
프로젝트 내의 로직이 원하는 대로 작동하는지 UI 가 정상동작하는 지 검증할 수 있다. </p>
<p>아래는 UnitTest 의 예이다.</p>
<pre><code class="language-yaml">jobs:
  # CI 작업
  integration:
    runs-on: ubuntu-18.04
    steps:
      .
      .
      .
      # 프로젝트 Unit 테스트
      - name: 프로젝트 Unit 테스트
        run: ./gradlew testdebugUnitTest</code></pre>
<p>위에 언급했던 <code>5. 프로젝트 build</code> 와 비슷하게 특정 variant 에 대응하여 처리가 가능하다.
다양한 variant 에 대한 테스트가 필요없을 경우 위와 같이 처리하여 시간을 줄일 수 있다.</p>
<h3 id="8-기타">8. 기타</h3>
<p>다른 gradlew 명령어를 수행하거나, 다른 작업을 수행할 수 있다.
필자의 경우에는 <code>슬랙으로 메시지 보내는 과정</code>을 추가했으며, 경우에 따라 <code>커버리지 리포트를 업로드</code>하거나, dokka 등을 활용하여 <code>주석에 기반한 전용 문서</code>를 만들어 낼수도 있다.</p>
<h1 id="정리">정리</h1>
<p>원래는 직접 스타일 가이드를 적용하고 테스트 코드를 작성하는 것도 하려고 했지만 
문단이 너무 길어질 것 같아 여기에서 마무리 지으려 한다.</p>
<p>여기까지가 이론과 예시였으니, 다음 과에서 직접 코드 스타일 라이브러리와 테스트 코드를 작성하여 
실제 github actions 에 어떻게 보이게 되는지를 확인해보자.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Github Actions 으로 Android CI/CD 구축한 썰 만들어보기 [3. on, job, workflow]]]></title>
            <link>https://velog.io/@ricky_0_k/Github-Actions-%EC%9C%BC%EB%A1%9C-Android-CICD-%EA%B5%AC%EC%B6%95%ED%95%9C-%EC%8D%B0-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EA%B8%B0-4.-on-job</link>
            <guid>https://velog.io/@ricky_0_k/Github-Actions-%EC%9C%BC%EB%A1%9C-Android-CICD-%EA%B5%AC%EC%B6%95%ED%95%9C-%EC%8D%B0-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EA%B8%B0-4.-on-job</guid>
            <pubDate>Sun, 12 Dec 2021 16:25:26 GMT</pubDate>
            <description><![CDATA[<p>앞장에서 우리는 CI/CD 를 통해 hello world 를 찍어보면서 다양한 내용들 (언어보단 리눅스, 라이브러리의 존재 등) 을 확인하고 CI 의 이점도 새삼 확인해볼 수 있었다.
이제 지난번에 간략하게만 언급했던 on 과 job 에 대해서 다시 다루어보고 다음 단계로 넘어가보려 한다.
(사실 단원을 따로 두긴 했지만 그렇게 많은 내용을 다루진 않을 것 같다.)</p>
<h1 id="1-on">1. on</h1>
<p>on 에 대해서는 주로 <code>Event</code> 와 연결되어 많이 이야기가 나온다. 
github 에서 이야기하는 Event 는 github Repository 내에서 동작하는 <code>일련의 활동</code> (ex. pull Request, push 등) 을 통칭한다.</p>
<p>아래는 on 의 간단한 예이다.</p>
<pre><code class="language-yaml">on:
  push:
    branches:
      - &#39;*&#39;</code></pre>
<p>위 내용을 그대로 해석하면 아래와 같다. </p>
<blockquote>
<p><code>어떤 브랜치든 push 할때마다</code> github actions 를 실행시킨다.</p>
</blockquote>
<p>이렇게 특정 Event 에 맞춰 github actions 을 실행해줄 수 있다.</p>
<h2 id="사용-예-정리">사용 예 정리</h2>
<p>사실 위 내용만 알면 끝났다. 
우리는 다양한 Event 및 그 Event 의 <code>조합</code> 을 통해 다양한 케이스를 만들 수 있다.</p>
<h3 id="1-tag-push">1. tag push</h3>
<p>on 은 태그 push 도 캐치할 수 있다. </p>
<pre><code class="language-yaml">on:
  push:
    tags:
      - &#39;test_*&#39;</code></pre>
<p>위 내용을 해석하면 아래와 같다.</p>
<blockquote>
<p><code>test_</code> 문구로 시작되는 모든 문자열에 대응하는 모든 태그를 push 하는 경우에 github actions 를 동작시킨다.</p>
</blockquote>
<h3 id="2-각종-event-조합">2. 각종 Event 조합</h3>
<p>이벤트를 조합하여 캐치도 가능하다.
<code>:</code> 를 통해 각 Event 를 구분시키며 언급된 event 들은 or 조건으로 동작한다. </p>
<pre><code class="language-yaml">on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main</code></pre>
<p>위 내용을 해석하면 아래와 같다.</p>
<blockquote>
<p><code>main</code> 브랜치로 PR 이 올라오거나, <code>main</code> 으로 push 가 되는 경우 github actions 을 동작시킨다.</p>
</blockquote>
<p>그 외에 다양한 내용(page 작성 등)을 캐치할 수 있고, <code>!</code> 을 통해 특정 이벤트의 경우를 제외하는 등 다양한 처리가 가능하다. 자세한 내용은 <a href="https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#on">공식 문서</a> 참고를 해보는 걸 추천한다.</p>
<h1 id="2-job">2. job</h1>
<p>job 은 <code>하나의 실행 작업</code> 을 이야기한다.
위에서 이야기했던 &quot;github actions 를 동작시킨다&quot; 가 사실은 <code>jobs 에 언급된 job 을 동작시킨다</code> 와 같은 의미이다.</p>
<h2 id="사용-예-정리-1">사용 예 정리</h2>
<p>job 역시 위 내용만 알면 끝났다. 
간단한 사용 예 하나를 언급해보려 한다.</p>
<h3 id="병렬-처리">병렬 처리</h3>
<p>job 은 파일 내에 여러개를 만들 수 있다. (jobs 이름에서 일부 예상한 사람들도 있을 것이다.)
그러면 job 들을 분리 시켜 병렬로 실행할수도 있을까? 가능하다.</p>
<p>필자는 오늘 배운 것을 테스트해보기 위해 
<code>.github/workflows</code> 디렉터리 내에 <code>3_on_job</code> 브랜치를 만들고 
<code>3_on_job.yaml</code> 이라는 파일을 만들어 아래와 같이 작성하였다. </p>
<pre><code class="language-yaml">name: 3_on_job

on:
  push:
    branches: 
      - &#39;3_*&#39;
      - &#39;master&#39;

  pull_request:
    branches:
      - &#39;master&#39;

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2
    - name: set up JDK 11
      uses: actions/setup-java@v2
      with:
        java-version: &#39;11&#39;
        distribution: &#39;adopt&#39;
        cache: gradle

    - name: Grant execute permission for gradlew
      run: chmod +x gradlew
    - name: Build with Gradle
      run: ./gradlew build
    - name: Print Hello World
      run: echo &quot;3_*&quot;</code></pre>
<p><img src="https://images.velog.io/images/ricky_0_k/post/52554ce6-bb41-48cc-b10d-a93e3c2821f9/image.png" alt=""></p>
<p>작성을 완료하면 최종적인 파일 구조는 이렇게 된다. 작성을 완료하면 어떤 동작이 일어날까? 
바로 캡처는 못했지만 위의 push 조건으로 인해 job 이 동작하는 것을 확인할 수 있다.</p>
<p><img src="https://images.velog.io/images/ricky_0_k/post/e961c59e-71a6-444d-a3dc-e3b775e3779d/image.png" alt=""></p>
<p>그리고 master 로 PR 을 만들어보면 어떨까?
<img src="https://images.velog.io/images/ricky_0_k/post/eabd198f-f358-40c4-b921-c6146c25bcff/image.png" alt="">
<code>Android CI</code> 로 적혀 있는 내용은 일전에 on 조건에 아래같이 적혀있어서 그렇다.</p>
<pre><code class="language-yaml">on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]</code></pre>
<p>master 로 PR 또는 push 가 이루어질 때 캐치하므로, Android CI 도 동작한다.</p>
<p>우리는 이를 통해 하나의 디렉터리에 <code>여러개의 yaml 파일을 둘 수 있고</code> 
이를 통해 여러개의 job 을 <code>병렬로 실행</code>시킬 수 있음을 확인할 수 있다.</p>
<p>이 외에 다양한 기능들 또한 존재한다.
자세한 내용은 <a href="https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#jobs">공식 문서</a> 참고를 해보는 걸 추천한다.</p>
<h1 id="3-workflow">3. WorkFlow</h1>
<p>위에서 짤막하게 언급했지만 jobs 에는 여러개의 job 을 등록할 수 있다. </p>
<pre><code class="language-yaml">jobs:
  my_first_job:
    name: My first job
  my_second_job:
    name: My second job</code></pre>
<p>그러면 우리는 jobs 를 아우르는 파일 단위를 어떻게 부를까? 
<code>WorkFlow</code> 라고 이야기한다.</p>
<p>아래 조건만 지킨다면 github repository 내에서, 하나의 workflow 로 인지된다.</p>
<ol>
<li>repository 내 <code>.github/workflows</code> 디렉터리 내에 존재해야 한다.</li>
<li><code>.yml</code> or <code>.yaml</code> 확장자를 가져야한다.</li>
</ol>
<h1 id="4-결론">4. 결론</h1>
<p>예상보다 한 파트 설명 하나가 늘었다 (<code>workflow</code>) </p>
<p><code>대략적인 큰 구조</code>를 이해하는 시간이 있으면 좋겠다고 생각해서 
한 단원을 할애해 정리해 보았는데 돌아보니 기초적인 내용만 언급하고, 
공식 문서 참고하라는 말만 있어 조금 쑥스럽긴 하다.
(막상 정리해보려했는데 공식 문서 양이 너무 많기도 했다. ㅎㅎ)</p>
<p>하지만 공식 문서에 왠만한 내용과 사용 예는 나와있고, 
막히는 내용들은 왠만해서 공식문서에 언급되어 있으므로 
github actions 코드를 작성할 때, 공식 문서를 1순위로 참고하는 것을 추천한다.</p>
<p>다음장에선 오늘까지 다뤄본 내용을 기반으로 드디어 CI 코드를 작성해볼 예정이다.</p>
<h1 id="참고">참고</h1>
<ol>
<li><a href="https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions">공식 문서</a></li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[Compose 0.  Compose 프로젝트에서 처음 만들어지는 MainActivity 얕게 분석하기]]></title>
            <link>https://velog.io/@ricky_0_k/Compose-0.-%EC%B2%98%EC%9D%8C-%ED%81%B4%EB%A6%AD%ED%95%B4%EC%84%9C-%EC%83%9D%EC%84%B1%EB%90%98%EB%8A%94-MainActivity-%ED%95%B4%EC%84%9D%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@ricky_0_k/Compose-0.-%EC%B2%98%EC%9D%8C-%ED%81%B4%EB%A6%AD%ED%95%B4%EC%84%9C-%EC%83%9D%EC%84%B1%EB%90%98%EB%8A%94-MainActivity-%ED%95%B4%EC%84%9D%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Fri, 26 Nov 2021 14:24:37 GMT</pubDate>
            <description><![CDATA[<h1 id="서론">서론</h1>
<p>사실 개인적으로 Compose 를 공부해보려는 시도가 처음은 아니다. 작년 12월에 개인 회고를 하며 <code>Compose 경험하기</code> 를 목표로 잡았었고, 실제 이번 연도 언젠가 <code>stable</code> 버전이 나왔다는 소식을 들어 그 때 Compose 를 살짝 맛보려 했었다.</p>
<p>하지만 업무와 먼저 해야될 일들로 인해 마음먹은 지 1~2일도 안 되어 포기했고, 지금도 벌려놓은 개인 일정들로 인해 Compose 프로젝트를 해볼 생각을 못하고 어느덧 12월을 마주하게 되었다.</p>
<p>그러던 찰나 Android Studio 정식 버전에서 Compose 기반의 프로젝트를 생성할 수 있는 걸 다시 보게 되었다. 12월도 가까워지고 이 내용을 다시 보니 개인의 최소한의 속죄(?)와 양심을 생각해서(...) 기본적으로 생성되는 Compose 프로젝트를 분석해보는 시간이라도 가져야겠다는 생각이 생겨 이 포스트를 작성하게 되었다.</p>
<p>자신감 제로의 상태이지만 열정 만땅으로 일단 시작해본다. 내용을 작성하면 서 <a href="https://developer.android.com/codelabs/jetpack-compose-basics#0">코드랩</a> 또한 참고하였다.</p>
<h1 id="접근-및-생성">접근 및 생성</h1>
<p>먼저 프로젝트 생성은 Compose 기반 프로젝트를 선택하면 된다. 
이해가 안될수도 있어 사진 한 장을 아래 첨부한다.
<img src="https://images.velog.io/images/ricky_0_k/post/dabfe9aa-12a4-49e2-8495-1e4ac060034f/image.png" alt="">
그러면 우리는 아래의 코드를 보게 된다.</p>
<h1 id="안녕-compose-이하-컴포즈">안녕 Compose (이하 컴포즈)</h1>
<p>잠시 기다리면 평소와 같이 <code>MainActivity.kt</code> 코드를 마주하게 된다.
이전과 차이가 있다면, xml 이 없다. (코드는 분석 단계에서 언급할 예정이다.)</p>
<blockquote>
<p>갑자기 에러가 발생해요</p>
<p>갑자기 이런 류의 에러가 발생할 수 있다.
<img src="https://images.velog.io/images/ricky_0_k/post/5d81907b-a99c-43f3-add1-f2e4c74f6f08/image.png" alt="">
앞선 <code>Compose 적용기</code> 포스트에 언급했다시피 컴포즈는 Kotlin version (1.5.31), Android Gradle Plugin (7.0.0), minSdkVersion (21) 을 맞춰주어야 한다. (의심된다면 <a href="https://developer.android.com/jetpack/compose/interop/adding?hl=ko">이 링크</a>를 보라)
필자의 경우에는 gradle JDK 설정과 compileOptions, kotlinOptions 의 Java, JVM 설정을 11로 바꾸었다.</p>
</blockquote>
<p>위의 요구사항을 맞추고 빌드에 성공하여 아래 화면을 본다면 성공이다.
<img src="https://images.velog.io/images/ricky_0_k/post/3175dcc0-737d-421c-a1b0-523710f2f2a4/image.png" alt="">
조금은 멋 없지만, 예쁘게 하는 건 이 포스트의 목적이 아니므로 바로 분석으로 들어간다.</p>
<h1 id="분석">분석</h1>
<h2 id="1-하나의-언어에서-실행된-파일">1. 하나의 언어에서 실행된 파일</h2>
<p>일단 기존과 다르게 하나의 언어로만 Activity 가 만들어진다.
그런데 주목할 건 단순히 Activity 에 대해서만 하나의 언어가 아니라는 것이다.</p>
<p>파일을 분석하다보면 <code>ui.Theme</code> 라는 패키지가 보인다.
거기에는 색상값, 테마, 기존의 selector drawable 등 다양한 리소스 대체물들이 보인다.
그리고 실제 아래와 같이 적용하면 우리는 <code>Teal200</code> 색상이 적용된 텍스트를 만날 수 있다. 
스크린샷은 따로 넣지 않겠다.
<img src="https://images.velog.io/images/ricky_0_k/post/3a1e93d5-491a-4d8a-8184-6c5ab2f733bf/image.png" alt="">
기존에 사용해왔던 xml 지식을 이젠 활용 못한다는 아쉬움(?)이 있지만 
Kotlin 순도 100% 로 안드로이드 프로젝트를 만들기 위해선 어쩔 수 없다는 생각이 든다.</p>
<h2 id="2-mainactivity-분석">2. MainActivity 분석</h2>
<pre><code class="language-kotlin">package kr.co.compose

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import kr.co.compose.ui.theme.ComposeTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeTheme {
                // A surface container using the &#39;background&#39; color from the theme
                Surface(color = MaterialTheme.colors.background) {
                    Greeting(&quot;Android&quot;)
                }
            }
        }
    }
}

@Composable
fun Greeting(name: String) {
    Text(text = &quot;Hello $name!&quot;)
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    ComposeTheme {
        Greeting(&quot;Android&quot;)
    }
}</code></pre>
<p>Activity 에 대한 코드는 이 내용밖에 없다고 했으므로 이 코드를 이제 분석해보자</p>
<h2 id="1-oncreate">1. onCreate()</h2>
<p><code>savedInstanceState</code> 를 파라미터로 받는 건 똑같다. 
다만 <code>setContentView</code> 가 보이지 않는데 이건 뭘로 대체가 되었는지 대충봐도 알 수 있다.</p>
<pre><code class="language-kotlin">setContent {
    ComposeTheme {
        // A surface container using the &#39;background&#39; color from the theme
        Surface(color = MaterialTheme.colors.background) {
            Greeting(&quot;Android&quot;)
        }
    }
}

// ...
@Composable
fun Greeting(name: String) {
    Text(text = &quot;Hello $name!&quot;, style = TextStyle(color = Teal200))
}</code></pre>
<p>setContent 안의 내용을 별다른 개념 없이 <code>{ ... }</code> 을 &quot;설정한다&quot;로 이해하고,
함수의 개념대로만 이해하면 아래와 같이 이야기할 수 있다.</p>
<ol>
<li>ComposeTheme 를 설정한다.</li>
<li><code>MaterialTheme.colors.background</code> 배경 색상을 가지는 Surface 를 설정한다.</li>
<li>Surface 내에 Greeting 를 실행한다.</li>
<li>Greeting 은 Text 를 띄워주는 Composable 어노테이션 함수이다.</li>
</ol>
<p>확실하게 느낀 건 이 Activity 에 무엇을 설정했고 사용하는지 한 눈에 이해된다는 것이다.
(ComposeTheme 를 설정했고, Text 를 가지고 있으며 텍스트 색상은 Teal200 이다.)
이전의 Kotlin 파일에 1줄짜리 setContentView 를 제공하고, style.xml, activity_main.xml 를 봐야했던 과거와 달리, Kotlin 파일에 View 의 이야기가 코드로 드러난 모습이다.</p>
<h3 id="1-composetheme">1. ComposeTheme</h3>
<p>타고 들어가면 <code>ui.theme</code> 패키지에서 Theme.kt 에 이미 선언되어 있는 것을 확인할 수 있다.</p>
<pre><code class="language-kotlin">@Composable
fun ComposeTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -&gt; Unit) {
    val colors = if (darkTheme) {
        DarkColorPalette
    } else {
        LightColorPalette
    }

    MaterialTheme(
        colors = colors,
        typography = Typography,
        shapes = Shapes,
        content = content
    )
}</code></pre>
<p><code>@Composable</code> 어노테이션을 가진 함수로 구현되어 있다.</p>
<p>다크 테마 여부를 체크하여 이에 맞는 색상 테마, 폰트 등을 설정하는 것을 확인할 수 있다.
기존의 <code>styles.xml</code> 을 생각하면 편할 것 같다.</p>
<h3 id="2-surface">2. Surface</h3>
<p>Surface 하고 MaterialTheme 는 Google 에서 만든 Material Design 과 관련된 개념이라고 한다.
시각적으로 관련되는 방식 및 그림자를 드리우는 방식 등에 영향을 주며 이것 또한 <code>@Composable</code> 어노테이션을 가진 함수로 구현되어 있다.</p>
<h3 id="3-greeting-함수">3. Greeting 함수</h3>
<p>Text 함수를 가지고 있는 <code>@Composable</code> 함수이며 Text 를 설정해주는 것 같이 보인다.
(참고로 Text 또한 <code>@Composable</code> 함수이다.)</p>
<p>여기까지 오면 공통된 단어가 계속 보일 것이다. <code>@Composable</code> 어노테이션을 가진 함수
이 함수의 정체는 무엇일까?</p>
<h3 id="4-composable">4. @Composable</h3>
<p>코드랩에 보면 이런 내용이 있다.</p>
<blockquote>
<p>Compose App 은 Composable 함수로 이루어집니다. 
Composable 은 다른 Composable 함수를 호출할 수 있는 단순한 함수일 뿐입니다.
새 UI 구성 요소를 만드는 데 필요한 건 함수 뿐이며, Composable 어노테이션은 UI 를 업데이트하고 유지할 수 있도록 Compose 에 지시합니다. Compose 를 사용하면 코드를 작은 뭉치로 구조화할 수 있고, Composable 함수는 줄여서 Composable 이라고도 합니다.</p>
</blockquote>
<p>정리해보면 Composable 어노테이션 함수는 <strong>UI</strong> 이다. 
그러면서 반환값이 없기 때문에, UI 값을 리턴 받아 다른 위치에서 활용될 가능성은 없다.
그 개념으로 이해를 해보면 Greeting 함수는 <code>일종의 UI</code>이며, Greeting 내에서는 <code>Text UI</code> 를 가진다고 표현할 수 있다. </p>
<h3 id="5-결론">5. 결론</h3>
<p>우리는 위 onCreate() 분석을 통해, Composable 함수를 선언하여 UI 를 설정하고, UI 와 관련된 설정 (ex. 텍스트 색상 설정) 은 오로지 Composable 함수 선언부에서만 이루어진다는 것을 확인할 수 있다.</p>
<h1 id="2-preview">2. @preview</h1>
<p>onCreate 밑을 보면 이런 함수가 있다.</p>
<pre><code class="language-kotlin">@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    ComposeTheme {
        Greeting(&quot;Android&quot;)
    }
}</code></pre>
<p>내용을 보면 Composable 함수인데, onCreate 의 setContent 와 비슷한 내용이 적혀있다.
왜 사용하는 걸까? 정의를 보면 바로 알 수 있다.</p>
<blockquote>
<p>@PreView 는 Composable 함수에 적용할 수 있으며, Android Studio Preview 를 보여주기 위해 활용됩니다.</p>
</blockquote>
<p>정리하면 IDE 에서 UI 를 보기 위해 사용된다. 실제 보면 아래 우측과 같이 preview 를 볼 수 있다.
<img src="https://images.velog.io/images/ricky_0_k/post/ef046853-18ab-4140-bd19-12ab2eb9e2b2/image.png" alt=""></p>
<h2 id="실험">실험</h2>
<h3 id="1-중복-없애기">1. 중복 없애기</h3>
<p>개인적으로 코드가 비슷하고 중복을 없애도 문제없을 것 같아 보인다.
우리가 배운 Composable 개념을 활용해 이렇게 하면 안될까?</p>
<pre><code class="language-kotlin">class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MainView()
        }
    }
}

@Composable
fun MainView() {
    ComposeTheme {
        // A surface container using the &#39;background&#39; color from the theme
        Surface(color = MaterialTheme.colors.background) {
            Greeting(&quot;Android&quot;)
        }
    }
}

@Composable
fun Greeting(name: String) {
    Text(text = &quot;Hello $name!&quot;, style = TextStyle(color = Teal200))
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    MainView()
}</code></pre>
<p><code>MainView</code> 라는 Composable 함수를 만들어 setContent 와 Preview 에서 사용되는 중복 코드를 줄였다. 이렇게 해도 결과는 동일하다.</p>
<h3 id="2-preview-여러개-선언하기">2. Preview 여러개 선언하기</h3>
<p>PreView 를 여러개 사용할 수 있다. 
아래와 같이 DefaultPreView2 를 선언하고 빌드하면 PreView 가 2개 보인다.
<img src="https://images.velog.io/images/ricky_0_k/post/31015c1c-b248-4dad-ab72-84c5090e6713/image.png" alt=""></p>
<h1 id="결론">결론</h1>
<p>오직 MainActivity 만 얕게 분석해보았고 이에 대한 실험만 해보았는데도 Compose 의 장점은 명확히 보였다.</p>
<ol>
<li><code>100%</code> 에 가까워진 Kotlin 코드</li>
<li><code>선언형 UI</code> 로 인한 직관성 향상</li>
<li>UI 의 반환값이 없어 <code>코드 수정으로 인한 영향 최소화</code></li>
</ol>
<p>여기에 MVVM 이나 MVP 를 적용시킨다면 xml 에 kotlin 을 강제 적용하여 고통받았던 에피소드는 이제 없어질 것이다. (함수형 변수 넣을 때 맨날 고통받았던 기억....) 더불어 <code>View 가 수동적</code>이 되어야 하는 MVVM 의 경우엔, 3번 요인으로 인해 더욱 아키텍처 개념을 확고하게 할 수 있을 것 같다.</p>
<p>장점을 보고 관련된 레퍼런스들을 깊게 <code>deep Dive</code> 해야겠다는 생각이 들었던 분석 시간이었다.</p>
<h1 id="참고---깊게-분석해볼-레퍼런스">참고 ( + 깊게 분석해볼 레퍼런스)</h1>
<ol>
<li><a href="https://thdev.tech/android/2020/10/18/Android-Jetpack-Compose-Basics/">안드로이드 Jetpack Compose! 구글 Codelabs을 통해 알아본다.</a></li>
<li><a href="https://developer.android.com/codelabs/jetpack-compose-basics#0">코드랩</a></li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[티스토리 컴포즈 적용기]]></title>
            <link>https://velog.io/@ricky_0_k/%ED%8B%B0%EC%8A%A4%ED%86%A0%EB%A6%AC-%EC%BB%B4%ED%8F%AC%EC%A6%88-%EC%A0%81%EC%9A%A9%EA%B8%B0</link>
            <guid>https://velog.io/@ricky_0_k/%ED%8B%B0%EC%8A%A4%ED%86%A0%EB%A6%AC-%EC%BB%B4%ED%8F%AC%EC%A6%88-%EC%A0%81%EC%9A%A9%EA%B8%B0</guid>
            <pubDate>Tue, 23 Nov 2021 14:01:55 GMT</pubDate>
            <description><![CDATA[<p><strong>TMI</strong>
컨퍼런스에서 들었던 내용도 이제 Velog 에 정리하려 한다.</p>
<p>보통 강연을 들을 때 노션에다 정리하고 있다. 
정리하면서 내 생각을 직접 언급하고 스크린샷을 남용하기 때문에 
저작권 대외비 등을 고려하여 공유할 수 없는 빨간맛 문서가 될 때가 많다.</p>
<p>여기에 순화시켜 정리하면서 복습한다 생각하려한다 <del>_</del></p>
<p>바로 내용이 궁금하신 분은 <a href="https://tv.kakao.com/v/423590300">티스토리 컴포즈 적용기</a> 를 참고</p>
<h1 id="적용-결정까지">적용 결정까지</h1>
<p><img src="https://images.velog.io/images/ricky_0_k/post/460e65aa-2f45-4216-b531-e1c823b19b84/image.png" alt=""></p>
<p>개인적으로는 5월 beta 때부터 적용을 준비했다는 것이 놀라웠고 많은 생각이 들었다.</p>
<p>예전에 beta 나 0점대 버전 라이브러리를 1점대 안정화 버전으로 올리면서 
사용 방법이라거나 함수명이 바뀌어서 안정화에 고초를 겪었던 기억이 있었다.</p>
<p>그런 경험이 있다보니 저 말씀의 의미가 무엇이고, 
저걸 위해 어떤 부분을 고려해야했을지 약간은 예상되어 개인적으로 존경(?)스러웠다.</p>
<h2 id="1-브런치-compose-pr">1. 브런치 Compose PR</h2>
<p>스터디로 진행했으면 좋았겠지만 시간 부족으로 인해 <code>연습용 PR</code> 로 진행하였다고 한다. (with 라벨?)
그렇게 PR 은 올렸지만 리뷰는 불가능한 내역들도 있었고, 코드를 직접 설명 및 설명 첨부한적이 많았다고 한다</p>
<h2 id="2-라이트닝-토크">2. 라이트닝 토크</h2>
<p>점진적 변경 방법에 대해서도 논의하였다고 한다.</p>
<h2 id="3-몹-프로그래밍">3. 몹 프로그래밍</h2>
<p>팀원들이 돌아가면서 <code>Compose</code> 작성</p>
<p>사실 모두가 익히기 위해서는 이게 최상의 솔루션일듯하다.</p>
<blockquote>
<p>결론 : 합법적 적용(?)까지 나아갈 수 있었음</p>
</blockquote>
<p><strong>Compose 의 특징 간단히 맛보기</strong></p>
<ol>
<li>Compose 뷰는 <code>반환값이 없음</code> (이로 인해 일부 아키텍처(ex. <code>MVP</code>) 는 불가능)</li>
<li>RxJava, 코루틴 모두 사용 가능 (하지만 코루틴 권장)</li>
<li>Coil 이미지 라이브러리만 compose 를 공식 지원함 (Glide ㅠㅠ)</li>
<li>min SDK 21, kotlin 1.5.10</li>
</ol>
<h2 id="티스토리에서는">티스토리에서는</h2>
<p>TMI : 이미 티스토리에서는 Hilt 를 쓰고 있다고 합니다.</p>
<p>Tistory, Presentation, Domain, Data 모듈이 존재
Presentation 에서 (<code>XML + DataBinding</code> → <code>Compose</code>) 작업 수행</p>
<hr>
<h1 id="compose-ui">Compose UI</h1>
<ol>
<li><p>선언형 (데이터의 변화에 맞춰 화면을 재구성함)</p>
<p>아마 Flutter 을 경험했던 사람들은 반가울수도 있다.
사실 선언형이라는 주제로 포스트 하나를 만들수도 있다.
간단하게 차이만 차이점만 확인한다면 이렇게 표현할 수 있다. (<a href="https://medium.com/@kimdohun0104/%EC%82%AC%EB%9E%8C%EB%93%A4%EC%9D%80-%EC%99%9C-%EC%84%A0%EC%96%B8%ED%98%95-ui%EC%97%90-%EC%97%B4%EA%B4%91%ED%95%A0%EA%B9%8C-1440d03f4e49">해당 링크</a>의 이미지 참조)
<img src="https://images.velog.io/images/ricky_0_k/post/a70f138f-2645-48e0-8e0a-d5b5146a32d5/image.png" alt=""></p>
</li>
<li><p>커스텀뷰 작성도 함수 형태로 작성 가능 (UI 의 확장은 함수들의 합성으로 수행됨)</p>
<p>상기 언급한 <code>선언형</code> 과 연결되는 내용이기도 하다.
직접 뷰의 정보들을 하나씩 선언해주는 특징이 있다보니 
뷰 하나를 만들고 그 값을 리턴하는 형태로도 함수를 만들 수 있다. 
(반복을 줄이기 위해 제법 많이쓴다.)</p>
<p><img src="https://images.velog.io/images/ricky_0_k/post/a962a235-0c1a-4ca4-91e7-a9cef363a963/image.png" alt="">
위 내용은 Flutter 로 탭뷰와 각 탭에 있는 레이아웃을 명세한 코드이다. 
(<del>2번째, 4번째에 Page 글자가 없어 불편할 수 있다.</del>)
각 페이지로 타고 들어가면 각 Page 에 대한 구현 및 반환하도록 되어 있으며 
Compose 에서는 완전 똑같진 않겠지만 이와 비슷하게 작성할 것이라 예상된다.
(필자는 아직 Compose 를 안해보았다 ㅠ)</p>
</li>
<li><p>핫 리로드 일부 가능 (Flutter 정도까진 아님)
필자는 Android 개발을 하다가 Flutter 를 하면서 편안함을 느낄때가 있다.
대표적인 것이 실시간으로 UI 변경을 확인(hot reload) 할 때, 
경우에 따라 빌드 속도도 빠를 때(hot restart) 이다.</p>
<p>Compose 에서도 이게 가능하다고 하니 눈이 좀 띠용했었다. ( ㅇ ㅅ ㅇ )<br>하지만 Flutter 정도까진 아니라고 하니 50%정도 시무룩해졌다. ( / _ \ )</p>
<p>Preview Annotation 을 통해 UI 보면서 직접 작업하며 핫 리로드를 느껴볼 수 있다고 하니 
개인적으로는 매우 기대가 되었다.</p>
</li>
</ol>
<h2 id="적용-방식-시나리오">적용 방식 시나리오</h2>
<p><img src="https://images.velog.io/images/ricky_0_k/post/3db302ab-c4cd-4cbe-b4b6-99aaf22d109f/image.png" alt="">
<del>오오 티스토리의 커스텀뷰 작성 방식이다</del> Android 에서 흔히 볼 수 있는 커스텀뷰 예제이다.
이걸 어떻게 하면 Compose 화 시킬 수 있을까?</p>
<p>아래를 포함한 몇 가지 과정을 거치면 아래의 Compose 형태의 뷰를 만들 수 있게 된다.</p>
<ol>
<li>TextView → Text, ImageView → Image</li>
<li>선언형으로 작성 가능</li>
<li>margin 개념은 spacer 또는 padding 으로 처리
....
<img src="https://images.velog.io/images/ricky_0_k/post/47dffc23-3e88-4b9b-8c3d-c8117b9ffc10/image.png" alt=""></li>
</ol>
<blockquote>
<p>Modifier</p>
<ol>
<li>글자 크기 등의 속성을 정함</li>
<li>chaining 함수여서 입력받은 순서대로 속성이 적용되므로 주의 필요
(코드의 예 : <code>패딩 적용 -&gt; 색상 적용</code> 과 <code>색상 적용 -&gt; 패딩 적용</code> 은 동작이 다르다)
(실생활 예 : <code>참깨빵 위에 패티 2장</code> 과 <code>패티 두장 위에 참깨빵</code> 은 아예 다르다. <del>싸움 날 수도 있다.</del>)</li>
</ol>
</blockquote>
<h2 id="compose-필수요소">Compose 필수요소</h2>
<h3 id="재구성">재구성</h3>
<p>데이터의 변경에 따라 UI 를 다시 그리는 것을 지칭
state 와 remember 에 기반함</p>
<p><strong>State</strong> 
데이터의 변경을 알리는 역할 (as like LiveData)</p>
<p><strong>Remember</strong>
화면이 유지되는 동안 데이터를 저장하고 다시 복원하는 역할
전환된 값은 Composable 이 살아있는 동안은 그대로 유지됨</p>
<p><strong>예시</strong>
<img src="https://images.velog.io/images/ricky_0_k/post/3dec2a96-0121-4332-8af4-e16f182b8e96/image.png" alt=""></p>
<p>분석</p>
<ol>
<li>state 와 remember 은 변수와 연결되어 사용되는 듯</li>
<li>View 가 재구성되어도 값이 유지되어야 할 때 remember 을 사용하는 듯 (값 유지)</li>
<li>StateFul 이라고 함 (이건 Flutter 과 존똑)</li>
<li>Flutter 에서 GetX 의 Rx 변수와 비슷해보이기도 한다.</li>
</ol>
<h3 id="stateful-stateless">StateFul, Stateless</h3>
<p><img src="https://images.velog.io/images/ricky_0_k/post/1eb4fec4-d1bd-4d89-9962-6f17bed07054/image.png" alt=""></p>
<p>상태를 직접 관리 : StateFul
상태를 가지지 않음 : Stateless</p>
<p>막상 이렇게 보면 Flutter 과 별 차이 없어보이기도 한데, 
함수를 보니 Stateless 는 Flutter 과 좀 다른 개념 같았다.</p>
<p>Flutter 의 경우 StatelessWidget 자체만으로는 <code>클릭하더라도 숫자 갱신이 안되었던 것</code>으로 기억하는 데 (아예 view 를 바꿔치기 해주어야 하거나 StateFulWidget 내에서 활용하여 <code>setState()</code> 를 사용해야하는 것으로 기억한다)
Compose 에서는 가능한듯보여 이는 한번 확인해봐야겠다.</p>
<p><code>State hoisting</code> 이라는 용어는 처음 봐서 <code>이런 용어도 있구나</code> 생각했다.</p>
<h2 id="compose-특징과-장점">Compose 특징과 장점</h2>
<ol>
<li><p>Kotlin 을 base 로 작성하며 함수로 구성되어 있음</p>
<p>사실 이거 하나만으로 엄청난 장점일거라 생각한다. 
이제 언어부터 다른 <code>xml</code> 과 잔 선언이 많은 <code>커스텀뷰</code>의 늪에서 
탈출할 수 있다는 것만으로도 큰 메리트라 생각한다.</p>
</li>
</ol>
<ol start="2">
<li><p>View 에 대한 리턴값이 존재하지 않으므로 외부에서 의도치않은 접근을 막음
(자연스럽게 사이드 이펙트가 최소화됨)</p>
<p>막연하게 아키텍처 관점에선 괜찮겠다고 생각이 든다.</p>
</li>
<li><p>간단하게 공통 컨테이너 작성 가능 (재사용성 뿜뿜)</p>
</li>
<li><p>UI 뿐만 아니라 다양한 이벤트 처리 로직도 재사용이 가능</p>
</li>
<li><p>높은 호환성을 가지고 있음</p>
<p>사실 이게 개인적으론 제일 궁금했었는데 높은 호환성을 가진다니 다행이라 생각했다.
이거 덕분에 반쪽자리 기능은 절대 안 될거라 생각했다.</p>
<p>하지만 직접 사용해봐야 알 것 같은 내용도 있어 
과거의 네이티브 프로젝트에서 직접 적용해보면서 
무슨 차이가 있는지 확인해봐야겠다는 생각이 들었다.</p>
<ol>
<li><p>ComposeView</p>
<p><img src="https://images.velog.io/images/ricky_0_k/post/5136aff0-64c3-482e-9079-694561cbcdaf/image.png" alt=""></p>
<p>xml 에 ComposeView 를 선언해두고, 앱(Kotlin) 내에서 Compose 클래스 변수 선언하기
(xml 에 ComposeView 선언하고 선언해둔 뷰 가져오기로 이해했다.)</p>
</li>
<li><p>AbstractComposeView</p>
<p><img src="https://images.velog.io/images/ricky_0_k/post/fe3dea65-c261-46b0-8b12-7e53631ea4cc/image.png" alt=""></p>
<p>xml 기반 커스텀 뷰 내에 Compose 사용하고 싶을 시 AbstractComposeView 상속받기
(커스텀 뷰 내에서도 활용이 가능한듯했다.)</p>
</li>
<li><p>기존 xml 컴포넌트 호출</p>
<p><img src="https://images.velog.io/images/ricky_0_k/post/b6e68f5a-489e-418a-a270-b3ca73113669/image.png" alt=""></p>
<p>Compose 클래스 내에서 AndroidView 사용하기</p>
<ul>
<li>factory : View 를 생성하는 함수</li>
<li>update : 재구성 시에 동작
(Compose 내에서도 기존의 UI 를 쓸 수 있겠다고 생각했다.)</li>
</ul>
</li>
<li><p>기타
직관적, 코드 감소, 빠른 개발, 더 강력한 API 
(머터리얼 디자인, 다크모드, 애니메이션, 이벤트, 접근성 등)    </p>
</li>
</ol>
</li>
</ol>
<h1 id="문제점들">문제점들</h1>
<p>하긴 좋은점만 있진 않겠지 생각했다. </p>
<p>결론부터 이야기하면 직접 구현하거나 코드를 뜯어 가져오는 사례로 해결한 게 많았다.
이 부분을 보면서 뷰에 대해 높은 지식이 필요하겠다는 생각이 들었다.</p>
<h2 id="1-있었지만-없는-기능">1. 있었지만 없는 기능</h2>
<ol>
<li><p><code>includeFontPadding</code> 존재하지 않음
 벌써부터 많이 쓰는 기능이 없다고 한다 ㅠ</p>
<p> 해결책</p>
<ol>
<li><p>AndroidView.factory 에서 기존 TextView 를 사용</p>
</li>
<li><p>직접 구현해서 해결 (Modifier 확장함수)
<img src="https://images.velog.io/images/ricky_0_k/post/52c1ad07-06cc-4f54-b3a4-aec5e1091253/image.png" alt=""></p>
<ol>
<li>Move<ol>
<li>onGlobalPositioned 를 통해 아이템의 현재 위치를 얻고 저장</li>
<li>index 변경 시 저장해둔 위치값을 기반으로 이동하는 애니메이션 진행
(LaunchedEffect)</li>
<li>offset 통해서 값만큼 이동</li>
</ol>
</li>
<li>Add &amp; Remove<ol>
<li>아이템 생성 시 추가되는 애니메이션 실행 (Modifier)</li>
<li>아이템 제거 시 사라지는 애니메이션 실행 (Modifier)  </li>
</ol>
</li>
</ol>
<p>순서를 적으면서 약간은 이해했었는데 이걸 제일 먼저 해봐야겠다는 생각을 했다.</p>
</li>
</ol>
</li>
<li><p>RecyclerView → LazyList (LazyColumn) 에서 animation 기능 부재
정리하면 애니메이션이 1도 없다고 한다.
즉각적으로 화면을 재구성함 (아직 지원하지 않음)</p>
<p>해결책</p>
<ol>
<li>직접 구현해서 해결
<img src="https://images.velog.io/images/ricky_0_k/post/03b6de7a-f663-4e03-b74c-01fb0e2e32a4/image.png" alt=""><ol>
<li>Move<ol>
<li>onGlobalPositioned 를 통해 아이템의 현재 위치를 얻고 저장</li>
<li>index 변경 시 저장해둔 위치값을 기반으로 이동하는 애니메이션 진행
(LaunchedEffect)</li>
<li>offset 통해서 값만큼 이동</li>
</ol>
</li>
<li>Add &amp; Remove<ol>
<li>아이템 생성 시 추가되는 애니메이션 실행 (Modifier)</li>
<li>아이템 제거 시 사라지는 애니메이션 실행 (Modifier)</li>
</ol>
</li>
</ol>
</li>
</ol>
<p>순서를 적으면서 약간은 이해했었는데 이걸 제일 먼저 해봐야겠다는 생각을 했다. x 2</p>
</li>
<li><p>ViewPager,2. ViewPager, SwipeRefreshLayout, WebView 등 주요 컴포넌트가 없음
<a href="https://github.com/google/accompanist">여기</a>를 참고해봐도 좋을 것 같고, 버전 업데이트를 하면서 해결될 문제라고 한다. (하긴 다 구현하긴 어렵지)
그리고 앞서 말한 호환성 파트에서 3번 케이스를 활용하면 될 것 같다는 생각을 했다.</p>
</li>
</ol>
<h1 id="2-완벽한-머터리얼-가이드">2. 완벽한 머터리얼 가이드</h1>
<p>구글에서 너무 스타일 상에 제약을 걸어두어서 문제도 있는듯하다.</p>
<ol>
<li><p>dp 사용 불가능한 <code>fontSize</code>
개인적으로 반응형때문에 dp 를 많이 사용하고 편이다. 그런데 아예 막혀있다고 한다.
(아니 그러면 반응형에 글자 개판되는 거 어케 대응해야 함?)</p>
<p>해결책
CompositionLocalProvider 를 통해 우회
<img src="https://images.velog.io/images/ricky_0_k/post/e293c5dd-0ec3-490c-9700-eab53076e6e2/image.png" alt="">
fontScale = 1f 를 통해 sp 를 dp 처럼 사용하도록 할 수 있다고 한다.
깊게 보면 Kotlin 상에서 접할수도 있는 내용인듯하여 <code>뷰에 대한 깊이</code>가 필요하겠다는 생각이 들었다.</p>
</li>
<li><p>Track, Thumb 크기가 조정되어 있는 Switch, 넓이 변경이 불가능한 NavigationDrawer
<img src="https://images.velog.io/images/ricky_0_k/post/2bb19b2c-1bc5-4740-8620-c4bc10c20741/image.png" alt="">
Switch 를 예로 들어 설명해주셨는데, <code>requiredSize</code> 로 크기를 지정하면 외부의 설정 일절 안먹히고, 상속또한 불가능하다고 한다. 그런데 Switch 내에서는 그렇게 구현되어 있어 일절 커스텀이 불가능하다고 한다.</p>
<p>해결책
모든 코드 복사하여 수정하는 게 나음</p>
</li>
</ol>
<p>어찌보면 스타일이 깨지는 것이기에 저렇게 제약하는 게 맞는거긴하겠지만 글쎄.... 
규약은 만들되 제약이 옵션이 아닌 필수인 것에 대해서는 개인적으로는 회의적이긴 하다</p>
<h1 id="적용-후">적용 후</h1>
<p>코드라인 수 : 공통으로 사용할 view 들이 기존 코드, compose 코드 두 개로 구현되어 있어 증가
앱 번들 : compat 라이브러리들을 제거하지 않고 compose 를 추가하여 증가함
개발 속도 : Compose 의 장점(+) + 앞서 말한 문제점(-) ~= 0</p>
<p>이렇게 보면 소소해보인다고 할 수 있겠지만, 
UI 까지 Kotlin 으로 작성하여 Kotlin 으로 모든 것을 한다는 것만으로도 의미는 크다고 생각한다.
그 외의 직관적임 등은 표현하지 않아도 알 수 있을 것이다.
(짧은 함수 몇줄을 순서대로 잇는 것 만으로 어떤 기능이 있는지 직관적으로 확인할 수 있음)</p>
<h1 id="적용해도-될까요">적용해도 될까요?</h1>
<p>명확한 장점들이 많음
업무가 많거나, 부족한 리소스로 인한 문제가 더 클거라 봄</p>
<hr>
<h1 id="개인-결론">개인 결론</h1>
<p>anko 나 다른 기능들과 다르게 이 기능은 무조건 활성화 될 거라 본다.</p>
<p>개발적으로도 배울 수 있는 부분이 많을 것 같다.
Compose 를 통해 View를 만들고 때로는 없는 기능을 만들면서 미처 놓쳤던 View 파트를 깊게 다루고, 
Flutter 에서 느꼈던 편안함(선언형, 핫 리로드 등)도 간접적으로 느낄 수 있을거라 생각이 든다.</p>
<p>실무에 적용시키기에는 버전 제약이 있어 어려울 것 같고, 단기적으로는 개인 프로젝트에 적용해볼 수 있을 것 같다.
과거에 런칭했던 앱에 Compose 를 적용시켜보면서 익히고, 여유에 따라 시리즈 포스트도 올려볼까한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Github Actions 으로 Android CI/CD 구축한 썰 만들어보기 [2. Hello World]]]></title>
            <link>https://velog.io/@ricky_0_k/Github-Actions-%EC%9C%BC%EB%A1%9C-Android-CICD-%EA%B5%AC%EC%B6%95%ED%95%9C-%EC%8D%B0-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EA%B8%B0-2.-Hello-world</link>
            <guid>https://velog.io/@ricky_0_k/Github-Actions-%EC%9C%BC%EB%A1%9C-Android-CICD-%EA%B5%AC%EC%B6%95%ED%95%9C-%EC%8D%B0-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EA%B8%B0-2.-Hello-world</guid>
            <pubDate>Sun, 21 Nov 2021 15:28:16 GMT</pubDate>
            <description><![CDATA[<p>앞장에서 우리는 CI/CD 가 필수까진 아니더라도 어떻게 요긴하게 쓰일 수 있는지 확인했었다.
이제 github actions 을 통해 hello world(?) 를 찍어보자.
<code>hello World</code> 만 출력할 것이고, 엄청난 스크린샷들과 같이할 것이므로 막힘을 걱정할 필요는 없다 ㅇㅅㅇ</p>
<h1 id="1-적용할-프로젝트에서-actions-클릭">1. 적용할 프로젝트에서 Actions 클릭</h1>
<p><img src="https://images.velog.io/images/ricky_0_k/post/09a051bd-3e11-44cd-be2e-5cf1c7d37fc6/image.png" alt="">
위의 Actions 버튼을 눌러 github actions 페이지로 접속한다.
(필자의 경우는 급하게 Repository 를 만들고 스크린샷을 찍어서 비어있지만, 
 독자 분들은 해당 Repository 가 Android 프로젝트 기반이어야 할 것이다.)</p>
<h1 id="2-android-ci-선택하기">2. Android CI 선택하기</h1>
<p>처음에 보고 놀랄 수 있다. <code>Android</code> 라는 글씨는 눈을 크게 떠도 보이지 않기 때문이다.
<img src="https://images.velog.io/images/ricky_0_k/post/49f176e6-e58e-4afd-b592-245b4a57e84e/image.png" alt="">
왜인지는 모르겠지만 Android CI 는 숨겨져 있다. <del>Android 인권(?) 을 찾아주십시요 뺴액</del>
스크롤 하다가 아래 <code>More continuous integration workflows</code> 버튼을 눌러보자
<img src="https://images.velog.io/images/ricky_0_k/post/09855829-61c7-4df3-a4b3-88ddee3cfde7/image.png" alt="">
이렇게 생긴 카드를 찾아 <code>Set up this workflow</code> 를 눌러보자
<img src="https://images.velog.io/images/ricky_0_k/post/96494b39-eb5f-4efe-9046-a1c9b1c2b2f8/image.png" alt=""></p>
<h1 id="3-기본-설정-완료한-후-commit-하기">3. 기본 설정 완료한 후 commit 하기</h1>
<p><code>Set up this workflow</code> 를 클릭하면 아래와 같은 화면이 나올 것이다.
<img src="https://images.velog.io/images/ricky_0_k/post/cff37ab5-8554-40dd-a7e0-897530cbbae3/image.png" alt=""></p>
<h2 id="1-만들어진-내용-간단하게만-분석하기">1. 만들어진 내용 간단하게만 분석하기</h2>
<p>자세한 내용은 다루지 않을 예정이며 간단하게 예제와 함께 언급만 할 예정이다. 
(자세한 내용은 다음 단원에서 다룬다)</p>
<h3 id="1-yaml">1. yaml</h3>
<p>github actions 는 <code>yaml</code> 언어를 사용한다. Flutter 의 <code>pubspec.yaml</code> 를 접했던 사람들은 반가울 수도(?) 있다.
경험이 없는 사람들은 새 언어를 해야하냐는 두려움에 빠질 수 있겠지만, 들여쓰기(<code>indentation</code>)만 주의하면 쉽게 내용을 작성할 수 있다.</p>
<h3 id="2-on">2. <code>on</code></h3>
<p><code>어떤 브랜치</code>(ex. master) 에 <code>어떤 작업</code>(ex. push, pull_request) 을 할 때 
github actions 을 동작시킬 것인지에 대한 정보를 이야기한다.</p>
<p>예제로 이해해보자.</p>
<pre><code class="language-yaml">on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]</code></pre>
<p>위의 경우에는 <code>master</code> 에 push 하는 경우, <code>master</code> 로 향하는 Pull request(이하 PR) 이 만들어지는 경우 github actions 을 실행하도록 설정한다.</p>
<h3 id="3-jobs">3. jobs</h3>
<p><code>어디서</code> 동작 시킬 것인지, <code>무엇을</code> 할 것인지에 대한 정보를 이야기한다.</p>
<ul>
<li><code>runs-on</code> : 어느 OS 환경에서 실행하는지 이야기한다. (ex. ubuntu, OSX, Window 등)</li>
<li><code>steps</code> : 순차적으로 실행시킬 내용들 목록을 이야기한다</li>
</ul>
<p>예제로 이해해보자.</p>
<pre><code class="language-yaml">jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2
    - name: set up JDK 11
      uses: actions/setup-java@v2
      with:
        java-version: &#39;11&#39;
        distribution: &#39;adopt&#39;
        cache: gradle

    - name: Grant execute permission for gradlew
      run: chmod +x gradlew
    - name: Build with Gradle
      run: ./gradlew build</code></pre>
<p> 위의 경우에는 <code>ubuntu</code> 에서 실행한다. 
 그리고 <code>java 11 설정</code>, <code>gradlew 에 접근할 수 있도록 chmod 설정</code>, <code>gradlew build</code> 를 순차적으로 실행한다</p>
<h3 id="4-hello-world">4. Hello World</h3>
<p>위대로만 두면 그냥 기본 설정대로만 하는 것이므로, 이번 단원의 목표인 <code>hello world 출력</code>을 해보도록 하자.</p>
<pre><code class="language-yaml">    - name: Print Hello World
      run: echo &quot;Hello World&quot;</code></pre>
<p>위 명령어를 들여쓰기에 맞게 추가해주면 된다.
<img src="https://images.velog.io/images/ricky_0_k/post/4837f7bb-def9-4149-aa2b-668d1ee6d201/image.png" alt="">
위와 같이 작성했다면 성공이다.</p>
<p><img src="https://images.velog.io/images/ricky_0_k/post/c07411ea-09e6-4d20-a0f9-a7e3354bb3c6/image.png" alt="">
이후 위와 같이 <code>Start commit</code> 을 눌러 적절한 commit 이름과 내용을 적어주고 <code>Commit new file</code> 를 눌러보자</p>
<h1 id="4-결과-확인">4. 결과 확인</h1>
<p>실제 commit 을 완료하고 Actions 로 들어가보자 
<img src="https://images.velog.io/images/ricky_0_k/post/190ef719-6471-4c9b-8041-36f00273436b/image.png" alt="">
무언가 CI 가 실행되고 있음을 확인할 수 있다. 이제 저 항목을 선택하고 들어가볼까?</p>
<p><img src="https://images.velog.io/images/ricky_0_k/post/b3e1cecc-4eef-4512-bb57-ebe20727d656/image.png" alt="">
들어가 보면 뭔가 실행되는 듯 하고 <code>build</code> 를 클릭해보자. 아무 <code>build</code> 나 눌러도 결과는 같다.</p>
<p><img src="https://images.velog.io/images/ricky_0_k/post/8ee601f7-0927-438c-8e77-0b7328e76389/image.png" alt="">
필자는 들어가는 도중 실패 메시지를 확인할 수 있었다. 
만약 동작중이라면 여전히 노란 progressbar 가 실행되고 있을 것이다.
아니 바로 실패라니 내용을 확인해볼까?
<img src="https://images.velog.io/images/ricky_0_k/post/0e77f066-fc81-401e-82d9-bb53c6b6629b/image.png" alt="">
에러 내용을 확인해보니 android min sdk 가 잘못 맞춰진 것 같다.
<img src="https://images.velog.io/images/ricky_0_k/post/22856560-f9df-43cd-9d16-0e25e1106683/image.png" alt="">
실제 프로젝트에서도 확인해보니 동일한 문제가 발생하고 있었다.</p>
<p><img src="https://images.velog.io/images/ricky_0_k/post/731a657e-4ab4-4f66-a197-668fbb8e79be/image.png" alt="">
필자의 경우 sdk Version 이 잘못 맞춰져 있어 <code>30</code> 으로 설정되었던 내용을 <code>31</code>로 설정했다.</p>
<p><img src="https://images.velog.io/images/ricky_0_k/post/d9ef45d7-0891-433f-b0d8-b805382cac28/image.png" alt="">
이후 master 로 변경된 내용을 반영하고 <code>push</code> 를 한다</p>
<p><img src="https://images.velog.io/images/ricky_0_k/post/487689d6-8a51-469a-a723-f79ca108f4af/image.png" alt="">
일전에 우리는 <code>on</code> 에서 master 로 push 할 경우에도 인지하도록 되어 있는 걸 확인했으므로 github actions 에 정상적으로 잡힌 것을 확인할 수 있다. 결과를 기다려보자
<img src="https://images.velog.io/images/ricky_0_k/post/851fb39e-f963-4431-a2df-162ad01bb8d4/image.png" alt="">
정상적으로 빌드도 완료되고, 우리가 적은 echo 명령어도 잘 동작된 것을 확인할 수 있다.</p>
<blockquote>
<p>TMI : commit 메시지가 이상하네요 <code>sdk version 오류 수정</code> 으로 하지 않으셨나요? </p>
<p>필자가 프로젝트내에서 pull 받고 push 해야하는 걸 깜빡해서 필자의 commit 메시지는 저렇게 보여지고 있다. 
독자 여러분들은 github actions commit 하시고 프로젝트에서 pull 받고 진행하는 걸 잊지 마세요 :)</p>
</blockquote>
<h1 id="정리">정리</h1>
<p>우리는 간단하게 github actions 를 설정하고 <code>Hello World</code> 출력까지 완료했다. 
이 과정에서 우리는 몇가지를 알 수 있었다.</p>
<h2 id="1-echo-는-ubuntu-명령어-아닌가요-설마">1. echo 는 ubuntu 명령어 아닌가요? 설마..</h2>
<p>사실 우리는 앞서 말한 yaml 문법보다 <code>우분투 명령어</code>에 더 신경써야 한다. 
앞으로 우리는 수많은 gradlew 명령어 및 우분투 명령어를 작성하게 될 것이다. 
안드로이드 빌드 및 apk 추출 등을 terminal 에서 자주 하는 독자라면 접근이 쉽겠지만 경험이 없다면 이번 기회에 같이 알아가자 :)</p>
<h2 id="2-어쩌다보니-ci-의-장점을-확인했네요">2. 어쩌다보니 CI 의 장점을 확인했네요</h2>
<p>난 프로젝트만 바로 만들고 <code>문제가 없을 것</code>이라 예상하고, 바로 Github 에 연결만 시켰다. 
그리고 github actions 작성한 코드를 <code>commit</code> 했다. </p>
<p>그 결과는 무엇이었는가? 바로 <strong>실패</strong> 였다
난 프로젝트에 문제가 있음을 <code>github actions</code> 을 통해 확인할 수 있었다.
<img src="https://images.velog.io/images/ricky_0_k/post/835408bf-2ce2-4433-8888-626b2bc243fb/image.png" alt=""></p>
<div style="text-align: center;"><span style="color:#999999;">사람이 인지하지 못한 프로젝트의 문제를 github actions 이 알려주었다.</span></div>

<p>이렇게 사람이 미처 인지하지 못한 내용들을 <code>CI</code> 는 충실하게 알려준다.</p>
<h2 id="3-ci-이외에-다른-것도-할-수-있을-것-같은데요">3. CI 이외에 다른 것도 할 수 있을 것 같은데요?</h2>
<p>맞다. <code>단순 명령의 순차적 조합</code>이기 때문에 온갖 작업들을 넣을 수 있다.
하나의 파일에서 다 처리할수도 있지만, 병렬로 처리하기를 원할 경우 따로 파일을 만들어 작업할 수도 있다. 
이는 CD 를 다루면서 이야기할 예정이다.</p>
<h2 id="4-java-는-어떻게-설정할-수-있었나요">4. java 는 어떻게 설정할 수 있었나요?</h2>
<p>이것도 추후 이야기하겠지만 marketPlace 에 올라와 있는 일종의 라이브러리를 사용했다. 
(편의상 라이브러리라 지칭하겠다.)
<img src="https://images.velog.io/images/ricky_0_k/post/b51fdbc9-20eb-4ade-9d46-2769f825d38c/image.png" alt="">
어마무시하게 많다. 필자도 여기에서 다양한 라이브러리들을 사용했다.
지금 우리가 java 를 설정할 때 사용했던 라이브러리는 <a href="https://github.com/marketplace/actions/setup-java-jdk">Setup Java JDK</a> 이다. 
궁금한 독자들은 위 링크로 들어가서 보면 자세한 설명을 볼 수 있을 것이다.</p>
<h1 id="결론">결론</h1>
<p>간단하게 Hello World 만 이야기하고 끝날 줄 알았는데 설명이 너무 길어졌다.
다음 단원에는 예시만 설명하고 넘어갔던 <code>on</code> 과 <code>jobs</code> 에 대해 알아보고 CI 에 걸맞게 내용을 수정해보자.</p>
<p>(전체 코드는 <a href="https://github.com/riflockle7/github-actions-example/tree/1.hello_world">1.hello_world tag</a> 에서 확인할 수 있습니다.)</p>
]]></description>
        </item>
    </channel>
</rss>