<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Meezzi 미찌</title>
    <link>https://sfida.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Mon, 29 Jun 2026 13:49:03 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Meezzi</managingEditor>
    <item>
      <title>[Android] ViewModel에서 Context 없이 예외 처리 하기</title>
      <link>https://sfida.tistory.com/169</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Firebase에 저장된 데이터를 불러오다가 예외가 날 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 ViewModel에서 예외 처리를 어떻게 하면 좋을지 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 문제 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ViewModel에서 하드코딩된 문자열이 있는 상황이다.&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;viewModelScope.launch {
    try {
        val categorySums = repository.fetchMonthlyCategoryTotals(
            type = type.name.lowercase(),
            year = year,
            month = month,
            uid = uid,
            householdId = householdId
        )

        val entries = categorySums.map { (category, amount) -&amp;gt;
            PieEntry(
                value = amount.toFloat(),
                pieLabel = category,
                pieColor = getColorForCategory(category)
            )
        }

        _uiState.value = _uiState.value.copy(
            chartData = entries,
            isLoading = false,
            errorMessage = null
        )
    } catch (e: Exception) {
        _uiState.value = _uiState.value.copy(
            errorMessage = e.message ?: &quot;알 수 없는 오류가 발생했습니다.&quot;,
            isLoading = false
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 간단해 보이지만 실제로 다음과 같은 문제가 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다국어 지원 불가&lt;/li&gt;
&lt;li&gt;문자열이 하드코딩되어 유지보수가 어려움&lt;/li&gt;
&lt;li&gt;context 의존성이 생김&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 다국어 지원을 하기 위해서 strings.xml파일에 문자열을 정의하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;context.getString() 방식으로 가져온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이런 방식을 적용하면 ViewModel에서는 context에 의존하게 되어버린다.&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;} catch (e: Exception) {
    _uiState.value = _uiState.value.copy(
        errorMessage = e.message ?: context.getString(R.string.unknown_error_message),
        isLoading = false
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ViewModel은 왜 Context에 의존하면 안 될까?&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ViewModel은 UI와 무관한 비즈니스 로직을 처리하는 곳으로 가장 큰 특징 중 하나는 UI Lifecycle과 분리되어 있다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 말해 ViewModel은 화면 회전, 컴포넌트 변경 등에도 상태를 유지할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;그렇다면 &lt;b&gt;ViewModel에서 Context를 참조하면 어떤 일이 생길까?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 참조를 하게 되면 메모리 누수가 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를들어&amp;nbsp;세로 모드에서 Activity가 실행되고, 이 Activity에 대한 Context를 ViewModel이 참조한다고 가정해 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 기기를 가로 모드로 전환하면 기존 Activity는 파괴되고 새로운 Activity가 생성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 ViewModel은 Lifecycle이 더 길기 때문에 파괴된 Activity의 Context를 계속 참조하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 결국 메모리 누수로 이어지게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 ViewModel에서는 가능한 Context에 직접 접근하지 않고 UI 계층에서 처리하거나 리소스 ID를 통해 전달받는 방식으로 분리해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 해결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 각 상황에 대한 예외 메시지를 다르게 표시하기 위해 각 예외에 대한 sealed class를 정의했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 sealed class로 만들었을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sealed class는 컴파일 타임에 하위 클래스가 모두 정해져 있어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;when 문에서 모든 경우를 강제적으로 처리할 수 있기 때문이다.&lt;/p&gt;
&lt;pre id=&quot;code_1754006595412&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class StatisticsUiState(
    val currentYear: Int = 2025,
    val currentMonth: Int = 1,
    val chartData: List&amp;lt;PieEntry&amp;gt; = emptyList(),
    val isLoading: Boolean = false,
    val error: StatisticsError? = null,
    val currentType: TransactionType = TransactionType.EXPENSE,
)

sealed class StatisticsError(@StringRes val messageResId: Int) {
    object NetworkError : StatisticsError(R.string.error_network)
    object FirestoreError : StatisticsError(R.string.error_firestore)
    object UnknownError : StatisticsError(R.string.error_unknown)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;StatisticsError에는 @StringRes가 들어가는데 이는 해당 필드가 문자열 리소스 ID임을 명시해 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 해당 예외에 대한 메시지가 messageResId에 들어간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 error를 기존 String 타입에서 StatisticsError 타입으로 변경하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nullable 타입으로 선언하여 예외가 발생하지 않을 때는 null 값이 들어가게 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;private fun fetchMonthlyCategoryTotals(
    year: Int,
    month: Int,
    uid: String,
    householdId: String,
    type: TransactionType,
) {
    _uiState.value = _uiState.value.copy(
        isLoading = true,
        error = null,
        currentYear = year,
        currentMonth = month,
        currentType = type,
    )

    viewModelScope.launch {
        try {
            val categorySums = repository.fetchMonthlyCategoryTotals(
                type = type.name.lowercase(),
                year = year,
                month = month,
                uid = uid,
                householdId = householdId
            )

            val entries = categorySums.map { (category, amount) -&amp;gt;
                PieEntry(
                    value = amount.toFloat(),
                    pieLabel = category,
                    pieColor = getColorForCategory(type, category),
                    labelResId = getLabelResIdForCategory(type, category)
                )
            }

            _uiState.value = _uiState.value.copy(
                chartData = entries,
                error = null
            )
        } catch (e: FirebaseNetworkException) {
            _uiState.value = _uiState.value.copy(
                error = StatisticsError.NetworkError
            )
        } catch (e: FirebaseFirestoreException) {
            _uiState.value = _uiState.value.copy(
                error = StatisticsError.FirestoreError
            )
        } catch (e: Exception) {
            _uiState.value = _uiState.value.copy(
                error = StatisticsError.UnknownError
            )
        } finally {
            _uiState.value = _uiState.value.copy(
                isLoading = false
            )
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 유심히 봐야 할 부분은 catch문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 다른 예외가 발생했을 때 그에 맞는 uiState.error를 업데이트한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;mel&quot;&gt;&lt;code&gt;uiState.error != null -&amp;gt; Text(
    text = stringResource(id = uiState.error!!.messageResId),
    color = MaterialTheme.colorScheme.error,
    style = MaterialTheme.typography.bodyLarge,
    modifier = Modifier.align(Alignment.Center)
)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 후, UI에서는 error가 null이 아니면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, error가 발생했다면 해당 error의 messageResId를 텍스트로 표시한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 회고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 작업을 통해 ViewModel에서 문자열을 처리하고 그에 관한 예외를 처리하는 방법에 대해 고민하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전까지는 단순히 &quot;&quot;로 문자열을 감싸 에러 메시지를 출력했지만, 다국어 지원이 어렵고, 무엇보다 ViewModel이 context에 의존해야 하는 문제가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 개선을 통해 context에 의존하지 않고도 에러 메세지를 관리할 수 있는 구조를 만들었고, ViewModel의 역할을 명확히 분리하는 계기가 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 사용자에게도 어떤 이유로 오류가 발생했는지를 명확하게 안내할 수 있게 되어 더 나은 사용자 경험을 제공할 수 있게 된 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Android</category>
      <category>android</category>
      <category>Compose</category>
      <category>exception</category>
      <category>viewmodel</category>
      <author>Meezzi</author>
      <guid isPermaLink="true">https://sfida.tistory.com/169</guid>
      <comments>https://sfida.tistory.com/169#entry169comment</comments>
      <pubDate>Fri, 1 Aug 2025 11:13:00 +0900</pubDate>
    </item>
    <item>
      <title>[Android] MPAndroidChart의 PieChart를 분석해서 나만의 CustomPieChart 만들기 (2)</title>
      <link>https://sfida.tistory.com/167</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://sfida.tistory.com/166&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;앞서 MPAndroidChart를 분석했다.&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 분석한 내용을 바탕으로 나만의 PieChart를 구현해보고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 데이터 정의&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MPAndroidChart에서는 PieEntry를 통해 파이 차트를 그리는데 필요한 데이터를 정의했음을 알 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마찬가지로 나만의 PieEntry를 정의해 보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저번에 Color를 따로 지정하는 게 불편하다고 느껴 PieEntry 데이터 클래스의 필드로 color도 같이 포함하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 MPAndroidChart에서는 Color를 Integer 타입으로 정의하였는데 이번에는 Color 타입으로 정의하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;data class PieEntry(
    val value: Float,
    val label: String,
    val color: Color,
)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 차트에 사용할 데이터는 이렇게 정의했다.&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;val sampleData = remember {
    listOf(
        PieEntry(30f, &quot;국밥&quot;, Color(0xFFFF6B6B)),
        PieEntry(25f, &quot;초밥&quot;, Color(0xFF4ECDC4)),
        PieEntry(20f, &quot;떡볶이&quot;, Color(0xFF45B7D1)),
        PieEntry(15f, &quot;치킨&quot;, Color(0xFF96CEB4)),
        PieEntry(10f, &quot;연어&quot;, Color(0xFFFFEAA7))
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 차트 만들기&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) Canvas와 drawArc로 간단한 파이 조각 만들기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Canvas와 drawArc를 이용해 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;drawArc는 부채꼴이나 파이 차트 조각을 그릴 때 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;startAngle&lt;/b&gt;은 현재 조각의 시작 각도&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;sweepAngle&lt;/b&gt;은 현재 조각이 차지할 각도&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;useCenter&lt;/b&gt;는 원의 중심과 연결하여 채워진 조각으로 그린다는 뜻이다.&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;Canvas(modifier = modifier) {
    drawArc(
        color = Color.Gray,
        startAngle = 0f,
        sweepAngle = 50f,
        useCenter = true,
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;257&quot; data-origin-height=&quot;282&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bE7FUc/btsPlhlapLP/EHsBrOrAEsqN0kcJXBXdb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bE7FUc/btsPlhlapLP/EHsBrOrAEsqN0kcJXBXdb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bE7FUc/btsPlhlapLP/EHsBrOrAEsqN0kcJXBXdb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbE7FUc%2FbtsPlhlapLP%2FEHsBrOrAEsqN0kcJXBXdb0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;257&quot; height=&quot;282&quot; data-origin-width=&quot;257&quot; data-origin-height=&quot;282&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) 간단 원형 차트 만들기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 주의할 점은 sweepAngle의 합이 360도가 되어야 하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 drawArc의 시작 각도가 0f로 시작하면 3시 방향에서 시작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 나는 12시를 기준으로 첫 번째 카테고리가 만들어지길 원하기 때문에 &lt;b&gt;270f부터 시작&lt;/b&gt;했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;Canvas(modifier = modifier) {
    var currentStartAngle = 270f // 첫 시작 각도를 270도로 설정 (원의 맨 위)

    // 첫 번째 조각
    drawArc(
        color = Color.Gray,
        startAngle = currentStartAngle,
        sweepAngle = 108f, // 30% -&amp;gt; 30/100 * 360 = 108
        useCenter = true,
    )
    currentStartAngle += 108f // 다음 조각의 시작 각도를 업데이트

    // 두 번째 조각
    drawArc(
        color = Color.Red,
        startAngle = currentStartAngle,
        sweepAngle = 90f,  // 25% -&amp;gt; 25/100 * 360 = 90
        useCenter = true,
    )
    currentStartAngle += 90f

    // 세 번째 조각
    drawArc(
        color = Color.Black,
        startAngle = currentStartAngle,
        sweepAngle = 72f, // 20% -&amp;gt; 20/100 * 360 = 72
        useCenter = true,
    )
    currentStartAngle += 72f

    // 네 번째 조각
    drawArc(
        color = Color.Blue,
        startAngle = currentStartAngle,
        sweepAngle = 54f, // 15% -&amp;gt; 15/100 * 360 = 54
        useCenter = true,
    )
    currentStartAngle += 54f

    // 다섯 번째
    drawArc(
        color = Color.Yellow,
        startAngle = currentStartAngle,
        sweepAngle = 36f, // 10% -&amp;gt; 10/100 * 360 = 36
        useCenter = true,
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;261&quot; data-origin-height=&quot;271&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SX6I7/btsPk40Ct6B/hkNKL8sYo1CsK8S8jCZNpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SX6I7/btsPk40Ct6B/hkNKL8sYo1CsK8S8jCZNpk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SX6I7/btsPk40Ct6B/hkNKL8sYo1CsK8S8jCZNpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSX6I7%2FbtsPk40Ct6B%2FhkNKL8sYo1CsK8S8jCZNpk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;261&quot; height=&quot;271&quot; data-origin-width=&quot;261&quot; data-origin-height=&quot;271&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) 파이 차트 만들기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이차트를 만드는 방법은 간단하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원형 차트 중간에 원만 그리면 된다.&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;Canvas(modifier = modifier) {
    var currentStartAngle = 270f // 첫 시작 각도를 270도로 설정 (원의 맨 위)

    // 첫 번째 조각
    drawArc(
        color = Color.Gray,
        startAngle = currentStartAngle,
        sweepAngle = 108f, // 30% -&amp;gt; 30/100 * 360 = 108
        useCenter = true,
    )
    currentStartAngle += 108f // 다음 조각의 시작 각도를 업데이트

    // 두 번째 조각
    drawArc(
        color = Color.Red,
        startAngle = currentStartAngle,
        sweepAngle = 90f,  // 25% -&amp;gt; 25/100 * 360 = 90
        useCenter = true,
    )
    currentStartAngle += 90f

    // 세 번째 조각
    drawArc(
        color = Color.Black,
        startAngle = currentStartAngle,
        sweepAngle = 72f, // 20% -&amp;gt; 20/100 * 360 = 72
        useCenter = true,
    )
    currentStartAngle += 72f

    // 네 번째 조각
    drawArc(
        color = Color.Blue,
        startAngle = currentStartAngle,
        sweepAngle = 54f, // 15% -&amp;gt; 15/100 * 360 = 54
        useCenter = true,
    )
    currentStartAngle += 54f

    // 다섯 번째
    drawArc(
        color = Color.Yellow,
        startAngle = currentStartAngle,
        sweepAngle = 36f, // 10% -&amp;gt; 10/100 * 360 = 36
        useCenter = true,
    )

    // Canvas의 가로/세로 중 작은 값의 절반이 원의 반지름
    val chartRadius = size.minDimension / 2f
    // Canvas의 정확한 중심 좌표 (원의 중심)
    val centerOffset = Offset(center.x, center.y)

    // 중앙 원의 반지름은 전체 원의 반지름의 반 (중앙 원의 크기)
    val holeRadius = chartRadius * 0.5f

    drawCircle(
        color = Color.White,
        radius = holeRadius,
        center = centerOffset,
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;240&quot; data-origin-height=&quot;267&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/paqkK/btsPne77mjk/vs9KiMdJ6Trjh4qNvELGZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/paqkK/btsPne77mjk/vs9KiMdJ6Trjh4qNvELGZ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/paqkK/btsPne77mjk/vs9KiMdJ6Trjh4qNvELGZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpaqkK%2FbtsPne77mjk%2Fvs9KiMdJ6Trjh4qNvELGZ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;240&quot; height=&quot;267&quot; data-origin-width=&quot;240&quot; data-origin-height=&quot;267&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4) 중복 코드 제거&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전까지는 파이 차트의 각 조각을 일일이 drawArc로 5번 반복해서 그렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 비효율적이기 때문에 반복문을 이용하여 파이 차트를 구현했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;package com.example.mypiechart.ui.chart

import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.mypiechart.data.PieEntry

@Composable
fun CustomPieChartScreen(
    modifier: Modifier = Modifier,
) {
    val sampleData = remember {
        listOf(
            PieEntry(30f, &quot;국밥&quot;, Color(0xFFFF6B6B)),
            PieEntry(25f, &quot;초밥&quot;, Color(0xFF4ECDC4)),
            PieEntry(20f, &quot;떡볶이&quot;, Color(0xFF45B7D1)),
            PieEntry(15f, &quot;치킨&quot;, Color(0xFF96CEB4)),
            PieEntry(10f, &quot;연어&quot;, Color(0xFFFFEAA7))
        )
    }
    Column(
        modifier = modifier
            .fillMaxSize()
    ) {
        Text(
            text = &quot;내가 먹고 싶은 음식&quot;,
            style = MaterialTheme.typography.headlineSmall,
            modifier = Modifier.padding(bottom = 16.dp)
        )

        MyCustomComposePieChart(
            data = sampleData,
            modifier = Modifier
                .fillMaxWidth()
                .height(400.dp)
        )
    }
}

@Composable
fun MyCustomComposePieChart(
    data: List&amp;lt;PieEntry&amp;gt;,
    modifier: Modifier = Modifier
) {
    Canvas(modifier = modifier) {
        val totalValue = data.sumOf { it.value.toDouble() }.toFloat()
        var startAngle = 270f

        val chartRadius = size.minDimension / 2f

        data.forEach { entry -&amp;gt;
            val sweepAngle = (entry.value / totalValue) * 360f
            drawArc(
                color = entry.color,
                startAngle = startAngle,
                sweepAngle = sweepAngle,
                useCenter = true,
            )
            startAngle += sweepAngle
        }

        val holeRadius = chartRadius * 0.5f
        drawCircle(color = Color.White, radius = holeRadius, center = center)
    }
}

@Preview(showBackground = true)
@Composable
fun Preview() {
    CustomPieChartScreen()
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;240&quot; data-origin-height=&quot;254&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4FEAb/btsPk5SSs72/N44k659JP1XzqPJEQvHKhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4FEAb/btsPk5SSs72/N44k659JP1XzqPJEQvHKhK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4FEAb/btsPk5SSs72/N44k659JP1XzqPJEQvHKhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4FEAb%2FbtsPk5SSs72%2FN44k659JP1XzqPJEQvHKhK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;240&quot; height=&quot;254&quot; data-origin-width=&quot;240&quot; data-origin-height=&quot;254&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5) 선과 라벨 추가&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;차트만 있으면 허전하니 선과 라벨도 추가했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;
@Composable
fun MyCustomComposePieChart(
    data: List&amp;lt;PieEntry&amp;gt;,
    modifier: Modifier = Modifier
) {
    Canvas(modifier = modifier) {
        // 차트 설정
        val totalValue = data.sumOf { it.value.toDouble() }.toFloat()
        var startAngle = 270f

        // 중앙 원
        val chartRadius = size.minDimension / 2f
        val holeRadius = chartRadius * 0.5f

        data.forEach { entry -&amp;gt;
            val sweepAngle = (entry.value / totalValue) * 360f
            val percentage = (entry.value / totalValue * 100)
            val formattedPercentage = &quot;%.1f&quot;.format(percentage)
            val midAngle = (startAngle + sweepAngle / 2f) % 360f

            drawArc(
                color = entry.color,
                startAngle = startAngle,
                sweepAngle = sweepAngle,
                useCenter = true,
            )

            // 연결 선과 라벨 그리기
            drawLabelWithLine(
                text = &quot;${entry.label}\n${formattedPercentage}%&quot;,
                angle = midAngle,
                innerRadius = chartRadius, // 선 시작점 (파이 차트의 반지름)
                outerRadius = chartRadius * 1.2f, // 선이 끝나는 점
                color = entry.color,
            )

            startAngle += sweepAngle
        }

        drawCircle(color = Color.White, radius = holeRadius, center = center)
    }
}

private fun DrawScope.drawLabelWithLine(
    text: String,
    angle: Float,
    innerRadius: Float,
    outerRadius: Float,
    color: Color,
) {
    val angleInRadians = toRadians(angle.toDouble()).toFloat()

    val textPaint = Paint().asFrameworkPaint().apply {
        isAntiAlias = true
        textSize = 14.sp.toPx()
        textAlign = android.graphics.Paint.Align.LEFT
    }

    // 선의 시작점
    val lineStartX = center.x + innerRadius * cos(angleInRadians)
    val lineStartY = center.y + innerRadius * sin(angleInRadians)

    // 선의 끝점
    val lineEndX = center.x + outerRadius * cos(angleInRadians)
    val lineEndY = center.y + outerRadius * sin(angleInRadians)

    // 선 그리기
    drawLine(
        color = color,
        start = Offset(lineStartX, lineStartY),
        end = Offset(lineEndX, lineEndY),
        strokeWidth = 2.dp.toPx()
    )

    // 텍스트 위치 계산
    val lines = text.split(&quot;\n&quot;)
    val textPadding = 8.dp.toPx()
    val lineHeight = textPaint.fontSpacing

    val textAnchorX: Float
    val textAnchorY: Float = lineEndY - (lines.size - 1) * lineHeight / 2

    if (angle &amp;gt; 90f &amp;amp;&amp;amp; angle &amp;lt; 270f) {
        textAnchorX = lineEndX - textPadding
        textPaint.textAlign = android.graphics.Paint.Align.RIGHT
    } else {
        textAnchorX = lineEndX + textPadding
        textPaint.textAlign = android.graphics.Paint.Align.LEFT
    }


    lines.forEachIndexed { index, line -&amp;gt;
        drawContext.canvas.nativeCanvas.drawText(
            line,
            textAnchorX,
            textAnchorY + index * lineHeight,
            textPaint
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;433&quot; data-origin-height=&quot;466&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bN9TWf/btsPooCXprc/KtMQUEas5hnBJaCIi8N3dK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bN9TWf/btsPooCXprc/KtMQUEas5hnBJaCIi8N3dK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bN9TWf/btsPooCXprc/KtMQUEas5hnBJaCIi8N3dK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbN9TWf%2FbtsPooCXprc%2FKtMQUEas5hnBJaCIi8N3dK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;433&quot; height=&quot;466&quot; data-origin-width=&quot;433&quot; data-origin-height=&quot;466&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 마무리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이 차트를 그리는 것은 생각보다 어렵지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 선과 라벨을 추가하는 부분부터 난이도가 급격히 상승했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각도를 계산해서 선을 그리고 텍스트의 위치를 지정하는 과정이 매우 까다로웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 텍스트 위치를 지정하는 곳에서 버그가 나서 하나하나 디버깅으로 값을 보면서 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 불필요한 라이브러리 없이 Compose만으로 커스텀 UI를 만들어본 것은 처음이라 재밌었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 코드는 아래 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/Meezzi/custom-pie-chart-android&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/Meezzi/custom-pie-chart-android&lt;/a&gt;&lt;/p&gt;</description>
      <category>Android</category>
      <category>android</category>
      <category>chart</category>
      <category>Compose</category>
      <category>MPAndroidChart</category>
      <author>Meezzi</author>
      <guid isPermaLink="true">https://sfida.tistory.com/167</guid>
      <comments>https://sfida.tistory.com/167#entry167comment</comments>
      <pubDate>Fri, 18 Jul 2025 09:34:02 +0900</pubDate>
    </item>
    <item>
      <title>[Android] MPAndroidChart의 PieChart를 분석해서 나만의 CustomPieChart 만들기 (1)</title>
      <link>https://sfida.tistory.com/166</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드의 유명한 차트 라이브러리인 &lt;a href=&quot;https://github.com/PhilJay/MPAndroidChart&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MPAndroidChart&lt;/a&gt;의 PieChart를 분석해 봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 라이브러리의 PieChart를 Compose로 나타낸 것이다.&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;AndroidView(
    factory = { context -&amp;gt;
        PieChart(context).apply {
            // 기본 설정
            description.isEnabled = false
            isRotationEnabled = false
            isHighlightPerTapEnabled = true
            isDrawHoleEnabled = true
            holeRadius = 50f

            setDrawEntryLabels(true)
            setUsePercentValues(true)

            // 범례 설정
            legend.isEnabled = true
        }
    },
    modifier = modifier,
    update = { chart -&amp;gt;
        // 데이터 설정
        val dataSet = PieDataSet(data, &quot;&quot;).apply {
            colors = listOf(
                &quot;#FF6B6B&quot;.toColorInt(),
                &quot;#4ECDC4&quot;.toColorInt(),
                &quot;#45B7D1&quot;.toColorInt(),
                &quot;#96CEB4&quot;.toColorInt(),
                &quot;#FFEAA7&quot;.toColorInt()
            )
            valueTextSize = 12f
            valueTextColor = Color.BLACK
        }

        chart.data = PieData(dataSet)
        chart.invalidate()
    }
)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;MPAndroidChart 라이브러리가 꽤나 오래전에 자바로 개발되었기 때문에 자바 스타일 컨벤션이 많이 남아 있었다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이를 분석해서 나만의 PieChart를 만들 예정이기에 개요에서는 주로 &lt;b&gt;보완할 내용에 집중&lt;/b&gt;했다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;1) 차트의 색상&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 &lt;b&gt;차트의 색상&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 데이터가 5개라면 차트 색상을 5개 넣어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 데이터 개수만큼 색상을 설정하지 않으면 누락이 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 방지하기 위해 색상을 필수로 넣도록 강제하는 기능이 있었으면 좋겠다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째로 Color의 표현 방식도 개선하고 싶었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 코드에서는 &lt;b&gt;android.graphics.Color&lt;/b&gt;를 사용했으며, 이는 내부적으로 색상을 Int 값으로 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면, 현재 Compose에서는 &lt;b&gt;androidx.compose.ui.graphics.Color&lt;/b&gt;를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Int 기반 색상 표현은 RGB값만 봤을 때 어떤 색깔인지 명확하지 않아 개선할 필요가 있어 보였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) 접근 방식&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 것은 속성값으로 접근하고, 어떤 것은 메서드로 동작한다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 isDrawHoleEnabled는 속성으로 접근하지만 setDrawEntryLabels나 setUsePercentValues는 메서드로 접근한다.&lt;/p&gt;
&lt;pre id=&quot;code_1752768475853&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;PieChart(context).apply {
    // 기본 설정
    description.isEnabled = false
    isRotationEnabled = false
    isHighlightPerTapEnabled = true
    isDrawHoleEnabled = true
    holeRadius = 50f

    setDrawEntryLabels(true)
    setUsePercentValues(true)

    // 범례 설정
    legend.isEnabled = true
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통일하는 것이 좋을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 이 코드를 작성하면,&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;setDrawHoleEnabled(true)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;'Use of setter method instead of property access syntax'라는&lt;/b&gt; 경고가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 아래처럼 수정해야 한다.&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;isDrawHoleEnabled = true&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 왜 setDrawEntryLabels, setUsePercentValues는 안될까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그건 PieChart에 getter/setter 함수가 없기 때문이라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;setDrawHoleEnabled는 getter/setter가 있지만 setDrawEntryLabels와 setUsePercentValues는 없다는 게 통일이 안 되는 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이번 커스텀 차트에서는 이를 통일해보려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 데이터 분석&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째로 파이 차트를 구성하는데 필요한 데이터를 분석해 보자.&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;val pieData = remember {
    listOf(
        PieEntry(30f, &quot;국밥&quot;),
        PieEntry(25f, &quot;초밥&quot;),
        PieEntry(20f, &quot;떡볶이&quot;),
        PieEntry(15f, &quot;치킨&quot;),
        PieEntry(10f, &quot;연어&quot;)
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이차트에 필요한 데이터는 &lt;b&gt;PieEntry&lt;/b&gt;를 통해 구성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PieEntry는 파이차트의 &lt;b&gt;각 영역에 대한 크기&lt;/b&gt;와 &lt;b&gt;카테고리 이름&lt;/b&gt;을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PieEntry는 Entry를 상속받았고, Entry는 BaseEntry를 상속&lt;/b&gt;받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 PieEntry의 내부에 대해 알아보자.&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;public PieEntry(float value, String label) {
    super(0f, value);
    this.label = label;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;만약 PieEntry(30f, &quot;국밥&quot;)이라면,&lt;br /&gt;첫 번째 인자값은 차트 전체에서 30만큼의 비중을 가진다는 의미로 실제 파이 차트의 크기를 결정하고,&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;두 번째 인자값은 해당 영역의 카테고리 이름이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 super를 통해 부모 클래스의 생성자를 호출한 후, label을 초기화한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;/**
 * A Entry represents one single entry in the chart.
 *
 * @param x the x value
 * @param y the y value (the actual value of the entry)
 */
public Entry(float x, float y) {
    super(y);
    this.x = x;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Entry 클래스로 왔다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 각 매개변수명은 x와 y인데 이는 x축으로 가로축을 의미하고, y는 y축으로 세로축을 의미한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;파이차트이기 때문에 앞서 x축에 값이 있을 필요가 없다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그렇기에 PieEntry의 x에 0f가 들어갔던 것이고, y는 value가 된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서도 Entry의 부모 클래스인 BaseEntry를 호출하고, 전달받은 x값을 초기화한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;public BaseEntry(float y) {
    this.y = y;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;최상단 부모 클래스인 BaseEntry이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 y 값을 초기화 함을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아마 PieEntry의 첫 번째 인자값(30f)을 토대로 데이터를 구성하는 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 &lt;b&gt;PieEntry는 x와 y값의 초기화를 Entry에 위임한 것이고, Entry는 y값의 초기화를 BaseEntry에 위임&lt;/b&gt;한 것이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;PieEntry -&amp;gt; Entry -&amp;gt; BaseEntry&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 왜 이렇게 번거로운 절차를 거쳐야 했을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BaseEntry의 abstract class에서 첫 번째 답을 찾았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유는 &lt;b&gt;일관성&lt;/b&gt; 때문이라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 부모 클래스인 &lt;b&gt;BaseEntry는 abstract class&lt;/b&gt;이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 구현체가 아닌 해당 속성과 메서드가 있어야 한다고 강제하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 구현하는 Entry 클래스는 모든 차트의 기초가 되고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PieEntry 같은 세부 차트는 Entry를 상속받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아마 차트를 만드는데 필요한 속성을 일관되게 적용하기 위해서가 아닐까라고 추측해 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째는 &lt;b&gt;캡슐화와 책임분리&lt;/b&gt;라고 생각하는데 이는 BaseEntry의 y값은 private으로 선언되어 있다는 점에서 답을 찾았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자식 클래스는 부모의 private 필드에 접근할 수 없으므로 부모가 제공하는 생성자를 통해서만 초기화할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, super를 통해 부모 클래스에게 필드를 초기화하는 책임을 위임한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 PieEntry에서는 label만 초기화한 후, x와 y의 초기화는 Entry로 위임하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Entry는 x만 초기화한 후, y의 초기화는 BaseEntry로 위임하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 캡슐화와 책임분리를 했다고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;474&quot; data-origin-height=&quot;858&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cMie61/btsPp7bnZav/KwqPTYgWInc6gPguYxiKzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cMie61/btsPp7bnZav/KwqPTYgWInc6gPguYxiKzK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cMie61/btsPp7bnZav/KwqPTYgWInc6gPguYxiKzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcMie61%2FbtsPp7bnZav%2FKwqPTYgWInc6gPguYxiKzK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;474&quot; height=&quot;858&quot; data-origin-width=&quot;474&quot; data-origin-height=&quot;858&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 파이차트 분석&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제는 파이차트를 실제로 그리는 부분에 대해 분석해 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 메서드가 많아 주요한 메서드만 분석했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) PieChart 내부&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 &lt;b&gt;PieChart는 PieRadarChartBase를 상속받고, PieRadarChartBase는 Chart를 상속&lt;/b&gt;받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PieChart 객체가 생성될 때 내부적으로 init() 메서드부터 시작해서 차트가 그려진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;init() : 차트 초기화 및 렌더러 설정&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@Override
protected void init() {
    super.init();

    mRenderer = new PieChartRenderer(this, mAnimator, mViewPortHandler);
    mXAxis = null;

    mHighlighter = new PieHighlighter(this);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;onDraw() : 차트 그리기 (렌더러를 통해 데이터와 하이라이트, 범례 등을 화면에 그림)&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    if (mData == null)
        return;

    mRenderer.drawData(canvas);

    if (valuesToHighlight())
        mRenderer.drawHighlighted(canvas, mIndicesToHighlight);

    mRenderer.drawExtras(canvas);

    mRenderer.drawValues(canvas);

    mLegendRenderer.renderLegend(canvas);

    drawDescription(canvas);

    drawMarkers(canvas);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;calculateOffsets() : 차트 중심점, 반지름, 오프셋 계산&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;calcMinMax() : 데이터의 최소/최대 값 계산. 파이차트에서 내부적으로 calcAngles() 호출&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;calcAngles() : 각 데이터 조각이 차지할 각도 계산&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;getMarkerPosition() : 터치된 조각의 마커 위치 계산&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;516&quot; data-origin-height=&quot;858&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2iLio/btsPp0cux6k/GgiO27KeL24i9nOLuT1KXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2iLio/btsPp0cux6k/GgiO27KeL24i9nOLuT1KXk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2iLio/btsPp0cux6k/GgiO27KeL24i9nOLuT1KXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2iLio%2FbtsPp0cux6k%2FGgiO27KeL24i9nOLuT1KXk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;516&quot; height=&quot;858&quot; data-origin-width=&quot;516&quot; data-origin-height=&quot;858&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) Renderer 내부&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;init메서드 내부를 보면 PieChartRenderer 객체를 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 차트는 Renderer 객체를 통해 그려지며, Renderer가 데이터를 해석해서 Canvas에 그려줘야 화면에 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PieChartRenderer는 DataRenderer를 상속받고, DataRenderer는 Renderer를 상속받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번엔 Renderer부터 확인해 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Renderer는 모든 Rendere의 최상위 추상 클래스로 공통 속성인 ViewPortHandler를 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ViewPortHandler는 차트의 화면 영역, 크기, 스케일, 오프셋 등을 관리하는 유틸리티 클래스로 차트가 그려질 수 있는 Canvas 영역을 정의한다.&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;public abstract class Renderer {

    /**
     * the component that handles the drawing area of the chart and it's offsets
     */
    protected ViewPortHandler mViewPortHandler;

    public Renderer(ViewPortHandler viewPortHandler) {
        this.mViewPortHandler = viewPortHandler;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DataRenderer는 모든 차트에서 공통으로 사용하는 렌더링의 부모 클래스로 관련 속성과 메서드를 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 데이터 중심의 차트를 그리는 Renderer의 기본 골격을 잡아준다.&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public DataRenderer(ChartAnimator animator, ViewPortHandler viewPortHandler) {
    super(viewPortHandler);
    this.mAnimator = animator;

    mRenderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mRenderPaint.setStyle(Style.FILL);

    mDrawPaint = new Paint(Paint.DITHER_FLAG);

    mValuePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mValuePaint.setColor(Color.rgb(63, 63, 63));
    mValuePaint.setTextAlign(Align.CENTER);
    mValuePaint.setTextSize(Utils.convertDpToPixel(9f));

    mHighlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mHighlightPaint.setStyle(Paint.Style.STROKE);
    mHighlightPaint.setStrokeWidth(2f);
    mHighlightPaint.setColor(Color.rgb(255, 187, 115));
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 PieChartRenderer는 파이차트만 그리는 Renderer로 파이차트에 특화된 페인트들이 추가된다.&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;public PieChartRenderer(PieChart chart, ChartAnimator animator,
                        ViewPortHandler viewPortHandler) {
    super(animator, viewPortHandler);
    mChart = chart;

    mHolePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mHolePaint.setColor(Color.WHITE);
    mHolePaint.setStyle(Style.FILL);

    mTransparentCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mTransparentCirclePaint.setColor(Color.WHITE);
    mTransparentCirclePaint.setStyle(Style.FILL);
    mTransparentCirclePaint.setAlpha(105);

    mCenterTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
    mCenterTextPaint.setColor(Color.BLACK);
    mCenterTextPaint.setTextSize(Utils.convertDpToPixel(12f));

    mValuePaint.setTextSize(Utils.convertDpToPixel(13f));
    mValuePaint.setColor(Color.WHITE);
    mValuePaint.setTextAlign(Align.CENTER);

    mEntryLabelsPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mEntryLabelsPaint.setColor(Color.WHITE);
    mEntryLabelsPaint.setTextAlign(Align.CENTER);
    mEntryLabelsPaint.setTextSize(Utils.convertDpToPixel(13f));

    mValueLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mValueLinePaint.setStyle(Style.STROKE);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 Renderer는 ViewPortHandler만 관리하여 그릴 영역을 관리하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DataRender는 페인트, 애니메이션 값, 그리기 기능의 공통 로직을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 PieChartRenderer는 파이차트 전용 페인트, 비트맵, 실게 그리기를 구현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;507&quot; data-origin-height=&quot;863&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mt1j9/btsPqJ16pIn/xNDzIRqx5X6u7zrwcSt9nK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mt1j9/btsPqJ16pIn/xNDzIRqx5X6u7zrwcSt9nK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mt1j9/btsPqJ16pIn/xNDzIRqx5X6u7zrwcSt9nK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fmt1j9%2FbtsPqJ16pIn%2FxNDzIRqx5X6u7zrwcSt9nK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;507&quot; height=&quot;863&quot; data-origin-width=&quot;507&quot; data-origin-height=&quot;863&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음으로 라이브러리의 구조에 대한 분석을 해봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각보다 복잡하고 다양한 경우의 수를 고려하여 설계한 것이 느껴졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 상속구조를 통한 일관성 유지, 캡슐화, 책임 분리 등 많은 것을 배울 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 분석을 바탕으로 Compose로 나만의 PieChart를 구현해 볼 계획이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현했습니다. 링크는 여기.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://sfida.tistory.com/167&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://sfida.tistory.com/167&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1752996903186&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Android] MPAndroidChart의 PieChart를 분석해서 나만의 CustomPieChart 만들기 (2)&quot; data-og-description=&quot;1. 개요앞서 MPAndroidChart를 분석했다. 이번에는 분석한 내용을 바탕으로 나만의 PieChart를 구현해보고자 한다. 2. 데이터 정의MPAndroidChart에서는 PieEntry를 통해 파이 차트를 그리는데 필요한 데이터&quot; data-og-host=&quot;sfida.tistory.com&quot; data-og-source-url=&quot;https://sfida.tistory.com/167&quot; data-og-url=&quot;https://sfida.tistory.com/167&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cU6jkN/hyZnbgkuPD/h5ke5nf7h8x3akMbgUJls1/img.png?width=433&amp;amp;height=466&amp;amp;face=0_0_433_466,https://scrap.kakaocdn.net/dn/ngvs7/hyZnma5P7z/MhZlkKumDiBRscpvqTVZJK/img.png?width=433&amp;amp;height=466&amp;amp;face=0_0_433_466,https://scrap.kakaocdn.net/dn/71whU/hyZnANIVkQ/oaXMwaXtLEteW6x47vjNb1/img.png?width=433&amp;amp;height=466&amp;amp;face=0_0_433_466&quot;&gt;&lt;a href=&quot;https://sfida.tistory.com/167&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://sfida.tistory.com/167&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cU6jkN/hyZnbgkuPD/h5ke5nf7h8x3akMbgUJls1/img.png?width=433&amp;amp;height=466&amp;amp;face=0_0_433_466,https://scrap.kakaocdn.net/dn/ngvs7/hyZnma5P7z/MhZlkKumDiBRscpvqTVZJK/img.png?width=433&amp;amp;height=466&amp;amp;face=0_0_433_466,https://scrap.kakaocdn.net/dn/71whU/hyZnANIVkQ/oaXMwaXtLEteW6x47vjNb1/img.png?width=433&amp;amp;height=466&amp;amp;face=0_0_433_466');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Android] MPAndroidChart의 PieChart를 분석해서 나만의 CustomPieChart 만들기 (2)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;1. 개요앞서 MPAndroidChart를 분석했다. 이번에는 분석한 내용을 바탕으로 나만의 PieChart를 구현해보고자 한다. 2. 데이터 정의MPAndroidChart에서는 PieEntry를 통해 파이 차트를 그리는데 필요한 데이터&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;sfida.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 관련 &lt;a href=&quot;https://github.com/Meezzi/custom-pie-chart-android&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;GitHub Repository&lt;/a&gt;입니다.&lt;/p&gt;
&lt;figure id=&quot;og_1752996910635&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - Meezzi/custom-pie-chart-android: Compose로 직접 구현한 Pie Chart&quot; data-og-description=&quot;Compose로 직접 구현한 Pie Chart. Contribute to Meezzi/custom-pie-chart-android development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/Meezzi/custom-pie-chart-android&quot; data-og-url=&quot;https://github.com/Meezzi/custom-pie-chart-android&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/hJILL/hyZnAGWSmX/CchtGn5w8XQ27EQtRw2Tw0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cVvnrE/hyZm9Jy1Fu/nc4uFs1Z0h770tK6zEkdak/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/Meezzi/custom-pie-chart-android&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/Meezzi/custom-pie-chart-android&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/hJILL/hyZnAGWSmX/CchtGn5w8XQ27EQtRw2Tw0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cVvnrE/hyZm9Jy1Fu/nc4uFs1Z0h770tK6zEkdak/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - Meezzi/custom-pie-chart-android: Compose로 직접 구현한 Pie Chart&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Compose로 직접 구현한 Pie Chart. Contribute to Meezzi/custom-pie-chart-android development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Android</category>
      <category>android</category>
      <category>chart</category>
      <category>Compose</category>
      <category>MPAndroidChart</category>
      <category>Piechart</category>
      <author>Meezzi</author>
      <guid isPermaLink="true">https://sfida.tistory.com/166</guid>
      <comments>https://sfida.tistory.com/166#entry166comment</comments>
      <pubDate>Fri, 18 Jul 2025 09:08:50 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] mocktail Bad state: A test tried to use `any` or `captureAny` on a parameter of type 오류 해결 트러블슈팅</title>
      <link>https://sfida.tistory.com/165</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mocktail을 이용하여 테스트 코드를 작성하던 중 마주친 오류와 해결 방안에 대해 서술했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 문제 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 제 아키텍처는 다음과 같은 계층 구조로 데이터가 흐릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DataSource -&amp;gt; Repository -&amp;gt; UseCase -&amp;gt; ViewModel&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 DataSource에서 외부 API와 통신을 하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과를 Repository에 반환한 후,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UseCase, ViewModel로 이어지는 구조입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 Repository 테스트 코드를 작성하던 중 다음과 같은 오류가 발생했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1751165096506&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Bad state: A test tried to use `any` or `captureAny` on a parameter of type `DreamDto`, but
registerFallbackValue was not previously called to register a fallback value for `DreamDto`.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해석하자면 DreamDto 타입의 인자에 대해 any 또는 captureAny를 사용하려 했지만, mocktail이 해당 타입의 더미 인스턴스를 생성할 수 없기 때문에 &lt;b&gt;registerFallbackValue를 통해 명시적으로 fallback 객체를 등록&lt;/b&gt;해줘야 한다는 뜻입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fafafa;&quot;&gt;any와 capture에 대한 설명은 &lt;b&gt;더보기&lt;/b&gt;를 클릭해주세요.&lt;/span&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px; background-color: #fafafa; caret-color: auto;&quot;&gt;mocktail로 테스트할 때 확인하고 싶은게 있을 것입니다.&lt;br /&gt;&lt;br /&gt;1. 이 메서드가 &lt;b&gt;실제로 호출됐는지&lt;/b&gt; 확인하고 싶다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px; background-color: #fafafa; caret-color: auto;&quot;&gt;2. 메서드에 &lt;b&gt;무슨 값이 전달됐는지&lt;/b&gt; 알고 싶다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px; background-color: #fafafa; caret-color: auto;&quot;&gt;3. 값이 뭐든 상관없이 &lt;b&gt;일단 호출됐는지&lt;/b&gt;만 확인하고 싶다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px; background-color: #fafafa; caret-color: auto;&quot;&gt;이때 사용하는 도구가 바로 any와 captureAny입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;any&amp;lt;T&amp;gt;()는 T 타입의 어떤 값이든 허용되는 인자로 인자값이 무엇인지에는 관심이 없고, 메서드가 호출되었는지만 확인하고 싶을 때 사용합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;captureAny&amp;lt;T&amp;gt;()는 메서드 호출 시 전달된 실제 인자 값을 캡처해서 나중에 확인할 수 있는 matcher로, 어떤 값이 전달되었는지 확인하고 싶을 때 사용합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제 코드에서 문제가 되는 곳은 이 부분이었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1751163081659&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;test('returns the uid of the dream when saving is successful', () async {
    // Arrange
    when(
      () =&amp;gt; dreamSaveDataSource!.saveDream(any()),
    ).thenAnswer((_) async =&amp;gt; 1);

    // Act
    final response = await remoteDreamRepository?.saveDream(dream);

    // Assert
    expect(response, 1);
    verify(() =&amp;gt; dreamSaveDataSource!.saveDream(any())).called(1);
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;꿈 저장에 성공하면 uid를 반환값으로 돌려주는 코드가 제대로 실행되었는지 테스트하는 코드입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 유의깊게 봐야할 점은 4번째 줄입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1751166650570&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;when(() =&amp;gt; dreamSaveDataSource!.saveDream(any()))&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dreamSaveDataSource의 saveDream 메서드는 인자값이 DreamDto 타입이 들어갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분을 any()라고 설정한 것이 문제가 되었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;any나 captureAny를 사용할 때는 T 타입의 유효한 객체를 내부적으로 만들어야하기 때문에 T가 사용자 정의 타입일 경우, 명시적으로 등록된 더미 객체가 필요했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DreamDto는 사용자 정의 타입이기 때문에 &lt;b&gt;mocktail이 임의로 객체를 생성할 수 없어 에러가 발생한 것&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 해결 방안&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트가 실패했을 때 TEST RESULTS를 보면 해결 방법이 친절하게 제시되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fake를 상속받은 Fake 객체를 만들고 이를 SetUpAll로 더미 객체를 등록하는 방법입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1751163608811&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class MyTypeFake extends Fake implements MyType {}

void main() {
  setUpAll(() {
    registerFallbackValue(MyTypeFake());
  });
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 필요한 FakeDreamDto를 만들고 이를 registerFallbackValue에 등록해서 해결했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1751164892370&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class FakeDreamDto extends Fake implements DreamDto {}

void main() {
  setUpAll(() {
    registerFallbackValue(FakeDreamDto());
  });
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 mocktail은 any&amp;lt;DreamDto&amp;gt;()를 사용할 때 내부적으로 FakeDreamDto 인스턴스를 사용하게 되어 테스트가 정상적으로 작동합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Flutter</category>
      <category>fake</category>
      <category>fakedata</category>
      <category>Flutter</category>
      <category>mocktail</category>
      <category>registerfallbackvalue</category>
      <category>TDD</category>
      <category>TEST</category>
      <category>unit test</category>
      <author>Meezzi</author>
      <guid isPermaLink="true">https://sfida.tistory.com/165</guid>
      <comments>https://sfida.tistory.com/165#entry165comment</comments>
      <pubDate>Sun, 29 Jun 2025 12:17:23 +0900</pubDate>
    </item>
    <item>
      <title>[TIL] 250515 트러블 슈팅</title>
      <link>https://sfida.tistory.com/163</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 트러블 슈팅&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://sfida.tistory.com/161&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://sfida.tistory.com/161&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>TIL</category>
      <category>til</category>
      <author>Meezzi</author>
      <guid isPermaLink="true">https://sfida.tistory.com/163</guid>
      <comments>https://sfida.tistory.com/163#entry163comment</comments>
      <pubDate>Fri, 16 May 2025 22:26:58 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] TMDB로 영화관 앱 만들기</title>
      <link>https://sfida.tistory.com/162</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 프로젝트 소개&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;b&gt;Flicksy&lt;/b&gt;는 Flutter로 개발된 영화 정보 앱으로, TMDB(The Movie Database) API를 활용하여 현재 상영 중인 영화, 인기 영화, 평점 높은 영화, 개봉 예정 영화 등의 정보를 제공합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;사용자는 영화의 포스터, 개봉일, 영화 설명, 장르, 흥행 정보, 제작사 로고 등 다양한 세부 정보를 확인할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 주요 기능&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;nbsp;TMDB 영화 데이터 연동&lt;/li&gt;
&lt;li&gt;&amp;nbsp;클린 아키텍처 기반 구조&lt;/li&gt;
&lt;li&gt;&amp;nbsp;당겨서 새로고침(Pull to refresh)&lt;/li&gt;
&lt;li&gt;&amp;nbsp;Riverpod을 활용한 상태 관리&lt;/li&gt;
&lt;li&gt;&amp;nbsp;영화 상세 페이지에서 다양한 정보 제공&lt;/li&gt;
&lt;li&gt;&amp;nbsp;.env 파일을 통한 API 키 관리&lt;/li&gt;
&lt;li&gt;화면 이동시 Hero 애니메이션&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 프로젝트 구조&lt;/h2&gt;
&lt;pre id=&quot;code_1747312216891&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;lib/
├── feature/                      # 기능 단위로 나눈 구조
│   └── home/                     # '홈' 도메인의 모든 코드
│       ├── core/                 # 홈 기능에 한정된 공통 상수
│       │   ├── constants/        # 포스터 URL 등
│       │        └── poster_base_url.dart
│       │
│       ├── data/                 # 외부 데이터 소스 (API, Provider 등)
│       │   ├── dtos/             # TMDB API 응답 모델 정의
│       │   ├── providers/        # 데이터 계층의 Provider
│       │   ├── repositories/     # 데이터 구현체 (Repository Impl)
│       │   └── services/         # TMDB API 호출 서비스
│       │
│       ├── domain/               # 비즈니스 로직 계층
│       │   ├── entities/         # 핵심 Entity 정의
│       │   ├── providers/        # 유스케이스 Provider
│       │   ├── repositories/     # 추상화된 Repository 인터페이스
│       │   └── usecases/         # UseCase 클래스
│       │
│       └── presentation/         # UI 및 상태 관리 계층
│           ├── detail/           # 영화 상세 화면
│           │   ├── widgets/      # 상세 화면용 위젯
│           │   └── detail_page.dart
│           │
│           ├── home/             # 홈 화면
│           │   └──  widgets/     # 홈 UI 구성 위젯
│           │
│           ├── providers/        # ViewModel Provider 정의
│           └── viewmodels/       # ViewModel 정의
│
└── main.dart                     # 앱 진입점&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 트러블슈팅&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://sfida.tistory.com/161&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;기능 우선 분리&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. GitHub&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/Meezzi/flicksy-app&quot;&gt;https://github.com/Meezzi/flicksy-app&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 회고&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) TMDB API 연동 및 클린 아키텍처 적용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TMDB API를 연동하면서 Service, Repository, Usecase, UI, 테스트 코드를 작성했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요구하는 필드가 많아 DTO, Entity를 작성하는 데 꽤 시간이 걸렸고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 계층에서 Null 데이터를 처리해야 할지, Null이 나오면 어떻게 할지 명확하지 않아 혼란스러웠다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 전체적인 구조를 클린 아키텍처에 맞춰 정리함으로써 어느 정도 이해할 수 있게 된 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) Hero 애니메이션 적용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면을 이동할 때 Hero 애니메이션을 적용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 tag를 설정하는 곳에서 고민이 많았다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 화면에 동일한 사진이 3장(가장 인기 있는 영화, 인기 있는 영화 리스트, 상영 중인 영화 리스트)이 있어 동일한 tag를 사용하지 않도록 주의가 필요했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 popular, nowPlaying이라는 섹션 ID를 붙여 해결했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 입장에서 화면 이동이 매끄럽게 이어져 좋은 점이 많은 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Clean Architecture와 Riverpod을 이용한 상태관리, MVVM 패턴을 적용하며 앱을 개발하였다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념적으로는 어느정도 이해하고 있었는데 직접 실전에서 겪으니 아직 어려운 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;향후 프로젝트에도 적용하여 더 익숙하게 느껴지도록 연습이 필요한 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Flutter</category>
      <category>Flutter</category>
      <category>tmdb</category>
      <category>영화 앱</category>
      <author>Meezzi</author>
      <guid isPermaLink="true">https://sfida.tistory.com/162</guid>
      <comments>https://sfida.tistory.com/162#entry162comment</comments>
      <pubDate>Thu, 15 May 2025 10:36:14 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] 기능 우선 분리(Feature first Structure)로 영화관 앱 구조 설계하기</title>
      <link>https://sfida.tistory.com/161</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클린 아키텍처를 대해 공부하면서 알게 된 주요한 분리 방식 두 가지가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째는 계층 우선 분리고, 두 번째는 기능 우선 분리이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 &lt;b&gt;계층 우선 분리&lt;/b&gt;는 기능보다는 &lt;b&gt;계층에 따라 폴더를 나누는 방식&lt;/b&gt;으로 각 계층별 책임이 명확하다는 장점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 기능이 추가됨에 따라 관련 파일들이 여기저기 흩어지기 때문에 파일들을 추적하거나 관리하기가 쉽지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 &lt;b&gt;기능 우선 분리&lt;/b&gt;는 &lt;b&gt;기능을 기준으로 폴더를 나누는 방식&lt;/b&gt;으로 특정 기능과 관련된 모든 코드가 한 곳에 있어 유지보수가 용이하고, 모듈화, 리팩토링, 테스트가 쉽다는 장점이 있지만, 폴더가 많아진다는 단점도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 러닝 앱을 구현하였을 때 크게 4가지 기능(로그인, 지도에 위치 표시, 채팅, 러닝 결과 계산)이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당시에 계층 우선 분리로 분리를 하였지만 코드 구조가 복잡하고 관리하기 어렵다는 단점을 느꼈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 경험을 바탕으로 기존의 계층 우선 분리 방식에서 기능 우선 분리 방식으로 구조를 구성하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. api 문서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.themoviedb.org/reference/intro/getting-started&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.themoviedb.org/reference/intro/getting-started&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 요구사항&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) HomePage&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HomePage는 총 5개의 영화 리스트를 보여준다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가장 인기 있는 영화(1개)&lt;/li&gt;
&lt;li&gt;현재 상영중인 영화&lt;/li&gt;
&lt;li&gt;인기있는 영화&lt;/li&gt;
&lt;li&gt;평점 높은 영화&lt;/li&gt;
&lt;li&gt;개봉 예정 영화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) DetailPage&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HomePage에서 클릭한 영화에 대한 상세 정보를 보여준다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;영화 포스터&lt;/li&gt;
&lt;li&gt;영화 이름&lt;/li&gt;
&lt;li&gt;개봉일&lt;/li&gt;
&lt;li&gt;설명&lt;/li&gt;
&lt;li&gt;흥행 정보&lt;/li&gt;
&lt;li&gt;제작 회사&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 프로젝트 구조 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HomePage와 DetailPage 두 화면 모두 영화 정보를 가져와서 표시하기 때문에 하나의 기능으로 묶었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;feature / home 폴더를 만들고 그 안에 data, domain, presentation 등 계층 구조를 분리하여 정리하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1747564448415&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;lib/
├── feature/                      # 기능 단위로 나눈 구조
│   └── home/                     # '홈' 도메인의 모든 코드
│       ├── core/                 # 홈 기능에 한정된 공통 상수
│       │   ├── constants/        # 포스터 URL 등
│       │        └── poster_base_url.dart
│       │
│       ├── data/                 # 외부 데이터 소스 (API, Provider 등)
│       │   ├── dtos/             # TMDB API 응답 모델 정의
│       │   ├── providers/        # 데이터 계층의 Provider
│       │   ├── repositories/     # 데이터 구현체 (Repository Impl)
│       │   └── services/         # TMDB API 호출 서비스
│       │
│       ├── domain/               # 비즈니스 로직 계층
│       │   ├── entities/         # 핵심 Entity 정의
│       │   ├── providers/        # 유스케이스 Provider
│       │   ├── repositories/     # 추상화된 Repository 인터페이스
│       │   └── usecases/         # UseCase 클래스
│       │
│       └── presentation/         # UI 및 상태 관리 계층
│           ├── detail/           # 영화 상세 화면
│           │   ├── widgets/      # 상세 화면용 위젯
│           │   └── detail_page.dart
│           │
│           ├── home/             # 홈 화면
│           │   └──  widgets/     # 홈 UI 구성 위젯
│           │
│           ├── providers/        # ViewModel Provider 정의
│           └── viewmodels/       # ViewModel 정의
│
└── main.dart                     # 앱 진입점&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 작성해야 할 파일&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DTO 설계 : TMDB 응답을 기반으로 모델 정의&lt;/li&gt;
&lt;li&gt;Service 인터페이스 및 구현체 : API 호출&lt;/li&gt;
&lt;li&gt;Service Provider : Riverpod 기반 상태 주입&lt;/li&gt;
&lt;li&gt;Entity 정의 : 도메인 모델 정의&lt;/li&gt;
&lt;li&gt;Repository 인터페이스 및 구현체 : 비즈니스 로직과 데이터 연결&lt;/li&gt;
&lt;li&gt;Repository Provider : 의존성 관리&lt;/li&gt;
&lt;li&gt;Usecase : 도메인 로직 캡슐화&lt;/li&gt;
&lt;li&gt;ViewModel : UI와 비즈니스 로직 사이에서 상태 관리&lt;/li&gt;
&lt;li&gt;ViewModel Provider : 의존성 관리&lt;/li&gt;
&lt;li&gt;UI : 사용자에게 표시&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 회고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능 우선 분리를 적용하여 개발을 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 기능이 많지 않아 기능 우선 분리에 대한 장점이 크게 와닿지는 않았지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능이 추가되고 복잡해질수록 기능별 코드가 명확하게 분리되어 파일을 잘 관리할 수 있을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 기존에 경험했던 계층 우선 분리 방식에는 서로 다른 기능의 파일들이 여러 계층에 흩어져 있어 코드를 추적하는데 어려움이 있었지만, 하나의 기능과 관련된 모든 코드가 한 폴더 내에 모여있어 구조적으로 더 직관적이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뿐만 아니라 Clean Architecture, MVVM, Riverpod 상태관리를 적용하면서 개념적으로 이해한 내용을 실전에서 적용해 보면서 의존성의 흐름과 역할 분리에 대해 더 이해할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 완벽하게 익숙하진 않지만 반복적으로 적용해보며 더 자연스럽게 사용할 수 있도록 연습해야겠다고 느꼈다.&lt;/p&gt;</description>
      <category>Flutter</category>
      <category>Flutter</category>
      <category>Mobile</category>
      <category>tmdb</category>
      <category>영화관 앱</category>
      <category>트러블슈팅</category>
      <author>Meezzi</author>
      <guid isPermaLink="true">https://sfida.tistory.com/161</guid>
      <comments>https://sfida.tistory.com/161#entry161comment</comments>
      <pubDate>Thu, 15 May 2025 10:35:52 +0900</pubDate>
    </item>
    <item>
      <title>[TIL] 250514 LeetCode 문제풀이, 개인 과제 구현</title>
      <link>https://sfida.tistory.com/160</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. LeetCode 문제풀이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://sfida.tistory.com/159&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://sfida.tistory.com/159&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1747229021599&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Dart] 15. 3Sum&quot; data-og-description=&quot;1. 문제https://leetcode.com/problems/3sum/description/?envType=study-plan-v2&amp;amp;envId=top-interview-150 2. 요구사항1) 정수배열 nums가 주어질 때, 총합이 0이 되는 모든 고유한 세 숫자의 조합을 찾아야 한다.2) 각 조합은 nu&quot; data-og-host=&quot;sfida.tistory.com&quot; data-og-source-url=&quot;https://sfida.tistory.com/159&quot; data-og-url=&quot;https://sfida.tistory.com/159&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cf0BNJ/hyYRzb0Bsr/WUy5qFOfIScnOrjo4tBTu1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bGzEbK/hyYRmcFqbw/rWyOk8XCGJuGhKoyvLFjOk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://sfida.tistory.com/159&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://sfida.tistory.com/159&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cf0BNJ/hyYRzb0Bsr/WUy5qFOfIScnOrjo4tBTu1/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bGzEbK/hyYRmcFqbw/rWyOk8XCGJuGhKoyvLFjOk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Dart] 15. 3Sum&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;1. 문제https://leetcode.com/problems/3sum/description/?envType=study-plan-v2&amp;amp;envId=top-interview-150 2. 요구사항1) 정수배열 nums가 주어질 때, 총합이 0이 되는 모든 고유한 세 숫자의 조합을 찾아야 한다.2) 각 조합은 nu&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;sfida.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 개인 과제 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- API 연동 및 데이터를 뷰에 표시&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Hero 위젯으로 애니메이션 구현&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Pull to Refresh 구현&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 회고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영화 앱을 만들긴 했는데 아직 기능이 많이 부족한 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유용한 기능을 좀 더 추가해서 포트폴리오로 낼 만큼의 완성도 높은 앱을 구현하고 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;++애자일 방법론에 관심이 생겨 책을 구매했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 다 읽어보진 않았는데 이번주 금요일부터 있을 팀 프로젝트부터 애자일을 적용해보고 싶다.&lt;/p&gt;</description>
      <category>TIL</category>
      <category>Flutter</category>
      <category>LeetCode</category>
      <category>til</category>
      <author>Meezzi</author>
      <guid isPermaLink="true">https://sfida.tistory.com/160</guid>
      <comments>https://sfida.tistory.com/160#entry160comment</comments>
      <pubDate>Wed, 14 May 2025 22:29:26 +0900</pubDate>
    </item>
    <item>
      <title>[Dart] 15. 3Sum</title>
      <link>https://sfida.tistory.com/159</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://leetcode.com/problems/3sum/description/?envType=study-plan-v2&amp;amp;envId=top-interview-150&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://leetcode.com/problems/3sum/description/?envType=study-plan-v2&amp;amp;envId=top-interview-150&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 요구사항&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 정수배열 nums가 주어질 때, 총합이 0이 되는 모든 고유한 세 숫자의 조합을 찾아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) 각 조합은 nums[i] + nums[j] + nums[k] == 0을 만족해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) 결과에는 중복된 조합이 포함되지 않아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 코드&lt;/h2&gt;
&lt;pre id=&quot;code_1747183201941&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Solution {
  List&amp;lt;List&amp;lt;int&amp;gt;&amp;gt; threeSum(List&amp;lt;int&amp;gt; nums) {
    // nums를 오름차순으로 정렬
    nums.sort();

    List&amp;lt;List&amp;lt;int&amp;gt;&amp;gt; list = [];

    // 첫 번째 숫자를 기준으로 반복
    // 세 수를 찾기 위해 최대 nums.length - 2까지만 반복
    for (int i = 0; i &amp;lt; nums.length - 2; i++) {

      // 중복된 값은 건너뜀
      // 이미 같은 값으로 처리했기 때문
      if (i &amp;gt; 0 &amp;amp;&amp;amp; nums[i] == nums[i - 1]) continue;

      int left = i + 1; // 왼쪽 포인터
      int right = nums.length - 1; // 오른쪽 포인터
      int target = -nums[i]; // 두 수의 합이 되어야 하는 값

      while (left &amp;lt; right) {
        int sum = nums[right] + nums[left];

        if (sum == target) {
          // 세 수의 합이 0이면 결과 리스트에 추가
          list.add([nums[i], nums[left], nums[right]]);
          
          // 중복된 값은 건너뜀
          while (left &amp;lt; right &amp;amp;&amp;amp; nums[left] == nums[left + 1]) left++;
          while (left &amp;lt; right &amp;amp;&amp;amp; nums[right] == nums[right - 1]) right--;

          // 다음 값으로 이동
          left++;
          right--;
        } else if (sum &amp;lt; target) {
          // 합이 작으면 왼쪽 포인터를 오른쪽으로 이동
          left++;
        } else {
          // 합이 크면 오른쪽 포인터를 왼쪽으로 이동
          right--;
        }
      }
    }

    return list;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Programming/LeetCode</category>
      <category>DART</category>
      <category>LeetCode</category>
      <author>Meezzi</author>
      <guid isPermaLink="true">https://sfida.tistory.com/159</guid>
      <comments>https://sfida.tistory.com/159#entry159comment</comments>
      <pubDate>Wed, 14 May 2025 10:06:19 +0900</pubDate>
    </item>
    <item>
      <title>[TIL] 250513 LeetCode, 개인 과제</title>
      <link>https://sfida.tistory.com/158</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. LeetCode 문제풀이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://sfida.tistory.com/157&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://sfida.tistory.com/157&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 개인 과제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- DTO 설계&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- Service, Repository 구현 및 테스트 코드 작성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 회고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 DTO 설계에 관해 시간을 많이 할애한 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 응답에서 어떤 값이 null이 올지 몰라 일단 디폴트 값이 없는 것은 null로 올 수 있다는 것을 가정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 후, null로 들어왔을 때 기본값을 설정해주는 방식으로 구현했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 테스트 코드도 작성해봤는데 어렵고 시간도 오래 걸려서 비효율적인 것 같다는 생각도 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 내가 작성한 코드를 직접 테스트해보며 검증할 수 있음에 뿌듯한 것 같다.&lt;/p&gt;</description>
      <category>TIL</category>
      <category>DART</category>
      <category>DTO</category>
      <category>Flutter</category>
      <category>LeetCode</category>
      <category>til</category>
      <author>Meezzi</author>
      <guid isPermaLink="true">https://sfida.tistory.com/158</guid>
      <comments>https://sfida.tistory.com/158#entry158comment</comments>
      <pubDate>Tue, 13 May 2025 23:41:18 +0900</pubDate>
    </item>
  </channel>
</rss>