<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>말랑한 하루</title>
    <link>https://malangdidoo.tistory.com/</link>
    <description>기억은 기록을 이길수 없다</description>
    <language>ko</language>
    <pubDate>Mon, 27 Jul 2026 03:15:32 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>지수는말랑이</managingEditor>
    <image>
      <title>말랑한 하루</title>
      <url>https://tistory1.daumcdn.net/tistory/3403180/attach/59ce28f5e83344eba847cfd38f13a123</url>
      <link>https://malangdidoo.tistory.com</link>
    </image>
    <item>
      <title>[BAEKJOON] 17266, 18352</title>
      <link>https://malangdidoo.tistory.com/273</link>
      <description>&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;※&amp;nbsp;소스코드는&amp;nbsp;각&amp;nbsp;문제&amp;nbsp;설명&amp;nbsp;하단에&amp;nbsp;&amp;lt;더보기&amp;gt;를&amp;nbsp;통해&amp;nbsp;확인하실&amp;nbsp;수&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;b&gt;[sliver4, 17266 어두운 굴다리] - 이분탐색&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제에서&amp;nbsp;요구하는&amp;nbsp;결과&amp;nbsp;값은&amp;nbsp;&quot;가로등의&amp;nbsp;높이&quot;이다. &lt;br /&gt;이를&amp;nbsp;pivot으로&amp;nbsp;두고&amp;nbsp;이분탐색을&amp;nbsp;진행하며,&amp;nbsp; &lt;br /&gt;가로등이&amp;nbsp;굴다리&amp;nbsp;전체를&amp;nbsp;비추고있는지&amp;nbsp;그렇지&amp;nbsp;않은지를&amp;nbsp;판단하여&amp;nbsp;mid값을&amp;nbsp;조정한다. &lt;br /&gt;&lt;br /&gt;굴다리&amp;nbsp;전체가&amp;nbsp;비춰지고&amp;nbsp;있다면,&amp;nbsp;현재&amp;nbsp;mid값은&amp;nbsp;최적이거나&amp;nbsp;너무&amp;nbsp;높은것이므로&amp;nbsp;right를&amp;nbsp;줄이고 &lt;br /&gt;굴다리&amp;nbsp;전체가&amp;nbsp;비춰지지&amp;nbsp;않고&amp;nbsp;있다면,&amp;nbsp;현재&amp;nbsp;mid값은&amp;nbsp;너무&amp;nbsp;낮은것이므로&amp;nbsp;left를&amp;nbsp;높인다. &lt;br /&gt;최적의&amp;nbsp;해를&amp;nbsp;찾아야하므로&amp;nbsp;left와&amp;nbsp;right가&amp;nbsp;동등할&amp;nbsp;때&amp;nbsp;까지&amp;nbsp;판단하는&amp;nbsp;것이&amp;nbsp;좋다. &lt;br /&gt;&lt;br /&gt;굴다리가&amp;nbsp;비춰지는지&amp;nbsp;판단하기&amp;nbsp;위한&amp;nbsp;핵심은&amp;nbsp;가로등을&amp;nbsp;기준으로 &lt;br /&gt;이전의&amp;nbsp;가로등(prev)가&amp;nbsp;비추는&amp;nbsp;오른쪽&amp;nbsp;끝값과,&amp;nbsp;이후의&amp;nbsp;가로등(next)가&amp;nbsp;비추는&amp;nbsp;왼쪽&amp;nbsp;끝값을&amp;nbsp;비교하여 &lt;br /&gt;겹쳐지지&amp;nbsp;않는&amp;nbsp;구간이&amp;nbsp;존재한다면&amp;nbsp;굴다리가&amp;nbsp;안비춰지고&amp;nbsp;있는&amp;nbsp;것이다. &lt;br /&gt;또한,&amp;nbsp;시작&amp;nbsp;및&amp;nbsp;끝&amp;nbsp;구간에&amp;nbsp;대해&amp;nbsp;0과&amp;nbsp;N을&amp;nbsp;기준으로&amp;nbsp; &lt;br /&gt;가로등&amp;nbsp;높이에&amp;nbsp;따른&amp;nbsp;예외처리를&amp;nbsp;먼저&amp;nbsp;진행해&amp;nbsp;준다면&amp;nbsp;더&amp;nbsp;쉽게&amp;nbsp;구현할&amp;nbsp;수&amp;nbsp;있을&amp;nbsp;것이다.&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;pre id=&quot;code_1714968782334&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;algorithm&amp;gt;
#pragma warning(disable:4996)
using namespace std;

int N, M, lamp[100001];

bool checkLamp(int h) {
	if (0 &amp;lt; lamp[0] - h) return false;
	if (lamp[M - 1] + h &amp;lt; N) return false;
	int prev = lamp[0] + h;
	for (int m = 1; m &amp;lt; M; m++) {
		int next = lamp[m] - h;
		if (prev &amp;lt; next) return false;
		prev = lamp[m] + h;
	}
	return true;
}

int main() {
	scanf(&quot;%d&quot;, &amp;amp;N);
	scanf(&quot;%d&quot;, &amp;amp;M);
	for (int m = 0; m &amp;lt; M; m++)
		scanf(&quot;%d&quot;, &amp;amp;lamp[m]);

	int left = 1;
	int right = 100000;
	int res = 100000;
	while (left &amp;lt;= right) {
		int mid = (left + right) / 2;
		printf(&quot;%d, %d -&amp;gt; %d\n&quot;, left, right, mid);
		if (checkLamp(mid)) {
			res = min(res, mid);
			right = mid - 1;
		}
		else {
			left = mid + 1;
		}
	}
	printf(&quot;%d&quot;, res);
}&lt;/code&gt;&lt;/pre&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;&lt;b&gt;[sliver2, 18352 특정 거리의 도시 찾기] - BFS(너비 우선 탐색)&lt;br /&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게&amp;nbsp;구현할&amp;nbsp;수&amp;nbsp;있으나,&amp;nbsp;X에서&amp;nbsp;X로가는&amp;nbsp;최단거리는&amp;nbsp;항상&amp;nbsp;0임에&amp;nbsp;유의하라.&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;pre id=&quot;code_1714972453837&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;queue&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;algorithm&amp;gt;
#pragma warning(disable:4996)
using namespace std;

int N, M, K, X;
int st, ed;
bool visit[1000001];

typedef struct data {
	int city, cnt;
}Data;

vector&amp;lt;int&amp;gt; answer;
int main() {
	scanf(&quot;%d %d %d %d&quot;, &amp;amp;N, &amp;amp;M, &amp;amp;K, &amp;amp;X);
	
	vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; load(N + 1);
	for (int m = 0; m &amp;lt; M; m++) {
		scanf(&quot;%d %d&quot;, &amp;amp;st, &amp;amp;ed);
		load[st].push_back(ed);
	}

	queue&amp;lt;Data&amp;gt;q;
	q.push({ X, 0 });
	visit[X] = true;
	while (!q.empty()) {
		Data out = q.front(); q.pop();
		if (out.cnt == K) {
			answer.push_back(out.city);
			continue;
		}
		for (int i = 0; i &amp;lt; load[out.city].size(); i++) {
			int next_city = load[out.city][i];
			int next_cnt = out.cnt + 1;
			if (!visit[next_city]) {
				visit[next_city] = true;
				q.push({ next_city, next_cnt });
			}
		}
	}
	if (answer.empty()) {
		printf(&quot;-1&quot;);
		return 0;
	}
	sort(answer.begin(), answer.end());
	for (int i = 0; i &amp;lt; answer.size(); i++) {
		printf(&quot;%d\n&quot;, answer[i]);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>문제풀이/BAEKJOON Online Judge</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/273</guid>
      <comments>https://malangdidoo.tistory.com/273#entry273comment</comments>
      <pubDate>Mon, 6 May 2024 13:13:17 +0900</pubDate>
    </item>
    <item>
      <title>[BAEKJOON] 11286, 1251, 14938, 1715, 2665</title>
      <link>https://malangdidoo.tistory.com/272</link>
      <description>&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;※&amp;nbsp;소스코드는&amp;nbsp;각&amp;nbsp;문제&amp;nbsp;설명&amp;nbsp;하단에&amp;nbsp;&amp;lt;더보기&amp;gt;를&amp;nbsp;통해&amp;nbsp;확인하실&amp;nbsp;수&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;b&gt;[sliver1, 11286 절대값 힙] - 우선순위 큐&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;C++에서는 무조건 &quot;struct compare&quot;로 선언할 것을 명심하자&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;pre id=&quot;code_1714833621047&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;queue&amp;gt;
#include &amp;lt;algorithm&amp;gt;
#pragma warning(disable:4996)
using namespace std;

int N, num;
struct Data {
	int value;
	int type;
};
struct compare {
	bool operator()(Data a, Data b) {
		if (a.value == b.value) {
			return a.type &amp;gt; b.type;
		}
		return a.value &amp;gt; b.value;
	}
};
priority_queue &amp;lt;Data, vector&amp;lt;Data&amp;gt;, compare&amp;gt; pq;

int main() {
	scanf(&quot;%d&quot;, &amp;amp;N);
	for (int n = 0; n &amp;lt; N; n++) {
		scanf(&quot;%d&quot;, &amp;amp;num);
		switch (num) {
		case 0:
			if (pq.empty()) printf(&quot;0\n&quot;);
			else {
				Data out = pq.top(); pq.pop();
				printf(&quot;%d\n&quot;, out.value * out.type);
			}
			break;
		default:
			pq.push({ abs(num), num &amp;lt; 0 ? -1 : 1 });
			break;
		}
	}	
}&lt;/code&gt;&lt;/pre&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;&lt;b&gt;[sliver5, 1251 단어 나누기] - 브루트포스&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;c++을 사용하는 경우, string 타입을 활용할 때 reverse&amp;nbsp;method는&amp;nbsp;algorithm&amp;nbsp;헤더에&amp;nbsp;있는&amp;nbsp;것에&amp;nbsp;유의하자&lt;br /&gt;또한 substr method는 인자로 받는 두 수를 (a, b)라 할 때, 시작위치&amp;nbsp;a에서&amp;nbsp;b&amp;nbsp;길이만큼의&amp;nbsp;문자열을&amp;nbsp;분할하겠다는&amp;nbsp;의미임에&amp;nbsp;유의하자&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;pre id=&quot;code_1714833687772&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;string&amp;gt;
#include &amp;lt;algorithm&amp;gt;
#pragma warning(disable:4996)
using namespace std;

string word, answer;
string reverse_word[3];

int len;
int slice_pivot[2];
bool visit[51];

void selectSlicePivot(int value, int cnt);
string reverseWord();

int main() {
	cin &amp;gt;&amp;gt; word;
	len = word.length();
	for (int i = 0; i &amp;lt; len; i++) answer += 'z';
	selectSlicePivot(0, 0);
	cout &amp;lt;&amp;lt; answer;
}

void selectSlicePivot(int value, int cnt) {
	if (cnt == 2) {
		string new_answer = reverseWord();
		if (answer.compare(new_answer) &amp;gt; 0) {
			answer = new_answer;
		};
		return;
	}
	for (int i = value; i &amp;lt; len - 1; i++) {
		if (!visit[i]) {
			visit[i] = true;
			slice_pivot[cnt] = i;
			selectSlicePivot(i, cnt + 1);
			visit[i] = false;
		}
	}
}

string reverseWord() {
	string st = word.substr(0, slice_pivot[0] + 1);
	string mid = word.substr(slice_pivot[0] + 1, slice_pivot[1] - slice_pivot[0]);
	string ed = word.substr(slice_pivot[1] + 1, len - slice_pivot[1]);
	reverse(st.begin(), st.end());
	reverse(mid.begin(), mid.end());
	reverse(ed.begin(), ed.end());
	return st + mid + ed;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;&lt;b&gt;[gold4, 14938 서강그라운드] - 플로이드 와샬&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각&amp;nbsp;지점끼리&amp;nbsp;연결된&amp;nbsp;길은&amp;nbsp;양방향이기&amp;nbsp;때문에,&amp;nbsp; &lt;br /&gt;플로이드&amp;nbsp;와샬&amp;nbsp;배열에&amp;nbsp;추가할&amp;nbsp;때&amp;nbsp;양방향으로&amp;nbsp;추가할&amp;nbsp;수&amp;nbsp;있도록&amp;nbsp;신경써야&amp;nbsp;한다.&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;pre id=&quot;code_1714833759501&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;queue&amp;gt;
#pragma warning(disable:4996)
using namespace std;

#define MAX 101

int N, M, R;
int item[101];
int floyd[101][101];

void init() {
	scanf(&quot;%d %d %d&quot;, &amp;amp;N, &amp;amp;M, &amp;amp;R);
	for (int n = 1; n &amp;lt;= N; n++) scanf(&quot;%d&quot;, &amp;amp;item[n]);

	for (int i = 1; i &amp;lt;= N; i++)
		for (int j = 1; j &amp;lt;= N; j++)
			floyd[i][j] = MAX;

	int st = 0, ed = 0, value = 0;
	for (int r = 0; r &amp;lt; R; r++) {
		scanf(&quot;%d %d %d&quot;, &amp;amp;st, &amp;amp;ed, &amp;amp;value);
		floyd[st][ed] = value;
		floyd[ed][st] = value;
	}
}

void floydwarshall() {
	for (int k = 1; k &amp;lt;= N; k++)
		for (int i = 1; i &amp;lt;= N; i++)
			for (int j = 1; j &amp;lt;= N; j++) {
				if (i != j &amp;amp;&amp;amp; floyd[i][j] &amp;gt; floyd[i][k] + floyd[k][j]) {
					floyd[i][j] = floyd[i][k] + floyd[k][j];
				}
			}
}

int answer = 0;
void solve() {
	for (int i = 1; i &amp;lt;= N; i++) {
		int cnt = 0;
		for (int j = 1; j &amp;lt;= N; j++) {
			if (i == j) {
				cnt += item[j];
				continue;
			}
			if (floyd[i][j] &amp;lt;= M) {
				//printf(&quot;%d-&amp;gt;%d is get %d items!\n&quot;, i, j, item[j]);
				cnt += item[j];
			}
		}
		//printf(&quot;%d point can get total %d items!\n&quot;, i, cnt);
		answer = answer &amp;gt; cnt ? answer : cnt;
	}
	printf(&quot;%d&quot;, answer);
}

int main() {
	init();
	floydwarshall();
	solve();
}&lt;/code&gt;&lt;/pre&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;&lt;b&gt; [gold4, 1715 카드 정렬하기] - 우선순위 큐&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1714833814037&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;queue&amp;gt;
#pragma warning(disable:4996)
using namespace std;

int N, card;

struct compare {
	bool operator()(int a, int b) {
		return a &amp;gt; b;
	}
};
priority_queue &amp;lt;int, vector&amp;lt;int&amp;gt;, compare&amp;gt; pq;

int main() {
	scanf(&quot;%d&quot;, &amp;amp;N);
	for (int n = 0; n &amp;lt; N; n++) {
		scanf(&quot;%d&quot;, &amp;amp;card);
		pq.push(card);
	}

	int answer = 0;
	while (true) {
		int fs = 0, se = 0;
		fs = pq.top(); pq.pop();
		if (!pq.empty()) {
			se = pq.top(); pq.pop();
		}
		else {
			break;
		}
		answer += fs + se;
		pq.push(fs + se);
	}

	printf(&quot;%d&quot;, answer);
}&lt;/code&gt;&lt;/pre&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;&lt;b&gt; &lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;[gold4, 2665 미로만들기] - BFS(너비 우선 탐색) + 우선순위 큐&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;검정색&amp;nbsp;방에&amp;nbsp;대해&amp;nbsp;0~최대&amp;nbsp;51개까지의&amp;nbsp;방을&amp;nbsp;직접&amp;nbsp;선택하는&amp;nbsp;조합으로&amp;nbsp;문제를&amp;nbsp;풀려하면, &lt;br /&gt;정말&amp;nbsp;쉽게&amp;nbsp;시간초과를&amp;nbsp;얻을&amp;nbsp;수&amp;nbsp;있다. &lt;br /&gt;&lt;br /&gt;그래서&amp;nbsp;본인은&amp;nbsp;다음의&amp;nbsp;우선순위를&amp;nbsp;가지는&amp;nbsp;큐를&amp;nbsp;활용하여&amp;nbsp;문제를&amp;nbsp;풀었다. &lt;br /&gt;1.&amp;nbsp;검은색&amp;nbsp;방을&amp;nbsp;지나왔던&amp;nbsp;카운트&amp;nbsp;기준&amp;nbsp;최소값&amp;nbsp;우선&amp;nbsp;탐색 &lt;br /&gt;2.&amp;nbsp;흰색&amp;nbsp;방&amp;nbsp;우선&amp;nbsp;탐색 &lt;br /&gt;이렇게&amp;nbsp;된다면,&amp;nbsp;문제에서&amp;nbsp;원하는&amp;nbsp;검은방을&amp;nbsp;최소로&amp;nbsp;뚫으면서&amp;nbsp;흰색방을&amp;nbsp;우선&amp;nbsp;탐색할&amp;nbsp;수&amp;nbsp;있다. &lt;br /&gt;&lt;br /&gt;위&amp;nbsp;방법&amp;nbsp;이외에도&amp;nbsp;검은색&amp;nbsp;방을&amp;nbsp;카운팅하여&amp;nbsp;3차원&amp;nbsp;배열을&amp;nbsp;통한&amp;nbsp;풀이법도&amp;nbsp;있을&amp;nbsp;것&amp;nbsp;같다.&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;pre id=&quot;code_1714833858353&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;queue&amp;gt;
#pragma warning(disable:4996)
using namespace std;

int N;
int map[51][51];

int dy[] = { -1,1,0,0 };
int dx[] = { 0,0,-1,1 };

typedef struct data {
	int y, x, type, cnt;
}Data;
struct compare {
	bool operator()(Data a, Data b) {
		if (a.cnt == b.cnt) {
			return a.type &amp;lt; b.type;
		}
		return a.cnt &amp;gt; b.cnt;
	}
};

bool range(int y, int x) {
	return y &amp;lt; 0 || x&amp;lt;0 || y&amp;gt;N - 1 || x&amp;gt;N - 1;
}
void bfs() {
	priority_queue &amp;lt;Data, vector&amp;lt;Data&amp;gt;, compare&amp;gt; q;
	bool visit[51][51] = { 0, };
	q.push({ 0, 0, map[0][0], 0});
	visit[0][0] = true;
	while (!q.empty()) {
		Data out = q.top(); q.pop();
		if (out.y == N - 1 &amp;amp;&amp;amp; out.x == N - 1) {
			printf(&quot;%d&quot;, out.cnt);
			return;
		}
		for (int d = 0; d &amp;lt; 4; d++) {
			int ny = out.y + dy[d];
			int nx = out.x + dx[d];
			if (!range(ny, nx) &amp;amp;&amp;amp; !visit[ny][nx]) {
				visit[ny][nx] = true;
				q.push({ ny, nx, map[ny][nx], map[ny][nx] == 0 ? out.cnt + 1 : out.cnt });
			}
		}
	}
}

int main() {
	scanf(&quot;%d&quot;, &amp;amp;N);
	char temp[51];
	for (int i = 0; i &amp;lt; N; i++) {
		scanf(&quot;%s&quot;, &amp;amp;temp);
		for (int j = 0; j &amp;lt; N; j++) {
			map[i][j] = temp[j] - '0';
		}
	}
	bfs();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>문제풀이/BAEKJOON Online Judge</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/272</guid>
      <comments>https://malangdidoo.tistory.com/272#entry272comment</comments>
      <pubDate>Sat, 4 May 2024 23:44:29 +0900</pubDate>
    </item>
    <item>
      <title>[BAEKJOON] 1253, 2529</title>
      <link>https://malangdidoo.tistory.com/271</link>
      <description>&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;※&amp;nbsp;소스코드는&amp;nbsp;각&amp;nbsp;문제&amp;nbsp;설명&amp;nbsp;하단에&amp;nbsp;&amp;lt;더보기&amp;gt;를&amp;nbsp;통해&amp;nbsp;확인하실&amp;nbsp;수&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;b&gt;[gold4, 1253 좋다] - 두 포인터&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최대한&amp;nbsp;이분탐색의&amp;nbsp;핵심&amp;nbsp;값인&amp;nbsp;mid를&amp;nbsp;활용하여&amp;nbsp;수행시간을&amp;nbsp;줄여보려&amp;nbsp;했으나, &lt;br /&gt;mid를&amp;nbsp;활용하는&amp;nbsp;경우,&amp;nbsp;값을&amp;nbsp;건너뛰는&amp;nbsp;과정에서 &lt;br /&gt;기준&amp;nbsp;값과&amp;nbsp;두&amp;nbsp;수의&amp;nbsp;합이&amp;nbsp;일치하는&amp;nbsp;경우를&amp;nbsp;전부&amp;nbsp;확인할&amp;nbsp;수&amp;nbsp;없기&amp;nbsp;때문에,&amp;nbsp;예외&amp;nbsp;케이스가&amp;nbsp;존재합니다. &lt;br /&gt;&lt;br /&gt;따라서,&amp;nbsp;가장&amp;nbsp;합리적인&amp;nbsp;방법이&amp;nbsp;두&amp;nbsp;포인터를&amp;nbsp;활용하는&amp;nbsp;것이며 &lt;br /&gt;자기&amp;nbsp;자신을&amp;nbsp;제외한&amp;nbsp;값을&amp;nbsp;활용해야&amp;nbsp;한다는&amp;nbsp;점에&amp;nbsp;주의하고 &lt;br /&gt;algorithm&amp;nbsp;헤더에&amp;nbsp;포함된&amp;nbsp;upper와&amp;nbsp;lower를&amp;nbsp;활용하여&amp;nbsp;동일한&amp;nbsp;케이스를&amp;nbsp;합산하는&amp;nbsp;방법을&amp;nbsp;활용하세요.&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;pre id=&quot;code_1714720042688&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;algorithm&amp;gt;
#pragma warning(disable:4996)

using namespace std;

int N, answer;
int ary[2001];

int main() {
	scanf(&quot;%d&quot;, &amp;amp;N);
	for (int n = 0; n &amp;lt; N; n++)
		scanf(&quot;%d&quot;, &amp;amp;ary[n]);

	sort(ary, ary + N);
	
	for (int n = 0; n &amp;lt; N; n++) {
		int pivot = ary[n];
		int left = 0;
		int right = N - 1;
		while (left &amp;lt; right) {
			int res = ary[left] + ary[right];
			if (left == n || pivot &amp;gt; res) {
				left++;
				continue;
			}
			if (right == n || pivot &amp;lt; res) {
				right--;
				continue;
			}
			if (pivot == res) {
				int upper = upper_bound(ary, ary + N, ary[n]) - ary;
				int lower = lower_bound(ary, ary + N, ary[n]) - ary;
				answer += upper - lower;
				n = upper - 1;
				break;
			}
		}
	}
	printf(&quot;%d&quot;, answer);
}&lt;/code&gt;&lt;/pre&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;&lt;b&gt;[sliver1, 2529 부등호] - 브루트포스&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제에서 나올 수 있는 최대 값은 9876543210, 최소는 00000000000이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숫자로 치환하면 int는 가뿐히 넘기 때문에, string을 활용하여 값을 비교할 땐 stoll을 사용해 주어야 하는 것에 유의하자.&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;현재 추가되는 값의 위치가 cnt라면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숫자배열의 cnt-1위치와 cnt 위치의 값을&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등호배열의 cnt 위치의 등호를 사용하여 비교하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;pre id=&quot;code_1714736768074&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;string&amp;gt;
#pragma warning(disable:4996)

using namespace std;

int K;
string max_answer = &quot;0000000000&quot;, min_answer = &quot;9876543210&quot;;
char sign[10];
int answer[10];
bool visit[10];

void selectNumber(int value, int cnt, string result);
bool compareValue(int a, int b, char symbol);

int main() {
	scanf(&quot;%d&quot;, &amp;amp;K);
	for (int k = 0; k &amp;lt; K; k++) {
		scanf(&quot; %c&quot;, &amp;amp;sign[k]);
	}

	for (int i = 0; i &amp;lt;= 9; i++) {
		visit[i] = true;
		selectNumber(i, 1, to_string(i));
		visit[i] = false;
	}

	cout &amp;lt;&amp;lt; max_answer &amp;lt;&amp;lt; '\n' &amp;lt;&amp;lt; min_answer;
}

void selectNumber(int value, int cnt, string result) {
	answer[cnt - 1] = value;

	if (cnt == K + 1) {
		if (stoll(max_answer) &amp;lt; stoll(result)) {
			max_answer = result;
		}
		if (stoll(min_answer) &amp;gt; stoll(result)) {
			min_answer = result;
		}
		return;
	}

	for (int i = 0; i &amp;lt;= 9; i++) {
		if (!visit[i]) {
			if (compareValue(value, i, sign[cnt - 1])) {
				visit[i] = true;
				selectNumber(i, cnt + 1, result + to_string(i));
				visit[i] = false;
			}
		}
	}
}

bool compareValue(int a, int b, char symbol) {
	switch (symbol) {
	case '&amp;lt;':
		return a &amp;lt; b;
	case '&amp;gt;':
		return a &amp;gt; b;
	default:
		return false;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>문제풀이/BAEKJOON Online Judge</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/271</guid>
      <comments>https://malangdidoo.tistory.com/271#entry271comment</comments>
      <pubDate>Fri, 3 May 2024 16:07:30 +0900</pubDate>
    </item>
    <item>
      <title>[BAEKJOON] 1477, 13702</title>
      <link>https://malangdidoo.tistory.com/270</link>
      <description>&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;※ 소스코드는 각 문제 설명 하단에 &amp;lt;더보기&amp;gt;를 통해 확인하실 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;[gold4, 1477 휴계소 세우기] - 이분탐색&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;처음엔&amp;nbsp;우선순위&amp;nbsp;큐로&amp;nbsp;접근했으나,&amp;nbsp;휴계소&amp;nbsp;사이에&amp;nbsp;정확히&amp;nbsp;1개가&amp;nbsp;아닌&amp;nbsp;여러개의&amp;nbsp;휴계소를&amp;nbsp;설치함으로써&amp;nbsp; &lt;br /&gt;최대&amp;nbsp;거리를&amp;nbsp;좁혀나갈&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;방법이&amp;nbsp;존재하기&amp;nbsp;때문에&amp;nbsp;정확한&amp;nbsp;답을&amp;nbsp;도출할&amp;nbsp;순&amp;nbsp;없다. &lt;br /&gt;&lt;br /&gt;그래서&amp;nbsp;이분탐색으로&amp;nbsp;풀어야&amp;nbsp;한다. &lt;br /&gt;가장&amp;nbsp;키&amp;nbsp;포인트는,&amp;nbsp;구간&amp;nbsp;사이사이에&amp;nbsp;휴계소를&amp;nbsp;설치하는&amp;nbsp;것인데, &lt;br /&gt;1)&amp;nbsp;얼만큼의&amp;nbsp;거리로&amp;nbsp;휴계소를&amp;nbsp;지을&amp;nbsp;것인지,&amp;nbsp;2)&amp;nbsp;그&amp;nbsp;거리&amp;nbsp;내에서&amp;nbsp;설치할&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;휴계소의&amp;nbsp;개수는&amp;nbsp;몇개인지 &lt;br /&gt;두&amp;nbsp;점을&amp;nbsp;파악하며&amp;nbsp;풀이하는&amp;nbsp;것이&amp;nbsp;핵심이다. &lt;br /&gt;&lt;br /&gt;신경써야&amp;nbsp;할&amp;nbsp;예외는,&amp;nbsp;특정&amp;nbsp;거리만큼&amp;nbsp;구간&amp;nbsp;내에&amp;nbsp;설치할&amp;nbsp;때,&amp;nbsp;등분을&amp;nbsp;활용하게&amp;nbsp;되는데, &lt;br /&gt;휴계소가&amp;nbsp;이미&amp;nbsp;지어져있는&amp;nbsp;마지막&amp;nbsp;구간을&amp;nbsp;포함하여&amp;nbsp;등분이&amp;nbsp;진행되는&amp;nbsp;경우를&amp;nbsp;제거해주어야&amp;nbsp;한다. &lt;br /&gt;간단하게&amp;nbsp;예를들면,&amp;nbsp;10과&amp;nbsp;100에&amp;nbsp;휴계소가&amp;nbsp;지어져있고,&amp;nbsp;그&amp;nbsp;사이에&amp;nbsp;10&amp;nbsp;거리만큼&amp;nbsp;휴계소를&amp;nbsp;지은다면,&amp;nbsp;90/10&amp;nbsp;=&amp;nbsp;9개가&amp;nbsp;지어져야&amp;nbsp;하는데, &lt;br /&gt;100&amp;nbsp;위치에는&amp;nbsp;이미&amp;nbsp;휴계소가&amp;nbsp;지어져&amp;nbsp;있으므로&amp;nbsp;배제해주어야&amp;nbsp;한다는&amp;nbsp;뜻이다. &lt;br /&gt;&lt;br /&gt;가장&amp;nbsp;어렵고&amp;nbsp;이해가&amp;nbsp;안된&amp;nbsp;부분은,&amp;nbsp;실제&amp;nbsp;휴계소를&amp;nbsp;지을&amp;nbsp;땐,&amp;nbsp;거리가&amp;nbsp;일정하지&amp;nbsp;않을&amp;nbsp;수&amp;nbsp;있으므로 &lt;br /&gt;일정&amp;nbsp;거리마다&amp;nbsp;휴계소를&amp;nbsp;정확히&amp;nbsp;M개만&amp;nbsp;지었을&amp;nbsp;때를&amp;nbsp;판별하는&amp;nbsp;것이&amp;nbsp;아닌, &lt;br /&gt;휴계소가&amp;nbsp;M개&amp;nbsp;이하가&amp;nbsp;지어지는&amp;nbsp;모든&amp;nbsp;경우에,&amp;nbsp;휴계소를&amp;nbsp;짓는&amp;nbsp;거리의&amp;nbsp;최대값의&amp;nbsp;최소값을&amp;nbsp;구해야&amp;nbsp;하는&amp;nbsp;것이다. &lt;br /&gt;휴계소가&amp;nbsp;M개&amp;nbsp;이하인&amp;nbsp;모든&amp;nbsp;경우에&amp;nbsp;판별하는&amp;nbsp;이유는,&amp;nbsp;나머지&amp;nbsp;휴계소를&amp;nbsp;구간&amp;nbsp;사이&amp;nbsp;어느곳에나&amp;nbsp;추가해도&amp;nbsp;현재&amp;nbsp;최대값은&amp;nbsp;유지되기&amp;nbsp;때문이다.&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;pre id=&quot;code_1714445374938&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;queue&amp;gt;
#include &amp;lt;algorithm&amp;gt;
#pragma warning(disable:4996)

using namespace std;

#define MAX 52
int N, M, L, answer = 1000;
int place[MAX];

int main() {
	scanf(&quot;%d %d %d&quot;, &amp;amp;N, &amp;amp;M, &amp;amp;L);

	place[0] = 0;
	place[N + 1] = L;

	for (int n = 1; n &amp;lt;= N; n++)
		scanf(&quot;%d&quot;, &amp;amp;place[n]);

	sort(place, place + N + 1);

	int left = 1;
	int right = L - 1;
	while (left &amp;lt;= right) {
		int mid = (left + right) / 2;
		int cnt = 0;
		for (int i = 1; i &amp;lt;= N + 1; i++) {
			int dist = place[i] - place[i - 1];
			int build = dist / mid;
			if (build &amp;gt; 0) {
				cnt += dist % mid == 0 ? build - 1 : build;
			}
		}
		// 더 많이지어졌으면, 거리가 짧은 것이므로
		if (cnt &amp;gt; M) left = mid + 1;
		// 더 적게 지어졌으면, 거리가 긴 것이므로
		else {
			answer = answer &amp;lt; mid ? answer : mid;
			right = mid - 1;
		}
	}

	printf(&quot;%d&quot;, answer);
}&lt;/code&gt;&lt;/pre&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;&lt;b&gt;[sliver2, 13702 이상한 술집] - 이분탐색&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;K명수에&amp;nbsp;맞춰서&amp;nbsp;따르려는&amp;nbsp;막걸리의&amp;nbsp;양이&amp;nbsp;동등해야&amp;nbsp;한다는&amp;nbsp;점을&amp;nbsp;기억하고, &lt;br /&gt;막걸리의&amp;nbsp;최대&amp;nbsp;값이&amp;nbsp;2^31-1임에&amp;nbsp;유념하자. &lt;br /&gt;&lt;br /&gt;이분탐색을&amp;nbsp;통한&amp;nbsp;막걸리의&amp;nbsp;값을&amp;nbsp;mid라&amp;nbsp;할&amp;nbsp;때, &lt;br /&gt;N개의&amp;nbsp;막걸리&amp;nbsp;주전자에서&amp;nbsp;mid만큼&amp;nbsp;따를&amp;nbsp;때,&amp;nbsp;cnt명에게&amp;nbsp;동등하게&amp;nbsp;제공할&amp;nbsp;수&amp;nbsp;있는데, &lt;br /&gt;&lt;br /&gt;cnt명&amp;nbsp;수가&amp;nbsp;K명&amp;nbsp;수보다&amp;nbsp;작다면,&amp;nbsp;막걸리&amp;nbsp;양이&amp;nbsp;너무&amp;nbsp;많은거니까&amp;nbsp;최대치를&amp;nbsp;줄이고 &lt;br /&gt;cnt명&amp;nbsp;수가&amp;nbsp;K명&amp;nbsp;수보다&amp;nbsp;크다면,&amp;nbsp;막걸리&amp;nbsp;양이&amp;nbsp;너무&amp;nbsp;적은거니까&amp;nbsp;최소치를&amp;nbsp;키워야하며 &lt;br /&gt;cnt명&amp;nbsp;수가&amp;nbsp;K명&amp;nbsp;수와&amp;nbsp;같을&amp;nbsp;땐,&amp;nbsp;동등하게&amp;nbsp;제공되지만&amp;nbsp;최대치인지는&amp;nbsp;알&amp;nbsp;수&amp;nbsp;없으므로 &lt;br /&gt;이분탐색&amp;nbsp;조건을&amp;nbsp;만족할&amp;nbsp;때까지&amp;nbsp;최소값을&amp;nbsp;증가하는&amp;nbsp;방향으로&amp;nbsp;설계하는&amp;nbsp;것이&amp;nbsp;첫&amp;nbsp;번째&amp;nbsp;핵심&amp;nbsp;포인트이다. &lt;br /&gt;&lt;br /&gt;막걸리의&amp;nbsp;최대&amp;nbsp;값이&amp;nbsp;int의&amp;nbsp;최대범위이므로,&amp;nbsp;mid값을&amp;nbsp;설정하는&amp;nbsp;(left+right)/2값이&amp;nbsp;int&amp;nbsp;자료형&amp;nbsp;범위를&amp;nbsp;초과할&amp;nbsp;수&amp;nbsp;있다. &lt;br /&gt;따라서,&amp;nbsp;관련&amp;nbsp;left,&amp;nbsp;right,&amp;nbsp;mid,&amp;nbsp;answer에&amp;nbsp;대해&amp;nbsp;long&amp;nbsp;long으로&amp;nbsp;자료형을&amp;nbsp;설정하는&amp;nbsp;것이&amp;nbsp;두&amp;nbsp;번째&amp;nbsp;핵심&amp;nbsp;포인트이다.&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;pre id=&quot;code_1714473450128&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#pragma warning(disable:4996)

using namespace std;

#define MAX 1000001
#define ll long long

int N, K;
ll answer = 0;
ll kettle[MAX];

int main() {
	scanf(&quot;%d %d&quot;, &amp;amp;N, &amp;amp;K);
	for (int n = 0; n &amp;lt; N; n++) {
		scanf(&quot;%d&quot;, &amp;amp;kettle[n]);
	}

	ll left = 1;
	ll right = 2147483647 - 1;
	while (left &amp;lt;= right) {
		ll mid = (left + right) / 2;
		int cnt = 0;
		for (int n = 0; n &amp;lt; N; n++) {
			cnt += kettle[n] / mid;
		}
		if (cnt &amp;lt; K) {
			right = mid - 1;
		}
		else {
			answer = mid;
			left = mid + 1;
		}
	}
	printf(&quot;%lld&quot;, answer);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>문제풀이/BAEKJOON Online Judge</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/270</guid>
      <comments>https://malangdidoo.tistory.com/270#entry270comment</comments>
      <pubDate>Wed, 1 May 2024 12:50:22 +0900</pubDate>
    </item>
    <item>
      <title>[BAEKJOON] 1018</title>
      <link>https://malangdidoo.tistory.com/269</link>
      <description>&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #555555; text-align: center;&quot;&gt;※ 소스코드는 각 문제 설명 하단에 &amp;lt;더보기&amp;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;b&gt;[sliver4, 1018 체스판 다시칠하기] - 브루트포스&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 체스판을 칠하는 것이다. 8x8 정사각형을 자르는 기준인 좌측 상단 꼭짓점을 기준으로 했을 때, &lt;br /&gt;기준의 색을 변경하지 않고 체스판을 다시 칠하는 것과 &lt;br /&gt;기준의 색을 변경하고 체스판을 다시 칠하는 것을 고려하여 모든 경우의 수를 판단해주어야 한다. &lt;br /&gt;&amp;nbsp;&amp;nbsp;&lt;br /&gt;기준과 다름을 판단하는 원리는, 체스판의 규칙성에 존재한다. &lt;br /&gt;체스판은 모든 색상이 대각선으로 일치하며, 상하좌우로 나와는 다른 색을 지닌 칸이 존재한다. &lt;br /&gt;이 규칙들을 활용한다. &lt;br /&gt;&amp;nbsp;&amp;nbsp;&lt;br /&gt;보드의 (y,x)를 기준으로 (y-1,x), (y,x-1), (y+1,x), (y,x+1)은 다른 색이 존재하므로 &lt;br /&gt;(y+x) mod 2의 결과값은 (y-1,x), (y,x-1), (y+1,x), (y,x+1)에 mod 2한 값과 다르다. &lt;br /&gt;따라서, 기준과 mod 2의 결과값이 같은 칸은 색상이 일치하고, 다른 칸은 색상이 불일치 해야 한다. &lt;br /&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;pre id=&quot;code_1714442113968&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#pragma warning(disable:4996)

using namespace std;

#define MAX 51

int H, W, answer = 987654321;
char map[MAX][MAX];

void checkBoard(int sy, int sx);
void rePaint(int sy, int sx, char color);

int main() {
	scanf(&quot;%d %d&quot;, &amp;amp;H, &amp;amp;W);
	for (int h = 0; h &amp;lt; H; h++)
		scanf(&quot;%s&quot;, &amp;amp;map[h]);

	for (int i = 0; i &amp;lt;= H - 8; i++) {
		for (int j = 0; j &amp;lt;= W - 8; j++) {
			checkBoard(i, j);
		}
	}
	printf(&quot;%d&quot;, answer);
}

void checkBoard(int y, int x) {	
	rePaint(y, x, map[y][x]);
	rePaint(y, x, map[y][x] == 'W' ? 'B' : 'W');
}

void rePaint(int sy, int sx, char color) {
	int type = (sy + sx) % 2;
	int cnt = 0;
	for (int i = sy; i &amp;lt; sy + 8; i++) {
		for (int j = sx; j &amp;lt; sx + 8; j++) {
			if ((i + j) % 2 == type &amp;amp;&amp;amp; color == map[i][j]) continue;
			if ((i + j) % 2 != type &amp;amp;&amp;amp; color != map[i][j]) continue;
			cnt += 1;
		}
	}
	answer = answer &amp;lt; cnt ? answer : cnt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>문제풀이/BAEKJOON Online Judge</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/269</guid>
      <comments>https://malangdidoo.tistory.com/269#entry269comment</comments>
      <pubDate>Tue, 30 Apr 2024 11:55:23 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] (Project) MapleSpy: 45. App Store 출시 준비</title>
      <link>https://malangdidoo.tistory.com/268</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;  앱 심사 지침&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;App Store의 기본 원칙은 간단합니다. 우리는 사용자가 앱을 다운로드할 수 있는 안전한 경험을 제공하고 모든 개발자가 성공할 수 있는 좋은 기회를 제공하고자 합니다. 우리는 전문가가 모든 앱을 검토하고 편집팀이 사용자가 매일 새로운 앱을 발견하도록 돕는 고도로 선별된 App Store를 제공함으로써 이를 수행합니다. 또한 각 앱에서 사용자 안전, 보안, 개인 정보 보호에 영향을 미칠 수 있는 악성 코드 및 기타 소프트웨어를 검사합니다. 이러한 노력으로 인해 Apple의 플랫폼은 전 세계 소비자에게 가장 안전한 플랫폼이 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앱 승인이 최대한 원활하게 진행되도록 하려면 검토 프로세스를 지연시키거나 거부를 유발할 수 있는 아래 나열된 일반적인 실수를 검토하세요. 이것이 지침을 대체하거나 승인을 보장하지는 않지만, 목록의 모든 항목을 확인할 수 있는지 확인하는 것이 좋은 시작입니다. 앱이 더 이상 의도한 대로 작동하지 않거나 더 이상 적극적으로 지원하지 않는 경우 해당 앱은 App Store에서 제거됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  앱 충돌 및 버그 테스트&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;rArr; TestFlight를 활용하여 충분히 검토한 후 앱 번들을 등록하세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  모든 앱 정보와 메타데이터가 완전하고 정확한지 확인&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;rArr; 특히 스크린샷을 첨부할 때, 앱 심사 과정과 동일하지 않다면 Reject당할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;rArr; 앱이 iPad(12.9 inch)를 지원하지 않아도, 무조건 스크린샷을 첨부할 수 있도록 App Store Connect가 변경되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;rArr; iOS 17버전 이상에서는 5.5 인치를 지원하는 iPhone 8+ 기기가 호환되지 않습니다. 이때, 4.0 인치 기반으로 동작하는 iPhone SE Simulator에서 동작을 캡쳐한 후, imagemagick을 활용하여 사이즈를 늘려줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;imagemagick은 brew를 활용하여 설치할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;brew install imagemagick
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이후 imagemagick을 활용하여 스크린샷의 크기를 조절하는 가장 쉬운 방법은 다음과 같습니다. 5.5인치는 1242x2208입니다. 다른 형태로도 변경할 수 있으므로 이것을 기반으로 자유롭게 활용하세요.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;magick 1.png -resize 1242x2209 -crop 1242x2208+0+0 1-5.5.png
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  App Review에서 연락해야 할 경우를 대비하여 연락처 정보를 업데이트&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;rArr; 연락처 정보 중 전화번호를 제공할 때 국제번호를 입력해야 합니다. (+8210xxxxyyyy)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  앱 심사에 앱에 대한 전체 액세스 권한을 제공&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;rArr; 계정 기반 기능이 포함된 경우, 활성 데모 계정 또는 앱을 검토하는 데 필요할 수 있는 리소스를 제공해야 함&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  백엔드 서비스를 활성화 하여 검토 중에 실시간으로 액세스 할 수 있어야 함&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  적절한 경우 지원 문서를 포함하여, 앱 검토 노트에 명확하지 않은 기능 및 구매에 대한 자세한 설명을 포함해야 함&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  앱이 다음과 같은 문서의 지침을 따르는지 확인&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;a href=&quot;https://developer.apple.com/app-store/review/guidelines/#safety&quot;&gt;https://developer.apple.com/app-store/review/guidelines/#safety&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;a href=&quot;https://developer.apple.com/app-store/review/guidelines/#performance&quot;&gt;https://developer.apple.com/app-store/review/guidelines/#performance&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;a href=&quot;https://developer.apple.com/app-store/review/guidelines/#business&quot;&gt;https://developer.apple.com/app-store/review/guidelines/#business&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;a href=&quot;https://developer.apple.com/app-store/review/guidelines/#design&quot;&gt;https://developer.apple.com/app-store/review/guidelines/#design&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;a href=&quot;https://developer.apple.com/app-store/review/guidelines/#legal&quot;&gt;https://developer.apple.com/app-store/review/guidelines/#legal&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;또한, 앱 제출 후 검토 과정이 진행 중이라면 다음을 고려하여 행동해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;a href=&quot;https://developer.apple.com/app-store/review/guidelines/#after-you-submit&quot;&gt;https://developer.apple.com/app-store/review/guidelines/#after-you-submit&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이 외에도 신경 써야 하는 부분이 있다면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1) Apple 개발자 계정이 디지털 서비스법 규정을 준수하는지 확인&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;rArr; App Store Connect에서 &quot;앱&quot; 카테고리를 선택하는 경우, 디지털 서비스법 규정에 준수하지 않거나 설정되지 않은 경우 안내가 진행되므로, 제공되는 링크를 통해 규정에 준수하도록 설정하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;제공되는 앱이 &quot;무료&quot;이며, 유료앱/인앱결제/수익발생/은행계좌연결/세금약식계약서존재와 같은 사례에 해당되지 않는 경우 비 대상자로 설정하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2) 콘텐츠 권한 설정 (앱 정보)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3) 연령 등급 설정 (앱 정보)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;4) 앱이 수집하는 개인정보 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;rArr; 개인정보 처리방침 URL과, 앱 내에서 수집하는 데이터 기록에 관하여 설정을 진행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;만약 앱 내에서 수집하는 데이터 기록이 없다면, 해당 사항은 건너뛰어도 좋을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;5) 가격 및 사용 가능 여부&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;rArr; 가격을 설정하고, 앱이 제공되는 지역을 제한합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;또한, Apple Silicon Mac 및 Apple Vision Pro에서 iPhone/iPad를 사용 가능한지 호환성을 따지는데 관련하여 개발이 진행되지 않았다면 항목을 해제해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이를 완료하면, 저장 및 심사 제출 버튼을 통해 심사를 진행할 수 있습니다. 단, 심사 제출 시 작성되지 않은 부분이 있는 경우 오류 메세지를 통해 알려주므로, 관련 내용을 해결하는 과정을 진행하시면 될 것 같습니다.&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/268</guid>
      <comments>https://malangdidoo.tistory.com/268#entry268comment</comments>
      <pubDate>Thu, 11 Apr 2024 15:40:46 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] (Project) MapleSpy: 45. Flutter build ipa</title>
      <link>https://malangdidoo.tistory.com/267</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;Flutter buidl ipa가 성공적으로 완료되기 위한 처음 과정부터 자세히 기록한 칼럼입니다. ipa build 자체만 보고 싶으시다면, 맨 아래로 이동하세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://docs.flutter.dev/deployment/ios&quot;&gt;https://docs.flutter.dev/deployment/ios&lt;/a&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  App Store Connect에 앱 등록&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;App Store Connect에서 앱의 수명 주기를 관리할 수 있습니다. 앱의 이름과 설명을 정의하고, 스크린 샷을 추가하고, 가격을 설정하고, AppStore 및 TestFlight에 대한 릴리즈를 관리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Bundle ID 등록&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;모든 iOS 애플리케이션은 Apple에 등록된 고유 식별자인 Bundle ID와 연결되어 있기 때문에 필수로 등록해야합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;개발자 계정의 App ID 페이지에 접근합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;rArr; url :&amp;nbsp;&lt;a href=&quot;https://developer.apple.com/account/resources/identifiers/bundleId/add/bundle&quot;&gt;https://developer.apple.com/account/resources/identifiers/bundleId/add/bundle&lt;/a&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&quot;+&quot;를 클릭하여, iOS 앱 개발을 선택 후 새로운 Bundle ID를 생성합니다.&lt;/li&gt;
&lt;li&gt;앱 이름을 입력하고&amp;nbsp;명시적(Explicit) 앱 ID를 선택한 후 ID를 입력하세요.&lt;/li&gt;
&lt;li&gt;앱에서 사용하는 서비스를 선택한 다음&amp;nbsp;계속을 클릭하세요 .&lt;/li&gt;
&lt;li&gt;다음 페이지에서 세부 사항을 확인하고&amp;nbsp;등록을 클릭하여 번들 ID를 등록하세요.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  App Store Connect에서 애플리케이션 기록 생성&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;App Store Connect에 접근합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;rArr; url : &lt;a href=&quot;https://appstoreconnect.apple.com/apps&quot;&gt;https://appstoreconnect.apple.com/apps&lt;/a&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&quot;+&quot;를 클릭하여 새 앱을 선택 후, 세부 정보를 입력합니다.&lt;/li&gt;
&lt;li&gt;iOS가 선택됐는지 확인하세요. 단, Flutter는 tvOS를 지원하지 않으므로 해당 칸이 선택되지 않은 채로 두어야 합니다.&lt;/li&gt;
&lt;li&gt;앱의 세부정보로 이동 후, 앱 정보를 선택합니다.&lt;/li&gt;
&lt;li&gt;일반 정보 섹션에서 이전 단계에서 등록한 Bundle ID를 선택합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  Xcode 프로젝트 설정 검토&lt;/h4&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://docs.flutter.dev/deployment/ios#review-xcode-project-settings&quot;&gt;https://docs.flutter.dev/deployment/ios#review-xcode-project-settings&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Xcode 작업 영역에서 가장 중요한 설정을 검토하는 방법을 다룹니다. 그 전에 앱 배포에 대한 섹션을 먼저 진행해야 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  앱 배포 준비&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://help.apple.com/xcode/mac/current/#/dev91fe7130a&quot;&gt;https://help.apple.com/xcode/mac/current/#/dev91fe7130a&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;언제든지 프로젝트 설정을 편집할 수 있지만, 개발 중에 일부 설정이 필요합니다. 다른 것들은 테스트를 위해 앱을 배포할 때 권장되며, 앱을 업로드 한 후 나중에 앱 심사에 제출할 때 필요합니다. 이 섹션에서는 App Store Connect에 앱을 업로드하기 전에 앱에 대한 모든 필수 정보를 제공하는 단계입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Bundle ID 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Bundle ID는 시스템 전체에서 앱을 고유하게 식별합니다. 첫 번째 빌드를 업로드한 후에는 App Store Connect에서 Bundle ID를 변경할 수 없으므로 신중하게 설정하세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  팀에게 대상 할당&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://help.apple.com/xcode/mac/current/#/dev23aab79b4&quot;&gt;https://help.apple.com/xcode/mac/current/#/dev23aab79b4&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;TestFlight를 사용하거나, App Store를 통해 앱을 배포하려면 Apple Developer Program에 속한 팀에 대상을 할당해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  버전 번호 및 빌드 문자열 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;App Store Connect에서는 이를 사용하여 App Version과 일치시키므로, 앱을 배포하기 전에 버전 번호와 빌드 문자열을 설정해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  앱 카테고리 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;카테고리는 사용자가 App Store에서 앱을 찾는 데 도움이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  배포 정보 설정 편집&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;App Store Connect와 일치해야 하는 경우가 존재하므로, 앱을 배포하기 전에 배포 정보 설정을 편집하세요&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  앱 아이콘 추가&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;기기의 다양한 위치에서 앱을 나타내고 App Store Connect 유효성 검사 테스트를 통과하려면 앱 아이콘을 추가해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  시작 화면 제공(iOS)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;프로젝트 편집기의 일반 창에 존재하는 &quot;앱 아이콘 및 실행 이미지&quot;에서 시작 화면 및 시작 이미지를 지정해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  보호된 리소스에 액세스하기 위한 사용법 제공&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Xcode 프로젝트 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;관련 설정은 Xcode에서 &quot;Runner&quot; &amp;rarr; &quot;Info&quot; 경로를 통해 확인하고 설정할 수 있습니다. Flutter의 경우 대부분 환경 변수 값으로 설정되어 있기 때문에, 특별하게 수정하지 않아도 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Xcode에서 대상 설정으로 이동합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Flutter Project Directory의 최상단 터미널 창에서 다음 명령어를 실행하여 프로젝트의 기본 Xcode 작업공간을 엽니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;open ios/Runner.xcworkspace
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앱 설정을 보려면 Xcode 탐색기에서 Runner 대상을 선택하세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Display Name&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;rArr; 앱의 표시 이름&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Bundle Identifier&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;rArr; App Store Connect에 등록한 App ID&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  서명 및 기능 탭에서, Automatically manage signing&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;rArr; Xcode가 앱 서명 및 프로비저닝을 자동으로 관리해야 하는지 여부입니다. 이는 true가 기본으로 설정되어 있으며, 대부분의 앱에 충분하게 동작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Team&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;rArr; 등록된 Apple 개발자 계정과 연결된 팀을 선택하세요. 필요한 경우 계정 추가&amp;hellip;를 통해 설정을 업데이트할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이 과정에서 다음의 에러를 만날 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;⚠️ Communication with Apple failed, Apple과 통신에 실패했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;⚠️ No profiles for &amp;lsquo;~&amp;rsquo; were found, ~에 대한 프로파일을 찾을 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;두 문제 모두 profiles를 찾을 수 없음에 시작된 것 같기 때문에, &quot;&lt;a href=&quot;http://developer.apple.com/account/profiles/list&quot;&gt;developer.apple.com/account/profiles/list&lt;/a&gt;&quot; 경&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;로를 통해 새로운 Profiles를 등록하여 해결하려 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이 문제를 해결하는 과정을 진행하는 순서는 다음과 같이 진행할 수 있습니다. 만약 팀원이 없는 경우, ★이 표시된 부분만 진행하시면 됩니다. 또한, 하위 부분의 기술은 Flutter로 개발된 아주 간단한 프로젝트를 배포하는 과정을 담고 있으므로 생략된 부분이 많을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1) 개인 개발자 계정에 팀원 초대&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;rArr; &quot;&lt;a href=&quot;https://appstoreconnect.apple.com/&quot;&gt;https://appstoreconnect.apple.com/&lt;/a&gt;&quot; 경로에서 &quot;사용자 및 액세스&quot;를 통해 접근하고, &quot;+&amp;rdquo;를 클릭하여 새로운 사용자를 추가할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;u&gt;&lt;b&gt;★ 2) CertificateSigningRequest 생성&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;rArr; Profiles 등록에 필요한 인증서를 macOS 내부 &quot;키체인 접근&quot; 앱을 통해 생성할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&quot;키체인 접근&quot; 메뉴 &amp;rarr; 인증서 지원 &amp;rarr; 인증 기관에서 인증서 요청 경로를 통해 진행할 수 있습니다. 이때, &quot;디스크에 저장&quot;을 클릭하여 &quot;이메일&quot;과 &quot;이름&quot;을 입력하세요. 이름의 경우, 개인이 인식할 수 있는 용도로 저장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;u&gt;&lt;b&gt;★ 3) Apple Developer 에서 Certificates를 생성합니다.&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;개발용(iOS Development)와 배포용(iOS Distribution) 2개의 인증서를 생성하고, 개발 인증서의 경우 팀원에게 공유하여 사용하는 것이 일반적이라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;만약 Flutter로 배포를 위한 진행을 하기 위해선 iOS Distribution에 대한 인증서를 생성하는 것이 합당한 행위일 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;u&gt;&lt;b&gt;★ 4) 인증서를 &quot;키체인&quot;에 등록&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;키체인 접근 앱 &amp;rarr; 내 인증서 탭을 선택하여 다운로드 받은 인증서를 Drag and Drop 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;u&gt;&lt;b&gt;★ 5) 인증서와 Xcode 연동&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;[ command + , ] 조합을 통해 환경 설정에 접근합니다. 이후, Account Tap을 선택하고 Apple ID를 선택하여 &quot;Manage Certificates&quot;를 클릭하여 Certificates를 확인하고, &amp;ldquo;Download Manual Profiles&quot;를 클릭하면 완료됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;u&gt;&lt;b&gt;★ 6) Apple Developer에 App 등록&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&quot;Identifiers&amp;rdquo; 탭을 선택 후, Bundle ID를 활용하여 새롭게 추가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;7) 테스트 기기 등록&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&quot;Devices&quot; 탭을 선택하여 기기를 등록할 수 있습니다. 단 간단한 Flutter 프로젝트를 실행하며 macOS 내부 Simulator로 테스트하는 경우, 바로 배포를 진행하기 때문에 진행하지 않아도 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;u&gt;&lt;b&gt;★ 8) Provisioning Profiles 생성&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;App Store Connect에 배포를 하기 위해서 사용하기 때문에, Provisioning Profiles를 생성할 때 &quot;App Store Connect&quot;를 선택하고 Provisioning Profiles Name을 자유롭게 입력하세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;만약 Xcode에서 개발을 진행한다면, 위에서 진행할 때 개발용/배포용을 나눠 Profiles를 생성하고 Debug/Release에 개별적으로 등록하는 것이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;위 모든 과정에서 macOS 내부 인증서를 사용할 때, 대부분의 경우 &quot;Xcode later 11&quot;이라 표기된 인증서를 사용하라고 이야기 하고 있습니다. 내부 규약에 어떤 차이가 존재하는지 설명드릴 수 없는 점 죄송합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;u&gt;&lt;b&gt;★ 9) Xcode에 Profile 등록&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;생성한 Profile을 Xcode에 수동으로 등록합니다. Xcode의 AppIcon이 표기된 Runner에서 다양한 정보를 설정할 수 있는데, Signing &amp;amp; Capabilities 탭에서 Automatically manage signing을 해제 후 생성했던 Provisioning Profile을 선택합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;u&gt;&lt;b&gt;★ 10) VsCode에서 IPA Build&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;하위 작성될 App Bundle 생성 시 사용되는 메소드 &quot;flutter build ipa&quot;를 입력하고 대기합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이후 오류 없이 모든 과정이 완료되면 &quot;exportArchive&quot; 오류를 반환하는데, 함께 제공되는 &quot;open {path}&quot; 구문을 실행하여 Xcode에서 Distribution을 완료하면 App Store Connect에 버전이 정상적으로 올라가게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  빌드 설정 탭의 배포 섹션에서 iOS Development Target&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;rArr; 앱이 지원하는 최소 iOS 버전을 의미합니다. Flutter는 iOS 12이상을 지원합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  앱 배포 버전 업데이트&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Xcode 프로젝트에서 Deployment Target을 변경한 경우, &quot;ios/Fklutter/AppframeworkInfo.plist에 접근하여 &quot;MinimumOSVersion&quot; 값이 Flutter 앱과 일치하도록 설정하세요.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  앱 아이콘 추가&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;새로운 Flutter 앱이 생성되면 아이콘 세트가 생성됩니다. 아이콘 세트는 &quot;ios&quot; &amp;rarr; &quot;Runner&quot; &amp;rarr; &quot;Assets.xcassets&quot;를 변경하여 설정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이후, 새롭게 Flutter 앱을 시작하면서 변경된 내용을 확인할 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  시작 이미지 추가&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앱 아이콘과 마찬가지로, 시작 이미지를 변경할 수 있습니다. 변경된 사항은 Hot Restart를 통해 확인할 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  Build archive 생성 및 App Store Connect에 업로드&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;App Store 또는 TestFlight에서 사용자에게 앱을 출시할 준비가 되었을 때, 릴리즈 빌드를 준비해야 합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  앱 빌드 및 버전 번호 업데이트&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Flutter는 pubspec.yaml 파일에서 버전 번호를 업데이트 할 수 있습니다. 다음과 같이 버전, 빌드번호를 재 정의하여 ipa파일을 빌드할 수도 있습니다. 보다 다양한 방법이 있으나, pubspec.yaml을 통해 공통으로 관리하는 것을 추천합니다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;flutter build ipa --build-name--build-number
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  앱 번들 만들기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;프로젝트 드렉토리에서 다음 명령어를 통해 앱 번들을 만들 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;flutter build ipa
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그러면 다음 경로에 빌드 아카이브(파일)이 생성됩니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;..xcarchivebuild/ios/archive/.ipabuild/ios/ipa
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이때, Dart 코드를 난독화하여 리버스 엔지니어링을 더 어렵게 만드는 &quot;--obfuscate&quot; 와 &quot;--split-debug-info&quot;를 추가하는 것이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;즉 완벽한 방법으로는 다음과 같이 진행할 수 있습니다. 두 방법은 항상 같이 진행되어야 합니다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;flutter build ipa --obfuscate --split-debug-info 
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;App Store에 배포하지 않는 경우 선택적으로 내보내기 방법을 다음과 같이 진행할 수도 있으나, 대게의 경우 App Store에 배포하기 위해 앱 번들을 만들기 때문에 해당되지 않을 겁니다.&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;--export-method ad-hoc
--export-method development
--export-method enterprise
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;만약 후위 export-method를 사용할 수 없는 Flutter 버전에서는, &quot;build/ios/archive/MyApp.xcarchive&quot;아래에 지침을 열어서 Xcode에서 앱을 검증하고 배포하길 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Xcode에서 Signing &amp;amp; Certificate에서 수동으로 Profile을 설정해 놓은 경우, VsCode에서 빌드 완료 시 다음과 같은 에러가 뜰 수 있다.&lt;/p&gt;
&lt;pre class=&quot;subunit&quot;&gt;&lt;code&gt;Encountered error while creating the IPA:
error: exportArchive: &quot;Runner.app&quot; requires a provisioning profile.

Try distributing the app in Xcode: {path}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Runner.app 자체가 Provisioning profile을 필요로하므로, Xcode위에서 한번 더 앱을 배포 해주라는 이야기다.&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/267</guid>
      <comments>https://malangdidoo.tistory.com/267#entry267comment</comments>
      <pubDate>Thu, 11 Apr 2024 15:39:31 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] (Project) MapleSpy: 44. App Store 개발자 계정 만들기(개인)</title>
      <link>https://malangdidoo.tistory.com/266</link>
      <description>&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://developer.apple.com/kr/support/app-account/&quot;&gt;https://developer.apple.com/kr/support/app-account/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;개인 또는 개인 사업자/1인 기업의 경우 다음 단계를 따라 iPhone, iPad, Mac의 Apple Developer 앱에서 Apple Developer Program에 등록할 수 있습니다. 시작하기 전에 다음 사항들이 갖춰져 있는지 확인하시기 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Touch ID, Face ID 또는 암호가 활성화된 iPhone이나 iPad 또는 T2 보안 칩/Apple Silicon이 탑재된 Mac&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;등록 절차가 마무리될 때 까지 동일한 기기를 사용해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  &quot;이중 인증&quot;이 활성화 된 Apple ID&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://developer.apple.com/kr/support/authentication/&quot;&gt;https://developer.apple.com/kr/support/authentication/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Apple ID 정보는 유효하며 최신 상태여야 합니다. 해당 정보에는 이름, 성, 주소, 전화번호, 신뢰하는 전화번호 및 신뢰하는 기기 등이 포함되며 이에 국한되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  이중 인증 활성화 확인&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;지원되는 기기에서&amp;nbsp;Apple Developer&amp;nbsp;계정에 사용하는&amp;nbsp;Apple ID로&amp;nbsp;iCloud에 로그인하여 보안 설정을 확인할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;iOS 10.3&amp;nbsp;이후 버전 또는&amp;nbsp;iPadOS 13.1&amp;nbsp;이후 버전의 경우&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;설정 &amp;gt;&amp;nbsp;[사용자 이름] &amp;gt;&amp;nbsp;암호 및 보안으로 이동합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;iOS 10.2&amp;nbsp;이전 버전의 경우&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;설정 &amp;gt;&amp;nbsp;iCloud로 이동한 후&amp;nbsp;Apple ID &amp;gt;&amp;nbsp;암호 및 보안을 탭합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;OS X El Capitan&amp;nbsp;이후 버전의 경우&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Apple() 메뉴 &amp;gt;&amp;nbsp;시스템 환경설정 &amp;gt;&amp;nbsp;Apple ID &amp;gt;&amp;nbsp;암호 및 보안으로 이동합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이중 인증이 활성화된 경우 '이중 인증: 켜짐'이 표시됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이중 인중이 활성화되지 않은 경우 '이중 인증: 꺼짐'과 함께 이중 인증을 켤 수 있는 링크가 표시됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;mac에서는 OS X El Capitan 이후 버전의 과정을 진행하여 확인하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  이중 인증 활성화&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://support.apple.com/ko-kr/102660&quot;&gt;https://support.apple.com/ko-kr/102660&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  기기에 설치된 최신 버전의 Apple Developer App&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  iCloud에 로그인 된 기기&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  등록 시작&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  등록을 위해 사용하는 기기에서 Apple Developer 앱을 실행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  계정 탭을 선택합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Apple ID로 로그인합니다. 기기에 로그인 된 Apple ID와 달라도 괜찮지만, 이중 인증이 켜져 있어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Apple Developer 계약이 표시되면 검토한 후 동의를 탭하거나 클릭합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  지금 등록하기를 탭하거나 클릭합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  프로그램 혜택 및 요구 사항을 검토하고 계속을 탭하거나 클릭합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이후 개인정보를 등록하는데, macOS 자체에서 진행하는 경우 추가 인증의 벽에서 가로 막힌다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ url : &lt;a href=&quot;https://developer.apple.com/account&quot;&gt;https://developer.apple.com/account&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그래서 Web에서 직접 하는 것을 권유하는데, 가격대는 1년 129,000원으로 상당히 빡세다. 또한, 멤버십을 구입한다 하더라도 48시간 이내에 완료 처리가 진행되어야 추후 일정을 진행할 수 있으므로 잘 생각하길 바란다.&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/266</guid>
      <comments>https://malangdidoo.tistory.com/266#entry266comment</comments>
      <pubDate>Thu, 11 Apr 2024 15:34:39 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] (Project) MapleSpy: 43. macOS 프로젝트 App Icon 설정</title>
      <link>https://malangdidoo.tistory.com/265</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;  App Icon&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Android는 android 폴더 내부에 app icon 관련 파일을 추가해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;마찬가지로 iOS의 경우도 ios 파일 내부에 app icon 관련 파일을 추가해주어야, App이 다운로드 됐을 때 원하는 아이콘으로 설정이 가능하다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://www.appicon.co/&quot;&gt;https://www.appicon.co/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Icon Generator를 활용하면 Assets.xcassets와 appstore.png가 생성되는데, Assets.xcassets 폴더 전체를 그대로 Xcode의 Assets.xcassets와 변경해줍니다. vsCode를 활용한다면, &quot;ios&quot; &amp;rarr; &quot;Runner&quot; 폴더 하위에 덮어 씌우시면 됩니다.&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/265</guid>
      <comments>https://malangdidoo.tistory.com/265#entry265comment</comments>
      <pubDate>Thu, 11 Apr 2024 15:30:40 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] (Project) MapleSpy: 42. macOS 프로젝트 실행 with iOS Simulator</title>
      <link>https://malangdidoo.tistory.com/264</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;macOS에서 Flutter 프로젝트를 실행하고 Emulator에서 결과를 확인하는 방법을 기술하겠습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  Xcode Simulator 설정&lt;/h4&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://developer.apple.com/documentation/safari-developer-tools/adding-additional-simulators&quot;&gt;https://developer.apple.com/documentation/safari-developer-tools/adding-additional-simulators&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;시뮬레이터 런타임은 특정 OS 및 버전에 특정한 장치를 부팅할 때 시뮬레이터가 로드하는 OS 패키지입니다. 그런 다음 시뮬레이터 런타임은 iPhone 14 또는 iPad Pro와 같은 다양한 장치 유형을 갖춘 다양한 시뮬레이터에서 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;다른 플랫폼이나 OS 버전에서 테스트하려면 해당 플랫폼에 대한 시뮬레이터 런타임을 추가한 다음 새 시뮬레이터를 만들어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Simulator 추가&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;macOS 메뉴 모음의 &quot;Xcode&quot;에서 &quot;settings&quot;에 접근합니다.&lt;/li&gt;
&lt;li&gt;platforms 탭을 선택합니다.&lt;/li&gt;
&lt;li&gt;좌측 하단 &quot;+&amp;rdquo; 버튼을 통해 iOS Simulator 버전을 확인하고 다운로드 및 설치를 클릭합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이후, &quot;Xcode&quot; &amp;rarr; &quot;Open Developer Tool&quot; 경로의 &quot;Simulator&quot;를 선택했을 때, Simulator가 실행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;OS 버전 별로 설치하여, 내부에 존재하는 다양한 기기에 대해 접근이 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;단, 현재 macOS 버전에 맞춘 기본 iOS Platform은 무조건 추가해주세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  프로젝트 실행&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Flutter 프로젝트 실행 전, ios 폴더 내부에 완성되지 않은 &quot;Profile&quot;과 &quot;Profile.lock&quot; 파일이 존재한다면 삭제 후 진행해주세요. 만약 &quot;Profile&quot;파일은 존재하는데 &quot;Profile.lock&quot; 파일은 존재하지 않다면, 해당 &amp;ldquo;Profile&quot; 파일은 무조건 삭제 후 재 시작해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&quot;Profile&quot; 파일과 &quot;Profile.lock&amp;rdquo; 파일을 생성하는 시간은 꽤 오래 걸리므로, 잠시 쉬었다 오는 것을 추천드립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이후 프로젝트가 정상적으로 실행되는 Simulator를 확인하실 수 있습니다.&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/264</guid>
      <comments>https://malangdidoo.tistory.com/264#entry264comment</comments>
      <pubDate>Thu, 11 Apr 2024 15:23:06 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] (Project) MapleSpy: 41. macOS 환경 설정 with Flutter</title>
      <link>https://malangdidoo.tistory.com/263</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;  macOS 언어 변경&lt;/h4&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://support.apple.com/ko-kr/guide/mac-help/mh26684/mac&quot;&gt;https://support.apple.com/ko-kr/guide/mac-help/mh26684/mac&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&quot;Apple 메뉴  &quot; &amp;rarr; &quot;시스템 설정&quot; &amp;rarr; &quot;일반&quot; &amp;rarr; &quot;언어 및 지역&quot; 경로를 통해 언어 설정이 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&quot;+&quot;를 통해 언어를 추가하고, 최상단으로 드래그하여 언어를 설정하세요.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  macOS 버전 확인&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&quot;Apple 메뉴  &quot; &amp;rarr; &quot;이 Mac에 관하여&quot; 경로를 통해 macOS 버전 확인이 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  드라이버 장치 여유 공간 확인&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&quot;Apple 메뉴  &quot; &amp;rarr; &quot;이 Mac에 관하여&quot; &amp;rarr; &quot;저장 공간 탭&quot; 경로를 통해 남은 여유 공간 확인 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;macOS 업데이트 시 새롭게 저장 공간을 차지해야 하기 때문에, 꼭 여유 공간을 확보하고 업데이트를 진행해야 합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  macOS 업데이트&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&quot;Apple 메뉴  &quot; &amp;rarr; &quot;시스템 환경 설정&quot; &amp;rarr; &quot;소프트웨어 업데이트&quot; 경로를 통해 &quot;지금 업그레이드하기&quot;를 클릭하여 macOS를 업데이트 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;일반적으로 macOS 업데이트가 진행될 때 데이터가 제거되거나 하드 드라이브가 지워지지 않습니다. 기기를 신뢰하지 않는 경우 백업을 하는 것이 바람직 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;대부분 macOS 업데이트로 인해 데이터가 손실되는 경우는, 업데이트 설치 프로세스가 잘못 되어 하드 드라이브가 손상되는 경우입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  App Store에서 Apple ID 허용&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;다양한 라이브러리를 App Store에서 다운로드 받을 때, Apple ID가 App Store에 허용된 상태여야 한다. 만약 Apple ID가 App Store에 허용되지 않는다면, 다운로드를 진행할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그러나, 이후 바로 Apple ID가 App Store에 허용될 수 있도록 &quot;Apple ID 완성하기&quot;를 진행하는 창이 나온다. 해당 과정을 진행하면 허용 설정이 완료된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  Xcode&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Xcode&amp;nbsp;15를 사용하면 전체 Apple 플랫폼용 앱을 개발, 테스트, 배포할 수 있습니다. 향상된 코드 완성, 대화형 미리보기 및 라이브 애니메이션을 통해 앱을 더 빠르게 코딩 및 설계하고, Git 스테이징을 사용하여 코딩을 중단하지 않고 다음 커밋을 작성할 수 있습니다. 비디오 녹화가 포함된 새로운 테스트 보고서로 테스트 결과를 살펴보고 진단해 보세요. Xcode&amp;nbsp;Cloud에서 TestFlight 및 App&amp;nbsp;Store에 원활하게 배포할 수 있습니다. 이제 멋진 앱을 더욱 간편하게 개발할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://developer.apple.com/kr/xcode/&quot;&gt;https://developer.apple.com/kr/xcode/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Xcode를 설치하기 위해선 기본적으로 macOS 14.0 이상의 Version이 필요합니다. macOS 버전 확인과 업데이트 과정을 진행해주세요.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  Apple Developer&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Apple Developer를 설치하기 위해선 macOS 13.0 이상의 Version이 필요합니다. macOS 버전 확인과 업데이트 과정을 진행해주세요.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  Homebrew 설치&lt;/h4&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://brew.sh&quot;&gt;https://brew.sh&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;홈페이지 내부 &quot;Install Homebrew&quot;에서 설치 명령어를 복사한 후, 터미널에 입력&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;/bin/bash -c &quot;$(curl -fsSL &amp;lt;https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh&amp;gt;)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;터미널은 Dock에서 Launchpad 상단에서 &quot;terminal&quot;을 검색하여 초기 실행 가능&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  git 설치&lt;/h4&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;brew update
brew install git
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  node 설치&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발을 진행하지 않는다면, node는 사실 상 사용하지 않는 것 같습니다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;brew update
brew install node
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  Java 설치&lt;/h4&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://www.oracle.com/java/technologies/downloads/#jdk20-mac&quot;&gt;https://www.oracle.com/java/technologies/downloads/#jdk20-mac&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발을 진행하지 않는다면, Java는 사실 상 사용하지 않는 것 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;프로젝트 내부 버전과 동일한 Java 다운로드 및 압축해제&lt;/li&gt;
&lt;li&gt;jdk 폴더 내 /Contents/Home 경로 접근&lt;/li&gt;
&lt;li&gt;pwd 명령어로 java가 설치된 주소 복사&lt;/li&gt;
&lt;li&gt;해당 위치에서 vi ~/.bash_profile 명령어를 통해 bash_profile 파일 편집&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;// 입력 전 키보드 i 입력 필수(insert 약자)
// 이후, 하위 코드 작성
JAVA_HOME={pwd경로}
PATH=$PATH:$JAVA_HOME/bin
export JAVA_HOME
export PATH
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;작성 완료 시 esc로 insert 해제 후, :wq! 명령어로 저장하고 나가기 진행&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;환경변수 값 적용&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;cd ~/
source .bash_profile
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;적용 확인&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;echo $JAVA_HOME
java -version
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  Flutter 설치&lt;/h4&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://docs.flutter.dev/release/archive?tab=macos&quot;&gt;https://docs.flutter.dev/release/archive?tab=macos&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;프로젝트 내부 Flutter와 동일한 버전을 다운로드 받습니다. 경로는 어디든 상관 없습니다. 단, 설치 후 flutter/bin 폴더의 경로를 pwd 명령어를 활용해 확인해둡니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Zsh 환경 변수 파일을 수정합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;FLUTTER_HOME={pwd경로}
PATH=$FLUTTER_HOME:$PATH
export FLUTTER_HOME
export PATH
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이후 변경된 파일을 적용하고, 환경 변수를 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;cd ~/
source .zshenv
echo $FLUTTER_HOME
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Xcode 구성&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://docs.flutter.dev/get-started/install/macos/desktop?tab=vscode#configure-ios-development&quot;&gt;https://docs.flutter.dev/get-started/install/macos/desktop?tab=vscode#configure-ios-development&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Xcode 버전을 사용할 수 있도록 명령줄 도구를 구성하기 위해 다음 명령어를 실행 합니다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;sudo sh -c 'xcode-select -s /Applications/Xcode.app/Contents/Developer &amp;amp;&amp;amp; xcodebuild -runFirstLaunch'
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이후 Xcode 라이센스 계약에 서명합니다.&lt;/p&gt;
&lt;pre class=&quot;ceylon&quot;&gt;&lt;code&gt;sudo xcodebuild -license
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Xcode는 최신 버전을 유지하는 것을 권장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  CocoaPods 설치&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://docs.flutter.dev/get-started/install/macos/desktop?tab=vscode#install-cocoapods&quot;&gt;https://docs.flutter.dev/get-started/install/macos/desktop?tab=vscode#install-cocoapods&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앱이&amp;nbsp;기본 데스크톱 코드가 포함된&amp;nbsp;Flutter 플러그인 에 의존하는 경우&amp;nbsp;CocoaPods를&amp;nbsp;설치하세요 . 이 프로그램은 Flutter와 데스크톱 코드 전반에 걸쳐 다양한 종속성을 번들로 제공합니다.&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;sudo gem install cocoapods
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이때 Ruby 버전이 낮을 경우 cocoapods 설치에 실패하며, Ruby &amp;amp; RubyGems를 지원하는 drb의 마지막 버전이 2.0.6이라 안내되며, drb에는 Ruby 버전 2.7.0 이상이 필요하다는 안내와 함께, 현재 Ruby의 버전을 알려줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Ruby 버전 업데이트&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Ruby를 다운 받거나 업데이트하기 위해선 brew와 rbenv가 필요합니다. brew는 앞에서 설치하였으므로 넘어갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;rbenv는 루비의 버전을 독립적으로 사용할 수 있도록 도와주는 패키지 입니다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;// rbenv 설치
brew install rbenv
// 설치 가능한 Ruby 버전 확인
rbenv install -l
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;가능한 최신 버전의 Ruby를 설치하는 것을 권고하나, 현재는 2.7 버전 이하의 경우 모두 declared 되었으므로 아래 Hombrew에서 제공하는 Ruby 관련 업데이트 버전을 확인하여 적용하길 바랍니다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://formulae.brew.sh/formula/ruby#default&quot;&gt;https://formulae.brew.sh/formula/ruby#default&lt;/a&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// Ruby 설치
rbenv install 3.1.4
rbenv versions
rbenv global 3.1.4
// Ruby 버전 확인
ruby --version
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;하지만 이 경우에도 ruby 버전이 변경되지 않는 참사가 일어날 수 있다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;rbenv init
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;위 명령어를 실행하여, 자동으로 rbenv가 로드될 수 있도록 ~/.zshrc 파일의 마지막에 다음 문구를 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;eval &quot;$(rbenv init - zsh)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이후 터미널을 재 실행하면, 버전이 잘 적용된 것을 확인할 수 있습니다. 모든 것이 완벽해졌다면, 다시 cocoapods를 설치해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이후 ~/.zshenv 환경 변수 파일에 새로운 환경 변수를 설정합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;export PATH=$HOME/.gem/bin:$PATH
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  git clone&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Xcode를 실행했을 때, git account를 먼저 등록해야 합니다. 이후, git clone 경로를 입력하여, repository를 복제합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  VsCode 설치&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://code.visualstudio.com/download&quot;&gt;https://code.visualstudio.com/download&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;소스 코드 편집기를 다운로드 받습니다. 이후 확장프로그램, Extension에서 Dart를 필수적으로 받아주어야 합니다.&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/263</guid>
      <comments>https://malangdidoo.tistory.com/263#entry263comment</comments>
      <pubDate>Thu, 11 Apr 2024 15:21:57 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] (Project) MapleSpy: 40. Google Play 검색 최적화</title>
      <link>https://malangdidoo.tistory.com/262</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;Google Play에서는 애플리케이션이 어떤 방식으로 검색되고, 어떻게 설정해야 검색이 잘 되도록 할 수 있는지 다음과 같은 링크에서 안내하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  앱 검색 가능성 및 순위&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;a href=&quot;https://support.google.com/googleplay/android-developer/answer/9958766?hl=ko&quot;&gt;https://support.google.com/googleplay/android-developer/answer/9958766?hl=ko&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  Google Play에서 검색되도록 설정하기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;a href=&quot;https://support.google.com/googleplay/android-developer/answer/4448378?hl=ko&amp;amp;ref_topic=3450769&amp;amp;sjid=17634888397704337259-AP&quot;&gt;https://support.google.com/googleplay/android-developer/answer/4448378?hl=ko&amp;amp;ref_topic=3450769&amp;amp;sjid=17634888397704337259-AP&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;내용에 대해 빠르게 적용하고 싶다면, 기본적으로 다음 행동을 진행할 수 있습니다. 단, 각 단계는 주의 사항을 포함하고 있으니 유의하세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  앱 품질이 더 높은 앱이, 품질이 떨어지는 앱보다 잘 보이게 알고리즘이 강화됨&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앱 성능을 개선하여 앱 속도, 배터리 소모량, 충돌이 자주 발생하지 않는 앱을 만들어가는 것이 앱 품질 향상에 효과적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  앱 키워드 설정 및 노출&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;애플리케이션을 가장 잘 나타내면서, 사람들이 주로 검색하는 키워드를 사용해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;또한, 애플리케이션이 다방면에서 검색될 수 있도록 애플리케이션 이름을 선정하는 것이 중요합니다. 가능하다면 hyphen (-)을 사용하여 사람들이 주로 검색하는 짧은 키워드를 사용하고, 이를 통해 애플리케이션을 더욱 자주 노출시킬 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앱 설명에도 특정 키워드를 포함한 내용이 들어가는 것이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그러나, 너무 과도하게 사용하거나 반복적인 키워드를 열거하는 경우, &quot;메타데이터 정책 위반&quot;으로 애플리케이션이 정지되고 삭제될 수 있으므로 주의해야 합니다.&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/262</guid>
      <comments>https://malangdidoo.tistory.com/262#entry262comment</comments>
      <pubDate>Sun, 7 Apr 2024 02:09:46 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] (Project) MapleSpy: 39. Google Play 정식 출시</title>
      <link>https://malangdidoo.tistory.com/261</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;드디어...! 오랜 혈투 끝에 애플리케이션을 정식 출시하였습니다!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Google Play Console에 참여한 신입 개발자에게, 애플리케이션이 출시되기 위한 자세한 과정을 더욱 각인시켜주기 위한 노력을 진행하고 있는 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2주간의 비공개 테스트 기간을 진행함으로써, 개발자가 사용자와 소통하고 애플리케이션이 더욱 발전되고 안정화된 모습으로 변해갈 수 있었습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  개인적인 다짐&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;처음에는 빠르게 출시하여 사용자들에게 서비스를 제공하고 싶었기 때문에, 비공개 테스트 기간이 마냥 필요없고 너무나도 긴 시간과 과정을 할애한다는 생각이었으나, 객관적으로 애플리케이션에서 부족했던 부분을 개선하고 기본으로 제공해야 할 내용을 추가함으로써 꼭 필요한 기간이라고 인식되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앞으로도 새로운 애플리케이션을 제작할 때, 특정한 제약 조건이 정해져 있지 않더라도 비공개 테스트 기간을 거침으로써 사용자와 소통하고 애플리케이션의 부족한 모습을 개선하는 시간을 꼭 가져야 겠다는 다짐을 하게 되었습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  비공개 테스트 진행과 프로덕션용 출시 진행의 차이&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;비공개 테스트 기간에 대한 애플리케이션 평가는 프로덕션용 애플리케이션의 평가와 다르다는 점을 알 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;비공개 테스트 기간에는 애플리케이션 자체에 대한 테스트 진행을 하지 않고, 사용자들과 소통하며 애플리케이션을 더욱 발전시켜 나가라는 뜻을 담고 있는 것 같습니다. 관리자가 애플리케이션의 동작에 대한 큰 신경을 쓰지 않았기 때문이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그러나, 프로덕션용 애플리케이션의 평가는 달랐습니다. 애플리케이션의 소개에서 제공되는 내용에 대한 주장이, 실제 애플리케이션을 이용했을 때 반영되고 있는지, 즉 주장이 합당한지 면밀히 검토했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;실제 &amp;ldquo;사기 행위 정책 위반&amp;rdquo; 항목을 통해 애플리케이션이 정지 당했던 사례가 있기 때문에, 애플리케이션에서 정확한 정보를 전달하고 있는지, 또한 애플리케이션을 테스트할 수 있는 정보를 제공하고 있는지 면밀이 검토해야 함을 깨닫게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그리고 &quot;손상된 기능 정책 위반&quot;을 통해, 사용자와 개발자가 암묵적으로 인지하고 있는 내용에 대해서도 자세한 설명이 필요할 수 있음을 깨달았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이런 과정을 거쳐, 프로덕션용 애플리케이션의 평가는 비공개 테스트 기간에서 진행했던 평가와 크게 달랐음을 느꼈고, 준비해야 하는 것이 많다고 생각했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이번에 출시된 애플리케이션의 경우, 사용자 정보를 저장하지 않으며, 추가적인 활동 및 다양한 동적 콘텐츠를 제공하고 있지 않으므로 크게 문제될 내용을 만들어내지 않았습니다. 이런 부분이, 실제 사용자에게 적용될 때 Google Play 정책과 개발자 정책에 위배되는 사항이 무엇인지 검토하고, 몸소 겪으며 더 발전해 나갈 부분이 존재함을 깨달았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이전에 기술했던 컬럼들이, 저와 비슷한 경험을 겪은 분들에게 가벼운 가이드라인이 되어, 비슷한 깨달음을 얻어가셨으면 좋겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;또한, 저의 마지막 회고에 대한 내용이, 많은 신입 개발자분들이 애플리케이션 출시를 진행할 때, 비공개 테스트와, 프로덕션용 출시 과정에 대해 첫 느낌을 알아가실 수 있는 기회가 되었으면 좋겠습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  앞으로&amp;hellip;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;MapleSpy는 메이플스토리 게임이 끝나지 않는 한, 지속적으로 발전하기 위해 더 많은 사용자에게 피드백을 받고 개선되기 위한 노력을 진행할 것입니다. 앞으로의 내용은, 사용자의 오류 및 개선사항에 대해 어떻게 대처하고 해결해 나갔는지에 대해 기술하도록 하겠습니다.&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/261</guid>
      <comments>https://malangdidoo.tistory.com/261#entry261comment</comments>
      <pubDate>Sun, 7 Apr 2024 01:26:22 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] (Project) MapleSpy: 38. 앱 거부 이의제기</title>
      <link>https://malangdidoo.tistory.com/260</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;애플리케이션의 비공개 테스트를 진행하거나, 출시 버전을 관리자에게 게시함으로써 애플리케이션에 대한 정책 위반 요소가 포함되어 있는지 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;대부분의 경우, 메일을 통해 &quot;어떤 정책을 위반했는지&quot; 그리고 &quot;위반을 초래한 원인이 무엇인지&quot;를 정확하게 말해주기 때문에, 개발자가 해당 내용을 보고 애플리케이션이 더 발전될 수 있도록 수정해 나가면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;하지만 저의 경우, 대부분의 유저가 알고있는 사실과 정확한 데이터값을 전달하고 있는 기능에 대해 문제가 발생했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;데이터는, 모두 동일한 데이터를 가지고 있었고 그로 인해 애플리케이션의 버튼을 통해 변경되어야 할 데이터가 동일한 데이터로 인해 변경되지 않는 것처럼 보였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;관리자는 이 경험을 앱 콘텐츠가 특정 사용자 입력에 반응하지 않는, 콘텐츠를 새롭게 반환하지 않는 행위라 판단하고 &quot;손상된 기능 정책 위반&quot;을 제기하며 앱이 거부 당했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그래서 저는 또다시 이의 제기를 하러 갔습니다&amp;hellip; 이의 제기에 대한 내용은 다음과 같이 작성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  인사&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  반응하지 않은 앱과, 콘텐츠를 새롭게 반환하지 않은 결과를 초래한 &quot;사용자 입력&quot;에 대한 기본적인 설명&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  사용자 입력과 연관된, 데이터 설정 값의 경우의 수 소개&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  거부 당한 사유에 대한 주장&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  주장을 뒷받침할 수 있는 사레 (&amp;rArr; 올바르지 않다 판단된 사례와 올바른 판례의 차이)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  정리 및 부탁&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  마무리&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  이의제기 확인&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이의제기 확인 메일은 매크로인 것 같다. 담당자가 배정되기 전, 무조건 전송되는 메일로 보인다. 근데 이번에는 담당자가 배정되는 것이 아닌 이의 제기에 대해 바로 확인하고 즉각 조치가 이루어져 다음 진행해야 할 조치에 대해 안내하는 글을 받았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;글이 잘 짜여졌는지 모르겠지만, 내가 제기하는 주장과 애플리케이션의 진행이 올바르다 판단되어 새롭게 게시를 진행할 수 있도록&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&quot;조치 필요: 앱 제출&quot;이라는 조치를 받았다. 기존에 진행했던 대로 추가 게시를 진행하면 되는 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;애플리케이션을 배포하는데&amp;hellip; 구글 관리자 분들과 많이 싸우는 것 같다&amp;hellip; 좋은걸까&amp;hellip;? 싶지만, 또 하나의 경험이라고 생각하고, 다른 분들에게도 도움이 될 수 있도록 글을 작성해 봅니다.&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/260</guid>
      <comments>https://malangdidoo.tistory.com/260#entry260comment</comments>
      <pubDate>Thu, 4 Apr 2024 10:54:46 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] (Project) MapleSpy: 37. 앱 액세스 권한 추가</title>
      <link>https://malangdidoo.tistory.com/259</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;Play Console에서 Google 관리자가 애플리케이션을 테스트하기 위한 정보를 꼭 제공해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이 정보를 제공하지 않으면, 추후 컨텐츠를 경험할 수 없기 때문에 &quot;사기성 앱&quot;이라 판단되어 애플리케이션이 정지되므로 주의해야한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  앱 액세스 권한 설정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앱 액세스 권한은 Google Play Console에서 액세스 권한을 제공할 애플리케이션을 선택 후, 좌측 하단 &quot;정책 및 프로그램&quot; 카테고리의 &quot;앱 콘텐츠&quot; 항목을 통해 접근할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  앱 액세스 권한 추가&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앱이 검토되기 위해선 Google Play에서 앱의 모든 부분에 액세스할 수 있어야 한다고 명시해줬었습니다. 액세스에 제한된 항목은 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  사용자 이름 및 비밀번호&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  2단계 인증&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  위치 기반 엑세스&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  맴버십 및 정기 결제&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  다른 기기에서 실행해야 하는 작업&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;또한 검토자가 실행할 수 없는 작업은 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  계정을 만들거나, 자신의 기존 계정을 사용하여 앱에 액세스 하는 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  무료 체험판을 사용하여 앱에 액세스 하는 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  개발자에게 자세한 내용을 문의하는 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그래서 우리는 검토자의 편의를 위해 일명 &quot;테스트 계정&quot;을 추가해야 하고, 그 항목에 대한 설정 진행은 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  액세스 항목 추가&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;해당 화면에서 &quot;앱의 전체 또는 일부 기능이 제한됨&quot; 항목을 클릭하면, &quot;안내 추가&quot;를 확인할 수 있습니다. 우리는 이 &quot;안내 추가&quot;를 활용해서, 검토자에게 테스트 계정을 제공할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  안내 추가&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Google Play에서 앱의 제한된 부분에 액세스할 수 있도록 안내를 추가하세요. 이 정보가 최신 상태인지 확인해야 합니다. 업데이트를 제출할 때마다 Google Play에서 제공된 정보를 사용하여 앱을 검토합니다. 정보는 영어로 제공해야 합니다. 검토 절차가 지연될 수 있으므로 정책 지원팀에 이 정보 제공을 위해 문의하지 마시기 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;안내 추가에서 설정할 정보는 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  안내 이름(필수, ex: 미국에서 앱에 액세스 하는 방법 안내)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;간결하지만, 왜 제공하고 있는지 명확하게 제시해주어야 한다 생각합니다. 만약 다양한 계정을 지원하는 경우 [ 순서에 맞게 &quot;,&quot;로 구분된 계정 제공 ]과 같은 방법이면 괜찮을 것이라 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  사용자 이름 및 비밀번호&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;만약 비밀번호를 제공하지 않는 액세스 계정이라면, 이름만 입력하고 비밀번호는 공란으로 두어도 괜찮습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;단, 앱에 액세스하기 위해 두 개 이상의 정보가 필요한 경우, 앱 액세스에 필요한 기타정보란에 사용자 이름 및 비밀번호 데이터를 제공하는 것이 바람직하다 안내되고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  앱 액세스에 필요한 기타 정보&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2단계 인증, QR코드, 위치 기반 액세스, 멤버십, 생체인식과 같은 정보들의 경우 이 항목에 작성합니다. 또한, 관련하여 설명해야 할 안내는 보통의 경우 다음과 같습니다&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;생체 인식 로그인 또는 2단계 인증 우회 방법에 대한 안내&lt;/li&gt;
&lt;li&gt;앱에 액세스하는 데 필요한 QR 코드나 바코드에 액세스할 수 있는 정적 URL&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이후 마지막에 존재하는 &quot;앱에 액세스 하는데 다른 정보가 필요하지 않음&quot;의 경우, 현재까지 제공된 정보 이외의 다른 정보를 의미하기 때문에 선택 후 완료를 진행합니다.&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/259</guid>
      <comments>https://malangdidoo.tistory.com/259#entry259comment</comments>
      <pubDate>Wed, 3 Apr 2024 10:47:24 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] (Project) MapleSpy: 36. 앱 정지 및 이의제기와 결과</title>
      <link>https://malangdidoo.tistory.com/258</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;앱 출시를 기대하고 있던 나에게, 앱 정지라는 결과가 돌아왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앱 정지 상태가 되면, 앱과 관련된 모든 기록이 Google Play에서 삭제된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그리고 Play Console에서 기존에 진행해왔던 비공개 테스트, 프로덕션, 기본 스토어 등록정보가 포함된 앱 정보 등 모든 정보에 대한 열람이 거부된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앱을 출시하기 전, 내가 어떤 내용으로 비공개 테스트를 진행했는지 그리고 스토어 등록정보에 어떤 것을 추가했는지 정확하게 자료로 남겨놓길 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;필자는 관련 전체 정보는 지니고 있으나, 정확한 설정 정보와 스토어 등록 정보에 대해 보관해두지 않아서 이의 신청 시에 기억을 거슬러 올라가야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;꼭&amp;hellip;. 꼭&amp;hellip;. 기록해두자. 기록이 절대 나쁜 것이 아니다&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  이의 제기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이의 제기 하는 방법은 단순했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앱이 정지된 상태를 야기한 문제에 대해, 당신이 생각하는 부분과 내가 생각했던 부분 그리고 기대할 수 있는 결과값에 대해 이야기를 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이렇게 말할 수 있는 근거는, 내가 &quot;사기 행위 정책 위반&quot;사항에 해당됐기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  사기 행위 정책 정보&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&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;/ol&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;나는 1, 2에 해당된다 생각했다. 그 이유는, 문제 세부정보에서 함께 첨부된 스크린샷을 보고 알 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;스크린샷은 &quot;인앱 경험&quot;과 &quot;앱 스크린샷&quot;으로 구분되는데, 인앱 경험은 관리자가 직접 테스트했던 경험에 대한 스크린샷이고, 앱 스크린샷은 &quot;기본 스토어 등록정보&quot;에 저장된 이미지였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;내 앱은 &quot;특정 검색어&quot;를 검색했을 때 성공하거나 실패하는 결과를 맞이할 수 있다. 하지만 출시 테스트를 진행하던 관리자는 데이터가 존재하지 않는 검색어를 통해 실패를 경험하고 &quot;내가 기본 스토어 등록정보에 등록한 스크린샷&quot;을 경험할 수 없었기에 &quot;사기 행위 정책 정보&amp;rdquo;에 위반됨을 고지한 것 같았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;나는 이와 관련해 이의 제기를 다음과 같이 신청하였다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&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;li&gt;감사의 말씀&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  배정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이의 제기가 확인이 되면, Google Play팀에서 담당자를 배정해 주십니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;처음 이메일을 받았을 때는, 한국 지사에 계신 담당자분으로 느껴졌습니다. 관련 문제가 조사되는 동안 공유할 수 있는 추가 정보가 확인되면 연락을 준다고 하였습니다. 담당자 분이 배정되면, 관련된 소통은 이메일로 자유롭게 진행할 수 있는 것 같았습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  이의제기 확인&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;해당 이의 제기가 받아 들여지기 까지 약 2주의 기간이 걸렸습니다. 애플리케이션 복원은, 게시 개요를 마지막으로 제출하기 전 상태로 애플리케이션 상태를 돌려줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앱을 게시하기 전, 앱이 모든 개발자 프로그램 정책을 준수하는지 꼭 확인 후 게시를 해야하며, 로그인 사용자 인증 정보가 있는 경우, Google Play Console &quot;앱 엑세스&quot; 섹션을 통해 유효한 로그인 사용자 인증 정보 및 앱 검토에 필요한 기타 모든 리소스를 제공하는 것이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;제가 실수했던 부분은, 애플리케이션에 대한 테스를 진행하는 관리자에게 &quot;앱 엑세스&quot;를 통해 테스트와 관련된 정보를 넘기지 못했다는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이렇게 애플리케이션이 더 발전하고, 실수했던 부분에 대해 검토하여 해당 안내를 진행해주면서, 이의제기에 대한 확인이 완료됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;상당히 조마조마한 마음가짐을 가지고 기다렸습니다. 내가 오랫동안 쌓아올린 노력의 탑이, 한 순간의 실수로 무너져 내리는 것 같았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;애플리케이션을 배포하기 전, 최대한 많은 글을 찾아보고, 그 속에서 진행해야 할 부분이 무엇인지 명확하게 확인하고 진행하기를 바랍니다.&lt;/b&gt;&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/258</guid>
      <comments>https://malangdidoo.tistory.com/258#entry258comment</comments>
      <pubDate>Tue, 2 Apr 2024 22:01:38 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] OAuth, Kakao Login 개발</title>
      <link>https://malangdidoo.tistory.com/257</link>
      <description>&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://developers.kakao.com/docs/latest/ko/kakaologin/flutter&quot;&gt;https://developers.kakao.com/docs/latest/ko/kakaologin/flutter&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ design : &lt;a href=&quot;https://developers.kakao.com/docs/latest/ko/kakaologin/design-guide&quot;&gt;https://developers.kakao.com/docs/latest/ko/kakaologin/design-guide&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Kakao developers의 개발 및 디자인 가이드에 따라 kakaologin을 시작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  설치&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://developers.kakao.com/docs/latest/ko/flutter/getting-started#apply-sdk&quot;&gt;https://developers.kakao.com/docs/latest/ko/flutter/getting-started#apply-sdk&lt;/a&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;$ flutter pub add kakao_flutter_sdk
$ flutter pub get
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  애플리케이션 정보&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://developers.kakao.com/console/app&quot;&gt;https://developers.kakao.com/console/app&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;kakaologin을 시작하기에 앞서 내 애플리케이션 항목에서 애플리케이션을 추가하여 관련 정보를 얻어야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  초기화&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://developers.kakao.com/docs/latest/ko/flutter/getting-started#init&quot;&gt;https://developers.kakao.com/docs/latest/ko/flutter/getting-started#init&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;애플리케이션에서 다운로드 받은 Flutter SDK를 초기화 시키는 것이 첫 작업입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;main()에서 runApp()을 호출하기 전, KakaoSdk를 초기화합니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;void main() async {
  await dotenv.load(fileName: '.env');

  KakaoSdk.init(
    nativeAppKey: dotenv.get('KAKAO_NATIVE_APP_KEY'),
  );

  runApp(MyApp());
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;관련 Key는 노출되지 않도록 dotenv 라이브러리를 활용하거나 개별적으로 관리하세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;또한, Flutter의 minSdkVersion이 21보다 낮은 경우에 버전 문제를 일으킬 수 있습니다. android/app/build.gradle 경로의 android/defaultConfig 속성에 minSdkVersion을 21로 설정하세요&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  프로젝트 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  인터넷 사용 권한&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Flutter Project/android/app/src/AndroidManifest.xml 파일의 &amp;lt;manifest&amp;gt; Element 하위에 인터넷 사용 권한을 설정하는 &amp;lt;uses-permission&amp;gt; Element를 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;&amp;lt;uses-permission android:name=&quot;android.permission.INTERNET&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  커스텀 URL 스킴&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://developers.kakao.com/docs/latest/ko/flutter/getting-started#project-scheme&quot;&gt;https://developers.kakao.com/docs/latest/ko/flutter/getting-started#project-scheme&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;커스텀 URL 스킴(Custom URL Scheme)은 사용자가 Android와 iOS 환경에서 카카오톡으로 로그인 후 서비스 앱으로 돌아오거나, 카카오톡 메시지 버튼 또는 링크로 서비스의 앱을 실행하는 데 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Android&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Flutter Project/android/app/src/AndroidManifest.xml 파일을 수정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;단, AndroidManifest.xml 파일을 수정할 때 생성하는 &amp;lt;activity&amp;gt; Element에 대해 Android 12(API 31) 이상을 타깃하는 경우, android:exported 속성을 반드시 true로 선언해주어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  카카오 로그인&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;lt;application&amp;gt; Element 하위에 카카오 로그인 Redirect URI 설정을 위한 &amp;lt;activity&amp;gt; Element를 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- 카카오 로그인 커스텀 URL 스킴 설정 --&amp;gt;
&amp;lt;activity 
    android:name=&quot;com.kakao.sdk.flutter.AuthCodeCustomTabsActivity&quot;
    android:exported=&quot;true&quot;&amp;gt;
    &amp;lt;intent-filter android:label=&quot;flutter_web_auth&quot;&amp;gt;
        &amp;lt;action android:name=&quot;android.intent.action.VIEW&quot; /&amp;gt;
        &amp;lt;category android:name=&quot;android.intent.category.DEFAULT&quot; /&amp;gt;
        &amp;lt;category android:name=&quot;android.intent.category.BROWSABLE&quot; /&amp;gt;

        &amp;lt;!-- &quot;kakao${YOUR_NATIVE_APP_KEY}://oauth&quot; 형식의 앱 실행 스킴 설정 --&amp;gt;
        &amp;lt;!-- 카카오 로그인 Redirect URI --&amp;gt;
        &amp;lt;data android:scheme=&quot;kakao${YOUR_NATIVE_APP_KEY}&quot; android:host=&quot;oauth&quot;/&amp;gt;
    &amp;lt;/intent-filter&amp;gt;
&amp;lt;/activity&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  카카오톡 공유, 카카오톡 메세지&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;android:name=&quot;.MainActivity&quot; 속성을 갖는 &amp;lt;activity&amp;gt; Element 하위에 다음 &amp;lt;data&amp;gt; Element를 포함하는 &amp;lt;intent-filter&amp;gt; Element를 추가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;대부분의 경우 &amp;lt;intent-filter&amp;gt; Element가 존재하므로, 내부 &amp;lt;data&amp;gt; Element에 대해서만 추가할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;intent-filter&amp;gt;
	&amp;lt;data android:host=&quot;kakaolink&quot; android:scheme=&quot;kakao${YOUR_NATIVE_APP_KEY}&quot; /&amp;gt;
&amp;lt;/intent-filter&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  API KEY 관리&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Flutter Project의 android 폴더 하위에는 local.properties 파일이 있습니다. 대부분의 경우 이 폴더에서 api key를 추가적으로 관리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;local.properties 파일에서 api key를 작성하였다면, Flutter Project/android/app경로의 build.gradle에서 BuildConfig에 변수를 추가하면 완료입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;local.properties를 build.gradle에서 사용하기 위해 다음과 같이 properties 객체를 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이후, android&amp;gt;defaultConfig에 manifestPlaceholder를 활용하여 api key에 대한 변수를 지정합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://developer.android.com/build/manage-manifests?hl=ko&amp;amp;_gl=1*1pzo7kg*_up*MQ&quot;&gt;https://developer.android.com/build/manage-manifests?hl=ko&amp;amp;_gl=1*1pzo7kg*_up*MQ&lt;/a&gt;.._gaMTMxMzcyODE0LjE3MDg5MzM5MDI._ga_6HH9YJMN9MMTcwODkzMzkwMS4xLjAuMTcwODkzMzkwMS4wLjAuMA..#inject_build_variables_into_the_manifest&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Android의 기본 manifestPlaceholders가 존재하기 때문에, 공식문서처럼 대입하는 것이 아닌 다음과 같이 값을 추가해주어야 한다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;manifestPlaceholders += [KAKAO_NATIVE_APP_KEY:localProperties.getProperty('api.key.kakao.nativeApp')]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  iOS 커스텀 URL 스킴&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;iOS 디바이스에서는 커스텀 URL 스킴을 사용하기 위해선 Xcode를 활용한 두 개의 설정이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  앱 실행 허용 목록&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  URL Schemes&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  구현&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://developers.kakao.com/docs/latest/ko/kakaologin/flutter#before-you-begin-type-implementation&quot;&gt;https://developers.kakao.com/docs/latest/ko/kakaologin/flutter#before-you-begin-type-implementation&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Flutter SDK에서는 다양한 서비스 환경에 대응할 수 있도록 여러가지 구현 방식을 제공하고 있으나, 현재 파트에서는 권장 사항인 네이티브 앱 서비스에서 카카오톡으로 로그인방식을 진행하는 경우를 다룹니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ Kakao 로그인을 사용하기 위한 최소 SDK 버전은 21입니다. android/app/build.gradle의 android&amp;rarr;defaultConfig&amp;rarr;minSdkVersion을 21로 설정하세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  사전 설정&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://developers.kakao.com/docs/latest/ko/kakaologin/flutter#login-with-kakaotalk&quot;&gt;https://developers.kakao.com/docs/latest/ko/kakaologin/flutter#login-with-kakaotalk&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;조건부 및 선택형 설정을 제외한 사전 필수 설정 항목은 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  플랫폼 등록&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://developers.kakao.com/docs/latest/ko/getting-started/app#platform&quot;&gt;https://developers.kakao.com/docs/latest/ko/getting-started/app#platform&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;내 애플리케이션&amp;gt; 앱 설정&amp;gt; 플랫폼 경로를 통해 Android/iOS 플랫폼 등록을 진행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;android/app/src/main/AndroidManifest.xml 파일의 package 속성 값과 일치한지 확인합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;또한 키 해시 값이 등록되어 있는지 확인합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://developers.kakao.com/docs/latest/ko/android/getting-started#before-you-begin-add-key-hash&quot;&gt;https://developers.kakao.com/docs/latest/ko/android/getting-started#before-you-begin-add-key-hash&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;키 해시(Key Hash)는 인증서(Certificate)의 인증서 지문 값(Certificate fingerprints)을 해시(hash)한 값으로, 악성 앱인지 판별하고 카카오 API를 호출하기 위해 사용됩니다. 키 해시 값은 변경되지 않으나, 여러 개발자들과 함께하는 경우 각 개발자들의 개발 환경에 따라 디버그 키스토어가 다르게 생성되어 있으므로 모두 등록해주어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;카카오 API를 호출하면, 카카오 API 서버가 요청 헤더에 추가된 키 해시값과 카카오 플랫폼에 등록한 값이 일치하는지 확인하며, 디버그(Debug)/릴리즈(Release) 키 해시 두 가지가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://code.google.com/archive/p/openssl-for-windows/downloads&quot;&gt;https://code.google.com/archive/p/openssl-for-windows/downloads&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;다음과 같이 터미널에서 키 해시를 생성하려면 키 해시를 관리하는 키툴(keytool)이 필요합니다. Windows의 경우, Windows 라이브러리를 위한 OpenSSL을 다운로드해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;OpenSSL을 사용할 때는, openssl.exe가 존재하는 폴더의 경로를 입력하여 실행될 수 있게 만들어줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Debug Key Hash&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;keytool -exportcert -alias androiddebugkey -keystore %USERPROFILE%\\.android\\debug.keystore -storepass android -keypass android 
&amp;gt; | ${path}\\openssl sha1 -binary | ${path}\\openssl base64
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Release Key Hash&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;hellip;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  카카오 로그인 활성화 설정&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://developers.kakao.com/docs/latest/ko/kakaologin/prerequisite#kakao-login-activate&quot;&gt;https://developers.kakao.com/docs/latest/ko/kakaologin/prerequisite#kakao-login-activate&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;내 애플리케이션&amp;gt; 제품 설정&amp;gt; 카카오 로그인 경로를 통해 카카오 로그인 활성화 설정을 ON으로 변경합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  동의항목&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://developers.kakao.com/docs/latest/ko/kakaologin/prerequisite#consent-item&quot;&gt;https://developers.kakao.com/docs/latest/ko/kakaologin/prerequisite#consent-item&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;내 애플리케이션&amp;gt; 제품 설정&amp;gt; 카카오 로그인&amp;gt; 동의항목에서 카카오 로그인 시 사용자에게 동의받고자 하는 항목을 설정할 수 있습니다. 카카오 로그인 동의 화면에 반영되며, 사용자는 서비스 이용시 필요한 동의항목에 동의할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;동의 항목을 추가하는 경우, 동의 목적을 반드시 작성해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size18&quot;&gt;  로그인 요청&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://developers.kakao.com/docs/latest/ko/kakaologin/flutter#kakaologin-sample&quot;&gt;https://developers.kakao.com/docs/latest/ko/kakaologin/flutter#kakaologin-sample&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;로그인 요청은 기존 로그인을 통해 발급받은 토큰이 있는지 확인하거나, isKakaoTalkInstalled()메소드로 카카오톡 실행 가능 여부를 판단하는 방법이 있습니다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;void KakaoLogin() async {
  // 카카오톡 실행 가능 여부 확인
  // 카카오톡 실행이 가능하면 카카오톡으로 로그인, 아니면 카카오계정으로 로그인
  bool result = await isKakaoTalkInstalled();
  if (await isKakaoTalkInstalled()) {
    print(&quot;카카오톡 있음&quot;);
    try {
      await UserApi.instance.loginWithKakaoTalk();
      print('카카오톡으로 로그인 성공');
    } catch (error) {
      print('카카오톡으로 로그인 실패 $error');

      // 사용자가 카카오톡 설치 후 디바이스 권한 요청 화면에서 로그인을 취소한 경우,
      // 의도적인 로그인 취소로 보고 카카오계정으로 로그인 시도 없이 로그인 취소로 처리 (예: 뒤로 가기)
      if (error is PlatformException &amp;amp;&amp;amp; error.code == 'CANCELED') {
        return;
      }
      // 카카오톡에 연결된 카카오계정이 없는 경우, 카카오계정으로 로그인
      try {
        await UserApi.instance.loginWithKakaoAccount();
        print('카카오계정으로 로그인 성공');
      } catch (error) {
        print('카카오계정으로 로그인 실패 $error');
      }
    }
  } else {
    print(&quot;카카오톡 없음&quot;);
    try {
      OAuthToken token = await UserApi.instance.loginWithKakaoAccount();
      print('카카오계정으로 로그인 성공');
    } catch (error) {
      print('카카오계정으로 로그인 실패 $error');
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Kakao 로그인을 에뮬레이터 활용하여 테스트할 때, API 12버전 이상인 경우 &quot;동의하고 계속하기&quot; 부분에서 버그(화면이 표기되지 않는 현상/화면 깜박임 현상)가 발생한다고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(&lt;a href=&quot;https://devtalk.kakao.com/t/topic/134052/2&quot;&gt;https://devtalk.kakao.com/t/topic/134052/2&lt;/a&gt;) 이 점 참고하여 에뮬레이터 설정을 변경하세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  토큰&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://developers.kakao.com/docs/latest/ko/kakaologin/flutter#request-token&quot;&gt;https://developers.kakao.com/docs/latest/ko/kakaologin/flutter#request-token&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;토큰은 액세스 토큰(Access token), 리프레시 토큰(Refresh token), ID 토큰(ID token) 세 종류입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;액세스 토큰을 활용하면 토큰 정보를 보거나 ID 토큰 유효성 검증을 진행할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;서비스 서버에서 토큰을 발급받은 후, 서비스 서버에서 액세스 토큰으로 사용자 정보 가져오기를 호출해 회원 가입 및 로그인에 필요한 사용자 정보를 요청할 수 있습니다. 액세스 토큰을 서비스 클라이언트로 전달해 토큰 할당하면, 서비스 클라이언트에서 Flutter SDK를 통한 카카오 API 요청에 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  토큰 확인&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://developers.kakao.com/docs/latest/ko/kakaologin/flutter#token-presence&quot;&gt;https://developers.kakao.com/docs/latest/ko/kakaologin/flutter#token-presence&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;AuthAPI의 hasToken()을 호출하여 Flutter SDK에 기존에 발급받아 저장된 토큰이 있는지 확인합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;저장된 토큰이 있는 경우, 앱을 실행하거나 카카오 로그인이 필요한 API를 호출하기 전 검증하여 다음 행위를 진행하는데 도움을 줄 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  토큰 정보 보기&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://developers.kakao.com/docs/latest/ko/kakaologin/flutter#get-token-info&quot;&gt;https://developers.kakao.com/docs/latest/ko/kakaologin/flutter#get-token-info&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;UserApi의 accessTokenInfo()를 호출하여 Flutter SDK에 저장중인 액세스 토큰의 정보를 조회합니다. 요청 성공 시, 앱ID/회원번호/남은 유효시간 정보를 담은 AccessTokenInfo 객체가 반환됩니다. 액세스 토큰이 만료되었거나 존재하지 않는 경우에는 에러를 반환하므로, 기존 액세스 토큰의 만료 여부를 확인하는 용도로도 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;if (await AuthApi.instance.hasToken()) {
	try {
	  AccessTokenInfo tokenInfo = await UserApi.instance.accessTokenInfo();
	  print('토큰 유효성 체크 성공 ${tokenInfo.id} ${tokenInfo.expiresIn}');
	} catch (error) {
	  if (error is KakaoException &amp;amp;&amp;amp; error.isInvalidTokenError()) {
	    print('토큰 만료 $error');
	  } else {
	    print('토큰 정보 조회 실패 $error');
	  }
	
	  try {
	    // 카카오계정으로 로그인 = 새로운 토큰 발급
	    OAuthToken token = await UserApi.instance.loginWithKakaoAccount();
	    print('로그인 성공 ${token.accessToken}');
	  } catch (error) {
	    print('로그인 실패 $error');
	  }
	}
} 
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;결과가 true인 이후에도 토큰이 유효한지 확인해야 합니다. 토큰이 유효하지 않다면 다시 카카오 로그인을 통해 새로운 토큰 발급을 진행해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  사용자 정보 가져오기&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://developers.kakao.com/docs/latest/ko/kakaologin/flutter#req-user-info&quot;&gt;https://developers.kakao.com/docs/latest/ko/kakaologin/flutter#req-user-info&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;로그인에 성공하고 토큰 유효성 검증이 끝났다면, UserApi의 me()를 호출하여 사용자의 정보를 불러옵니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;try {
  User user = await UserApi.instance.me();
  print('사용자 정보 요청 성공'
        '\\n회원번호: ${user.id}'
        '\\n닉네임: ${user.kakaoAccount?.profile?.nickname}'
        '\\n이메일: ${user.kakaoAccount?.email}');
} catch (error) {
  print('사용자 정보 요청 실패 $error');
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  로그아웃&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://developers.kakao.com/docs/latest/ko/kakaologin/flutter#logout&quot;&gt;https://developers.kakao.com/docs/latest/ko/kakaologin/flutter#logout&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;로그아웃은 성공 여부와 상관 없이 Flutter SDK에 저장된 사용자 토큰 정보를 삭제합니다. 토큰이 삭제되는 경우, 현재 토큰 값을 통해 카카오 API를 호출할 수 없습니다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;try {
  await UserApi.instance.logout();
  print('로그아웃 성공, SDK에서 토큰 삭제');
} catch (error) {
  print('로그아웃 실패, SDK에서 토큰 삭제 $error');
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  연결 끊기&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://developers.kakao.com/docs/latest/ko/kakaologin/flutter#unlink&quot;&gt;https://developers.kakao.com/docs/latest/ko/kakaologin/flutter#unlink&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;요청 성공 시, 앱과 연결이 해제된 사용자의 회원번호를 포함한 UserIdResponse 객체가 반환됩니다. 또한 로그아웃 처리가 함께 이뤄져 Flutter SDK에 저장된 토큰이 삭제됩니다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;try {
  UserIdResponse = await UserApi.instance.unlink();
  print('연결 끊기 성공, SDK에서 토큰 삭제');
} catch (error) {
  print('연결 끊기 실패 $error');
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;로그아웃과 연결 끊기는, Flutter SDK에 저장된 카카오 토큰 정보를 삭제하는 행위입니다.&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/257</guid>
      <comments>https://malangdidoo.tistory.com/257#entry257comment</comments>
      <pubDate>Sat, 30 Mar 2024 19:28:37 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] (Error) Error type 3: Activity class {com.x.y.MainActivity} does not exist.</title>
      <link>https://malangdidoo.tistory.com/256</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;Error type 3: Activity class {com.x.y.MainActivity} does not exist. OAuth중 하나인 Kakao Login을 구현하면서 만난 문제이다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;Launching lib\\main.dart on sdk gphone64 x86 64 in debug mode...
&amp;radic;  Built build\\app\\outputs\\flutter-apk\\app-debug.apk.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;애플리케이션을 실행하면, 위와 같은 상태로 Flutter Launching 상태가 무한히 지속되는데, 단순히 오래걸리는 것이라는 착각은 하지 않는게 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이에 대한 문제점을 찾기 위해선 아래 명령어를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;flutter run -v
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;※ 만약 VsCode를 사용하고 있다면 main.dart 또는 다양한 페이지 내에서 F5를 눌러 실행하는 것 보다, 위 명령어를 통해, 정확히 실행되는지 확인해보도록 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그렇다면 제목과 같은 Error type 3: Activity class {com.x.y.MainActivity} does not exist. 오류를 만날 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;해당 오류는 인터넷을 검색하면 다음과 같은 해결 방법을 만나게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  emulator wipe data&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  emulator에 존재하는 application 삭제&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이 방법은 Setting &amp;rarr; 앱 및 설정 &amp;rarr; 모든 앱 보기를 통해 설치되었거나 설치 중 멈춘 애플리케이션을 삭제할 때 활용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;직접 emulator에서 삭제하는 방법 이외에, android-sdk의 tools를 찾아가 abd uninstall {packageName}을 진행해도 된다 하는데, 본인의 경우 android-sdk의 경로를 찾지 못했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;하지만 나의 경우에는, 두 방법 모두 효과가 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그 이유는 잘못된 AndroidManifest.xml 작성에 존재했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;나는 Flutter에서 Kakao Login을 진행하고 나서 원래의 앱으로 돌아오기 위해 커스텀 URL 스킴을 설정하여, 앱으로 리다이렉트 될 수 있도록 설정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그리고 추후 사용될 카카오톡 공유 및 메세지를 활용하기 위한 스킴을 추가했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이 과정에서 나는 AndroidManifest.xml을 잘못 작성했다고 말할 수 있다. 내가 xml 파일의 내용을 수정하면서, 해당 오류를 해결했기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;나의 잘못은 다음과 같았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  카카오톡 공유 및 메세지에 관한 스킴 설정을 새로운 MAIN &amp;lt;intent-filter&amp;gt; tag에 추가한 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;.MainActivity를 name으로 가지는 activity는 기본적으로 다음과 같은&amp;lt;intent-filter&amp;gt; tag를 가지고 있다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;intent-filter&amp;gt;
		&amp;lt;action android:name=&quot;android.intent.action.MAIN&quot;/&amp;gt;
		&amp;lt;category android:name=&quot;android.intent.category.LAUNCHER&quot;/&amp;gt;
&amp;lt;/intent-filter&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그리고 카카오톡 공유 및 메세지에 관한 인텐트는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;intent-filter&amp;gt;
		&amp;lt;action android:name=&quot;android.intent.action.VIEW&quot; /&amp;gt;
		&amp;lt;category android:name=&quot;android.intent.category.DEFAULT&quot; /&amp;gt;
		&amp;lt;category android:name=&quot;android.intent.category.BROWSABLE&quot; /&amp;gt;
		
		&amp;lt;!-- &quot;kakao${YOUR_NATIVE_APP_KEY}://${PRODUCT_NAME}&quot; 형식의 앱 실행 스킴 설정 --&amp;gt;
		&amp;lt;!-- 카카오톡 공유, 카카오톡 메시지 --&amp;gt;
		&amp;lt;data android:host=&quot;kakaolink&quot;
		    android:scheme=&quot;kakao${KAKAO_NATIVE_APP_KEY}&quot; /&amp;gt;
&amp;lt;/intent-filter&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;하지만 나는 이것을 혼합해서 다음과 같이 사용했습니다.&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;intent-filter&amp;gt;
		&amp;lt;action android:name=&quot;android.intent.action.MAIN&quot;/&amp;gt;
		&amp;lt;category android:name=&quot;android.intent.category.LAUNCHER&quot;/&amp;gt;
		
		&amp;lt;!-- &quot;kakao${YOUR_NATIVE_APP_KEY}://${PRODUCT_NAME}&quot; 형식의 앱 실행 스킴 설정 --&amp;gt;
		&amp;lt;!-- 카카오톡 공유, 카카오톡 메시지 --&amp;gt;
		&amp;lt;data android:host=&quot;kakaolink&quot;
		    android:scheme=&quot;kakao${KAKAO_NATIVE_APP_KEY}&quot; /&amp;gt;
&amp;lt;/intent-filter&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그래서 내 AndroidManifest 내부에서는 파악할 수 없는 &amp;lt;data&amp;gt;태그로 인해, MainActivity가 생성되지 않아 애플리케이션을 실행할 때, 찾을 수 없다고 말하는 것이라고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;잘못 혼합해서 사용했던 &amp;lt;intent-filter&amp;gt;를 수정하고, 다시 앱을 시작했을 때 내 앱은 정상적으로 설치되고 실행됐습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이 현상에 대해 정확하게 파악하고 넘어가기 위해선 다음 4개의 레퍼런스를 참고하길 바랍니다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ &lt;a href=&quot;https://developer.android.com/guide/components/intents-filters&quot;&gt;https://developer.android.com/guide/components/intents-filters&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ &lt;a href=&quot;https://developer.android.com/reference/android/content/Intent.html&quot;&gt;https://developer.android.com/reference/android/content/Intent.html&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ &lt;a href=&quot;https://developer.android.com/guide/topics/manifest/action-element?hl=ko&quot;&gt;https://developer.android.com/guide/topics/manifest/action-element?hl=ko&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ &lt;a href=&quot;https://developer.android.com/guide/topics/manifest/category-element?hl=ko&quot;&gt;https://developer.android.com/guide/topics/manifest/category-element?hl=ko&lt;/a&gt;&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/256</guid>
      <comments>https://malangdidoo.tistory.com/256#entry256comment</comments>
      <pubDate>Sat, 30 Mar 2024 19:23:39 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] (Project) MapleSpy: 35. 출시 준비</title>
      <link>https://malangdidoo.tistory.com/255</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;프로덕션 액세스 관련 심사가 통과되었다. 이전에 작성한 신청 양식은, 앱을 출시하기 까지 개발자가 어떤 과정으로 어떻게 노력했는지 진심을 다해 작성했을 때, 그 노력을 인정해주는 것 같았다. 진심을 다해 작성해보도록 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  할 일&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;프로덕션 액세스 권한이 부여되면, 버전 생성 및 게시를 통해 애플리케이션 출시 과정을 밟게 된다. 해야 할 일은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  국가 및 지역 선택&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;애플리케이션을 제공하고자 하는 국가를 선택해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;여지없이 대한민국을 선택하고, 게시 개요로 이동하지 않은 채 다시 대시보드로 돌아오자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  새 버전 만들기&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;프로덕션 탭에서 출시를 위한 새 버전을 만들어준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;버전에 대한 정보를 입력하는 것은 비공개 테스트 트랙을 관리하면서 진행했던 과정과 동일합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;출시 버전의 새로운 App Bundle을 추가하고, 출시 명/출시 노트를 작성하여 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  버전 미리보기 및 확인&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  검토를 위해 Google에 버전 전송&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;비공개 테스트를 진행하면서 경험했던 모든 과정은, 애플리케이션을 출시하기 위해 진행해야 하는 작업과 동일했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이름만 비공개 테스트이지, 출시했을 때 사용자 피드백과 더불어 진행해야 할 유지보수 및 개선 과정과 동일하다고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;처음에는 신입 개발자 계정에 대한 약관과 제한이 변경되어 그 의미를 몰랐으나, 모든 것을 경험했을 때 Google Play Store 입장에서는 꼭 필요한 행위였다고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;새롭게 시작하시는 분들도 2주라는 비공개 테스트 기간을 헛되이 쓰지 않고, 테스터에게 오류 및 개선사항을 받아 앱을 발전해 나갈 수 있기를 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;프로덕션 앱 검토는 비공개 테스트에서 진행했던 검토와는 다소 다르게 진행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;더 세부적으로, 더 꼼꼼하게 진행되며, 신경쓰지 못한 위반사유를 지닌 채 개발하고 있었을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앱을 이용함에 부적합한 내용은 없는지, 기망하지는 않았는지 꼭 신경쓰고 진행하기를 바랍니다.&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/255</guid>
      <comments>https://malangdidoo.tistory.com/255#entry255comment</comments>
      <pubDate>Mon, 18 Mar 2024 19:36:14 +0900</pubDate>
    </item>
    <item>
      <title>[BAEKJOON] 16953</title>
      <link>https://malangdidoo.tistory.com/254</link>
      <description>&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;※ 소스코드는 각 문제 설명 하단에 &amp;lt;더보기&amp;gt;를 통해 확인하실 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;[sliver 2, 16953 A&amp;rarr;B] - 너비우선탐색(BFS)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10^9자리에서 10^10자리까지 확인할 수 있기 때문에, long long 자료형을 활용해야 하는 것이 핵심이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 string stl을 사용하면서 숫자를 변환하기 위해 stoi를 많이 활용하는데, 이 함수는 무언가의 약자임을 기억하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;약자임을 기억한다면, stoi가 string to int임을 생각할 수 있고 다양한 방면으로 연계하여&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;string을 long long으로 바꾸자 할 때, stoll 함수를 활용할 수 있음을 깨달을 수 있다.&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;pre id=&quot;code_1710722085293&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;string&amp;gt;
#include &amp;lt;queue&amp;gt;
#pragma warning(disable:4996)

using namespace std;

typedef struct data {
	long long n;
	int cnt;
}Data;

int main() {
	long long int st, ed;
	scanf(&quot;%lld %lld&quot;, &amp;amp;st, &amp;amp;ed);

	queue&amp;lt;Data&amp;gt; q;
	q.push({st, 0});

	while (!q.empty()) {
		Data out = q.front(); q.pop();
		if (out.n == ed) {
			printf(&quot;%lld&quot;, out.cnt + 1);
			return 0;
		}
		string next = to_string(out.n) + '1';
		long long next1 = stoll(next);
		long long next2 = out.n * 2;
		if (next1 &amp;lt;= ed) {
			q.push({ next1, out.cnt + 1 });
		}
		if (next2 &amp;lt;= ed) {
			q.push({ next2, out.cnt + 1 });
		}
	}
	printf(&quot;-1&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>문제풀이/BAEKJOON Online Judge</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/254</guid>
      <comments>https://malangdidoo.tistory.com/254#entry254comment</comments>
      <pubDate>Mon, 18 Mar 2024 09:35:03 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] (Project) MapleApp: 34. 프로덕션 액세스 신청</title>
      <link>https://malangdidoo.tistory.com/253</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;벌써 출시의 때가 다가왔다! 비공개 테스트 버전을 게시하고, 20명 이상의 테스터 참여자를 구해 2주동안 비공개 테스트를 거쳐 현재로 왔다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;비공개 테스트 버전을 게시하기까지, 신입 개발자의 입장에서는 모르는 것 투성이였으나, 실제 경험하고 나니 왜 그렇게 오랜 기간이 걸렸는지 납득하진 못했다. 일각에서는 담당자 케바케라고한다. 비공개 테스트를 거쳐 5번의 게시를 진행했던 내 경우도 비슷한 느낌이 든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2주 동안 테스터들의 피드백을 받으며! 앱을 개선해왔던 노력이 헛되지 않게 프로덕션 액세스 신청을 진행하는 방법도 기술하겠다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  프로덕션 신청&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;프로덕션 요구사항인 다음 3가지 요구조건이 만족되면, 앱 대시보드 에서 프로덕션 신청을 진행할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  비공개 테스트 버전 개시&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  20명 이상의 비공개 테스터가 참여를 선택&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  20명 이상의 비공개 테스터가 14일 이상 비공개 테스트를 진행&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;신청을 진행할 땐, 다음에 대한 설문에 답을 하면 된다. 간단하다. 판단은 담당자님들께서 해주시나보다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  비공개 테스트 정보&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  비공개 테스트에 참여할 사용자를 어떻게 모았는지 물어본다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  테스터를 얼마나 쉽게 모집했는지도 물어본다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  그들의 참여도에 대해서도 설명해 달라 하며!&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  테스터로 받은 의견을 요약해 달라 한다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  앱 정보&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  앱의 주요 대상에 대해서 묻는다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  그 앱이 사용자에게 어떤 가치를 제공하는지도 말해야한다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  자신이 생각하는 설치 횟수 예상 수치를 묻는다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  프로덕션 준비&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  비공개 테스트에서 알게 된 내용을 바탕으로 어떻게 앱을 변경했는지 경험에 대해 묻는다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  또한, 왜 앱이 프로덕션 용으로 준비되었는지 어떻게 판단했는가를 묻는다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;실질적으로 신청 양식에 큰 어려움은 없다. 하지만, 그 내용을 기반으로 앱을 출시할 수 있는지 검토하고 기준에 충족되는지 심사를 받아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;내가 작성했던 글을 적지 않는 이유는, 출시가 진행되지 않았기 때문에 어떤 모습으로 작성해야 하는지 가이드라인을 제공해주지 못함을 헤아려 주시길 바란다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;출시가 진행되었을 때, 비로소 가이드라인을 작성하며 새로운 칼럼으로 찾아오겠습니다.&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/253</guid>
      <comments>https://malangdidoo.tistory.com/253#entry253comment</comments>
      <pubDate>Thu, 14 Mar 2024 19:35:24 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] (Error) flutter build apk Error Module was compiled with an incompatible version of Kotlin. The binary version of its metadata is 1.9.0, expected version is 1.6.0.</title>
      <link>https://malangdidoo.tistory.com/252</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;엄청나게 많은 해결법들이 제시되었고, 그 사이에서는 프로젝트 내부 gradle 버전과 외부 버전의 호환이 되지 않아 발생하지만 apk 파일은 만들어지므로 굳이 신경쓰지 않아도 된다는 말도 존재했다.&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; apk를 빌드하지 않는 과정에서는 다음 해결 방법이 적용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;path: flutterProject/android/build.gradle&lt;/p&gt;
&lt;pre id=&quot;code_1710392145768&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;buildscript {
    ext.kotlin_version = '1.9.23'
    repositories {
        google()
        mavenCentral()
    }

    dependencies {
        classpath &quot;org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version&quot;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kotlin_version을 최신 버전으로 업데이트해준다.&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;build.gradle을 수정했다면, 내용이 적용될 수 있도록 항상 clean, get을 진행해주자.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1710392500757&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;flutter clean
// flutter pub cache clean

// flutter pub upgrade
flutter pub get&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;span&gt;  버전 호환 문제&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼에도 불구하고 발생하는 에러는 Kotlin 버전에 맞게 JDK 버전을 수정해주니 해결되었다. 대부분 21 또는 17버전을 사용하면서 문제가 해결된 듯 하다.&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;※ jdk download:&amp;nbsp;&lt;a href=&quot;https://www.oracle.com/java/technologies/downloads/#jdk17-windows&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.oracle.com/java/technologies/downloads/#jdk17-windows&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;보통 사용자 이름 폴더 내부에 있는 Program Files/Java에 설치되고, bin 폴더 경로를 시스템 환경 변수에 등록해 준 뒤 java -version 명령어를 통해 적용됐는지 확인하자. 미적용이라면 prompt/IDE를 재시작하거나 컴퓨터를 재부팅해보자.&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>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/252</guid>
      <comments>https://malangdidoo.tistory.com/252#entry252comment</comments>
      <pubDate>Thu, 14 Mar 2024 14:06:09 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] Firebase Remote Config에서 Json데이터 활용하기</title>
      <link>https://malangdidoo.tistory.com/251</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;Remote Config는 기본적으로 bool, double, int, string 형식의 변수 만을 사용할 수 있다. 하지만, 많은 양의 데이터를 사용하기 위해서 우리는 json 형식의 변수를 자주 사용하기 때문에 Remote Config에서 json 데이터를 다루는 방법을 기술하려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;필자는 Remote Config를 사용하여 긴급 공지사항을 작성하기 위해 시도를 하고 있다. 다음 단계를 따라가며 해야 하는 일과 주의 해야 할 사항에 대해 파악하기를 바란다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  RemoteConfig 연결&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;코드 상에서 연결하는 부분은 다음과 같다. Firebase를 Flutter에 연동하고, RemoteConfig를 사용하기 위한 준비 과정을 진행하지 않았다면, [&amp;nbsp;&lt;a href=&quot;https://malangdidoo.tistory.com/247&quot;&gt;Firebase Remote Config&lt;/a&gt;&amp;nbsp;]를 참고하여 진행해 주길 바란다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;final remoteConfig = FirebaseRemoteConfig.instance;
remoteConfig.setConfigSettings(RemoteConfigSettings(
    fetchTimeout: Duration(milliseconds: 3000),
    minimumFetchInterval: Duration(milliseconds: 1)));
    
remoteConfig.setDefaults({
  'notice': Notice().toJson().toString(),
});

await remoteConfig.fetchAndActivate();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;우리가 RemoteConfig에서 데이터를 갱신했을 때, 가끔 데이터가 바로 적용되지 않는 모습을 확인할 수 있다. 그 이유는 당신이 minimumFetchInterval을 너무 높게 잡았기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;minimumFetchInterval에 설정해 놓은 시간을 기반으로 RemoteConfig에서 변경된 게시에 대해 fetch, 가져오기 때문에 테스트를 진행하는 과정 중엔 최소 시간으로 설정해 주길 바란다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;(RemoteConfig 테스트 도구를 활용하는 것이 가장 좋은 방법이나, 필자는 기대 비용이 너무 높다고 생각해서 진행하지 않았다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;fetch 이후엔 꼭 Activate를 진행해 주어야 RemoteConfig 값을 활용할 수 있으므로 유념하자. 자 이제 이를 기반으로 Entity를 만들고, Json 데이터 생성 과정과 가공을 진행해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  공지사항 Entity/Model 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;나는 제목, 내용, 날짜, 붙임말로 구성된 Notice Class를 활용하여 Json 데이터를 가공하고 활용할 것이다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class Notice {
  String? title;
  String? body;
  String? date;
  String? footer;

  Notice({this.title, this.body, this.date, this.footer});

  Notice.fromJson(Map&amp;lt;String, dynamic&amp;gt; json) {
    title = json['title'];
    body = json['body'];
    date = json['date'];
    footer = json['footer'];
  }

  Map&amp;lt;String, dynamic&amp;gt; toJson() {
    final Map&amp;lt;String, dynamic&amp;gt; data = new Map&amp;lt;String, dynamic&amp;gt;();
    data['date'] = this.date;
    data['title'] = this.title;
    data['footer'] = this.footer;
    data['body'] = this.body;
    return data;
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Remote Config에 Json 데이터 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Remote Config에서 변수를 생성하면, 편하게 문자열 작성을 도와주는 편집기를 제공하고 있다. 여기서 기본적인 Json 데이터를 생성해준다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
	&quot;title&quot;: &quot;제목&quot;,
	&quot;body&quot;: &quot;내용&quot;,
	&quot;date&quot;: &quot;날짜&quot;,
	&quot;footer&quot;: &quot;붙임말&quot;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;자 대부분의 경우 Json 데이터를 직접 작성하는 경우가 드물기 때문에, 작성하라면 위와 같이 작성할 것이다. 하지만 여기엔 결정적인 결함이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;바로 데이터 마지막 요소의 ( , )이다. 이는 syntax error를 일으키기 가장 쉬운 오류이므로 꼭 신경 써서 제거해 주길 바란다. 다시 작성된 Json 데이터는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
	&quot;title&quot;: &quot;제목&quot;,
	&quot;body&quot;: &quot;내용&quot;,
	&quot;date&quot;: &quot;날짜&quot;,
	&quot;footer&quot;: &quot;붙임말&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이 상태로 RemoteConfig에서 getString 메소드를 활용해 데이터를 가져오면 똑같이 출력 된다. 그 모습에 얼핏 정확하게 데이터를 가져오지 않았나 생각할 수 있지만, jsonEncode를 활용하여 String으로 변환하면 다음과 같은 모습이 된다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;String remoteConfigData = remoteConfig.getString('notice'); // notice = 변수 이름
var remoteConfigJsonData = jsonEncode(remoteConfigData);

// remoteConfigData result
&quot;&quot;{&quot;\\n &quot;title&quot;: &quot;제목&quot;,\\n &quot;body&quot;: &quot;내용&quot;,\\n	&quot;date&quot;: &quot;날짜&quot;,\\n	&quot;footer&quot;: &quot;붙임말&quot;\\n&quot;}&quot;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;정확한 작성은 아니나 특수문자, 뛰어쓰기, 줄바꿈들이 문자로 치환되어 문자열에 병합되는 모습을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;왜 굳이 jsonEncode를 진행하느냐, String이니까 바로 jsonDecode를 진행하면 되지 않느냐, 라고 생각하실 수 있다. 하지만, 우리가 기존에 작성했던 Json 데이터에 &quot;줄바꿈&quot;이 포함되어 있고, 그대로 jsonDecode를 진행한다면 다음과 같은 오류를 만날 수 있다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;FormatException (FormatException: Unexpected character (at line 6, character 1) } ^ )
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이런 오류가 발생하기 때문에 우리는 Json 데이터를 편하게 작성한 뒤, 꼭 다음처럼 정제해주어야 한다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{&quot;title&quot;:&quot;제목&quot;,&quot;body&quot;:&quot;내용&quot;,&quot;date&quot;:&quot;날짜&quot;,&quot;footer&quot;:&quot;붙임말&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;보통 API 서버에서 Json 데이터를 받은 뒤 출력하면 위와 같은 모습임을 느낄 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;자 그럼 jsonDecode를 활용하여 Map&amp;lt;String, dynamic&amp;gt; 형식의 json 데이터를 만들어주고, 이를 Notice Class로 변환하여 사용해보자.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;String remoteEmergencyNoticeData = remoteConfig.getString('emergency_notice');

Map&amp;lt;String, dynamic&amp;gt; remoteEmergencyNoticeJsonData = jsonDecode(remoteEmergencyNoticeData);

Notice notice = Notice.fromJson(remoteEmergencyNoticeJsonData);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;여기까지 Flutter에서 RemoteConfig의 Json데이터를 활용하는 방법이다. 중간 중간에 풀어놓은 사소한 과정에 대해 잊지 않기를 바란다.&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/251</guid>
      <comments>https://malangdidoo.tistory.com/251#entry251comment</comments>
      <pubDate>Thu, 14 Mar 2024 10:45:30 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] 기기 데이터 캐싱</title>
      <link>https://malangdidoo.tistory.com/250</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;애플리케이션 사용자의 UX 향상을 위해 사용자가 검색했던 닉네임을 캐시하여 검색 전 보여주려한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;API 이용 건수가 상당하지만, 일일 제한 횟수가 초과하게 된다면 애플리케이션 캐싱으로 관리해 볼 예정이고, 캐싱 데이터가 많아져 애플리케이션이 무거워진다면 backend 서버의 도움을 받고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;일단은 간단하게 닉네임 만을 대상으로 하고있으므로 관련 방법을 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  shared_preferences&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://pub.dev/packages/shared_preferences&quot;&gt;https://pub.dev/packages/shared_preferences&lt;/a&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;$ flutter pub add shared_preferences
$ flutter pub get
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;shared_preferences는 flutter.dev에서 관리하는 플러그인이다. 플랫폼별 영구 저장소를 래핑하며, 데이터는 비동기적으로 디스크에 유지된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;모든 캐시 데이터가 그렇듯, 중요한 데이터를 저장하는 것은 지양한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  객체 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;SharedPreferences의 인스턴스를 가져온다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// Obtain shared preferences.
final SharedPreferences prefs = await SharedPreferences.getInstance();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  데이터 쓰기&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;setInt/setBool/setDouble/setString/setStringList 메소드를 지원한다.&lt;/p&gt;
&lt;pre class=&quot;xquery&quot;&gt;&lt;code&gt;// Save an integer value to 'counter' key.
await prefs.setInt('counter', 10);
// Save an boolean value to 'repeat' key.
await prefs.setBool('repeat', true);
// Save an double value to 'decimal' key.
await prefs.setDouble('decimal', 1.5);
// Save an String value to 'action' key.
await prefs.setString('action', 'Start');
// Save an list of strings to 'items' key.
await prefs.setStringList('items', &amp;lt;String&amp;gt;['Earth', 'Moon', 'Sun']);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  데이터 읽기&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;// Try reading data from the 'counter' key. If it doesn't exist, returns null.
final int? counter = prefs.getInt('counter');
// Try reading data from the 'repeat' key. If it doesn't exist, returns null.
final bool? repeat = prefs.getBool('repeat');
// Try reading data from the 'decimal' key. If it doesn't exist, returns null.
final double? decimal = prefs.getDouble('decimal');
// Try reading data from the 'action' key. If it doesn't exist, returns null.
final String? action = prefs.getString('action');
// Try reading data from the 'items' key. If it doesn't exist, returns null.
final List&amp;lt;String&amp;gt;? items = prefs.getStringList('items');
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  항목 제거&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;// Remove data for the 'counter' key.
await prefs.remove('counter');
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;레퍼런스의 가이드라인을 활용하여 닉네임 리스트를 저장하고, 내가 원하는 화면에서 사용하면 된다.&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/250</guid>
      <comments>https://malangdidoo.tistory.com/250#entry250comment</comments>
      <pubDate>Wed, 13 Mar 2024 11:23:43 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] Firebase Cloud Firestore</title>
      <link>https://malangdidoo.tistory.com/249</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;기본적으로 Firebase와의 연결과, Firebase.initializeApp을 통해 초기화를 진행해 주어야 한다. 관련 내용은 이전에 작성한 [ &lt;a href=&quot;https://malangdidoo.tistory.com/247&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Firebase Remote Config&lt;/a&gt; ] 글의 &lt;b&gt;&quot;  Flutter에 Firebase 추가&quot;&lt;/b&gt; 단락을 참고하여 진행해 주길 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://firebase.google.com/docs/firestore/quickstart?hl=ko&amp;amp;_gl=1*30ps05*_up*MQ&quot;&gt;https://firebase.google.com/docs/firestore/quickstart?hl=ko&amp;amp;_gl=1*30ps05*_up*MQ&lt;/a&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  개발 환경 설정&lt;/h4&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;flutter pub add cloud_firestore
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  앱 체크&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://firebase.google.com/docs/app-check?hl=ko&amp;amp;_gl=1*1w5bp70*_up*MQ&quot;&gt;https://firebase.google.com/docs/app-check?hl=ko&amp;amp;_gl=1*1w5bp70*_up*MQ&lt;/a&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  Cloud Firebase 초기화&lt;/h4&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;db = FirebaseFireStore.instance;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;아래 내용을 통해 콜렉션 내부 문서의 id값, value값을 가져올 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;// collection 연결
final query = db.collection(&quot;collectionName&quot;).where(~);

// query 진행
final querySnapshot = await quiery.get();
final docSnapshot = querySnapshot.docs.first

// result 반환
final docId = docSnapshot.id;
final docData = docSnapshot.data();&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;size18&quot;&gt;보통 결과 값으로 주어지는 DocumentReference 또는 QuerySnapShot에 포함된 doc을 통해 doc의 id와 data() 정보를 가져올 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;특정 결과의 경우 (단일 항목만 매칭될 수 있도록 Collection 구조를 설계한 경우) 단일 값을 반환하여 위와 같이 사용할 수 있으며, 대부분의 경우 List로 반환되기 때문에 for문을 활용하여 결과 docs에 대한 정보를 얻을 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  데이터 추가&lt;/h4&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;final user = &amp;lt;String, dynamic&amp;gt;{
	'first': 'Ada',
	'last': 'Lovelace',
	'born': 1915,
}

db.collection('users').add(user).then((DocumentReference doc) =&amp;gt;
	print('DocumentSnapshot added with ID: ${doc.id}));
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;데이터를 추가할 때는 정형적인 키-값 쌍을 가진 클래스를 사용하는 것을 권장합니다. 키-값 쌍을 자유롭게 사용한다면, 특정 컬렉션 내 문서에 공통되지 않은 키-값 쌍이 생길 수 있음에 유의해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;add() 메소드 이외에 set() 메소드를 활용하여 추가하는 방법도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;set() 메소드는 문서가 없으면 생성되며, 문서가 존재한다면 새로 제공한 데이터로 오버라이팅 됩니다. 단, 기존 문서와 병합될 수 있도록 옵션을 지정하는 경우엔 기존 문서에 합쳐집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;또한, 데이터를 추가할 때 FireStore는 숫자를 항상 배정밀도 부동 소수점으로 저장하므로 사용에 유의해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  커스텀 객체&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Map 또는&amp;nbsp;Dictionary객체로는 문서를 표현하기가 불편한 경우가 많으므로 Cloud Firestore는 커스텀 클래스를 사용하여 문서를 작성하는 방식도 지원합니다. Cloud Firestore에서 객체를 지원되는 데이터 유형으로 변환합니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class City {
  final String? name;
  final String? state;
  final String? country;
  final bool? capital;
  final int? population;
  final List&amp;lt;String&amp;gt;? regions;

  City({
    this.name,
    this.state,
    this.country,
    this.capital,
    this.population,
    this.regions,
  });

  factory City.fromFirestore(
    DocumentSnapshot&amp;lt;Map&amp;lt;String, dynamic&amp;gt;&amp;gt; snapshot,
    SnapshotOptions? options,
  ) {
    final data = snapshot.data();
    return City(
      name: data?['name'],
      state: data?['state'],
      country: data?['country'],
      capital: data?['capital'],
      population: data?['population'],
      regions:
          data?['regions'] is Iterable ? List.from(data?['regions']) : null,
    );
  }

  Map&amp;lt;String, dynamic&amp;gt; toFirestore() {
    return {
      if (name != null) &quot;name&quot;: name,
      if (state != null) &quot;state&quot;: state,
      if (country != null) &quot;country&quot;: country,
      if (capital != null) &quot;capital&quot;: capital,
      if (population != null) &quot;population&quot;: population,
      if (regions != null) &quot;regions&quot;: regions,
    };
  }
}

// useage
final city = City(
  name: &quot;Los Angeles&quot;,
  state: &quot;CA&quot;,
  country: &quot;USA&quot;,
  capital: false,
  population: 5000000,
  regions: [&quot;west_coast&quot;, &quot;socal&quot;],
);
final docRef = db
    .collection(&quot;cities&quot;)
    .withConverter(
      fromFirestore: City.fromFirestore,
      toFirestore: (City city, options) =&amp;gt; city.toFirestore(),
    )
    .doc(&quot;LA&quot;);
await docRef.set(city);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  데이터 읽기&lt;/h4&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;await db.collection('user').get().then((event) {
	for (var doc in event.docs) {
		print('${doc.id} -&amp;gt; ${doc.data()}');
	}
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  데이터 업데이트&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;하위 사용된 doc() 메소드의 경우 내부 요소로 String type의 docId를 필요로 합니다&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;final washingtonRef = db.collection(&quot;cites&quot;).doc(&quot;DC&quot;);
washingtonRef.update({&quot;capital&quot;: true}).then(
    (value) =&amp;gt; print(&quot;DocumentSnapshot successfully updated!&quot;),
    onError: (e) =&amp;gt; print(&quot;Error updating document $e&quot;));
    
db.collection(&quot;users&quot;)
	.doc(&quot;frank&quot;)
	.update({&quot;age&quot;: 13, &quot;favorites.color&quot;: &quot;Red&quot;});
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  데이터 쿼리&lt;/h4&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;final citiesRef = db.collection(&quot;cities&quot;);

final query = citiesRef.where(&quot;state&quot;, isEqualTo: &quot;CA&quot;);
query.get().then(
	(querySnapshot) {
		print(&quot;Success&quot;);
		for (var docSnapshot in querySnapshot.docs) {
			print('${docSnapshot.id} =&amp;gt; ${docSnapshot.data()}');
		}
	}
	onError: (e) =&amp;gt; print(e);
)

final capitalcities = db.collection(&quot;cities&quot;).where(&quot;capital&quot;, isEqualTo: true);

&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  오류&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Local module descriptor class for com.google.android.gms.providerinstaller.dynamite not found.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  1&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;AndroidManifest.xml에 다음 uses-permission tag 추가&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;&amp;lt;uses-permission android:name=&quot;android.permission.ACCESS_NETWORK_STATE&quot; /&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  2&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;flutter clean
flutter pub get
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  3&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;android emulator의 추가 기능 중, 데이터를 삭제해주는 Wipe Data 실행&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  4&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;emulator의 google play services version 상태 확인&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;정보가 없거나 Loading 중이라면, Update를 진행하여 존재 여부 확인&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;필자의 경우 Google play services 사용자 추가 및 설치가 완료되었을 때 Firestore 관련 오류가 해결했습니다.&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/249</guid>
      <comments>https://malangdidoo.tistory.com/249#entry249comment</comments>
      <pubDate>Tue, 12 Mar 2024 19:54:12 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] 애플리케이션 강제 업데이트</title>
      <link>https://malangdidoo.tistory.com/248</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;사용자들에게 비공개 테스트를 진행하면서, 새로운 버전을 업데이트하여 제공해야 하는 경우가 발생합니다. 하지만 사용자들은 앱을 설치하고 나면, 애플리케이션 페이지로 다시 접속할 일이 없죠. 리뷰를 작성하지 않는다면요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;특정 앱 버전에서 문제가 발생하는 경우, 또는 앱의 버그를 고치는 경우가 새로운 버전이 나오는 시기이기 때문에 사용자들에게 앱이 업데이트가 되었음을 명시적으로 알리고, 업데이트를 진행할 수 있도록 안내해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그래서 이번 포스트는 강제 업데이트를 적용시키기 위해 어떤 과정을 거쳐야 하는지, 필요한 것은 무엇인지 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  구현&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;강제 업데이트를 구현해 내기 위한 최적의 UI는 애플리케이션 로딩 화면이라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;애플리케이션에서 필요한 정보를 가져오는 등, 내부 비즈니스 로직이 이루어지기 이전 대기 시간이 필요한 경우가 있다. 이런 경우, 로딩 화면을 활용하여 그 시간을 벌어 놀 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;또한, 로딩 화면은 이 애플리케이션이 구동 중임을 명확하게 표기해주므로 UX향상에 도움이 된다고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;사용자가 애플리케이션을 활용하기 전에, 버전이 최신인지 확인하고 PlayStore에서 업데이트를 진행할 수 있도록 안내 하는 설계로 로딩 화면을 구현해 가보겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;버전에 대한 관리는 Firebase의 Remote Config를 활용하는데, Flutter 프로젝트에 이와 관련된 설치부터 사용법까지는 &lt;a href=&quot;https://malangdidoo.tistory.com/247를&quot;&gt;https://malangdidoo.tistory.com/247를&lt;/a&gt; 통해 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;기본적으로 로딩 화면은 개발자가 설정한 임계값 시간만큼 대기한 뒤 동작이 이루어질 수 있도록 설계된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그 과정에서 사용자는 뒤로 가기를 통해 애플리케이션을 종료할 수 없어야 한다는 것이 특징이기도 하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  기본 설계&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;기본적으로 화면이 구성될 때 RemoteConfig에 대한 값과 현재 버전에 대한 값을 비교하고 다음 페이지로 진행될 지, 새로운 버전을 다운로드 받을 수 있게 Redirect시킬 지 정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Timer를 활용해 로딩 화면을 보여주는 최소 시간을 정해놓고 위 로직을 수행한다고 생각하면 편하다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;@override
void initState() {
  super.initState();

  checkApplicationVersion();
}

void checkApplicationVersion() async {
  ...
  Timer(Duration(milliseconds: 1500), () {
    if (condition) {
      // navigatior redirect
      context.go('/');
    } else {
      // google store redirect
    }
  });
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;주의할 점은, initState에 async/await 키워드를 사용하면 안되므로, 외부 함수로 구현하여 호출해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  버전 정보 가져오기&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;먼저 Firebase의 Remote Config에서 최신 버전에 대한 정보를 가져온다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;final remoteConfig = FirebaseRemoteConfig.instance;
remoteConfig.setConfigSettings(RemoteConfigSettings(
  fetchTimeout: Duration(milliseconds: 1500),
  minimumFetchInterval: Duration(hours: 24),
));
remoteConfig.setDefaults({
  'latest_version': '0.0.0',
});
await remoteConfig.fetchAndActivate();

var remoteLatestVersion = remoteConfig.getString('latest_version');
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;RemoteConfigSettings Class의 경우 fetchTimeout과 minimumFetchInterval에 대한 값을 필수로 요구한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;fetchTimeout은 remoteConfig값을 불러오는 대기시간을 의미하고, 이 시간이 넘어가면 setDefaults를 통해 설정한 값을 사용하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;현재 애플리케이션에서 버전 정보를 가져오는 라이브러리는 다양하다. 그 중, 가장 가시적인 방법으로 정보를 가져오는 yaml을 사용해보려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://pub.dev/packages/yaml&quot;&gt;https://pub.dev/packages/yaml&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;yaml 패키지는 yaml에서 넘어온 값을 loadYaml() 메소드로 파싱하여 jsonDecode처럼 값을 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;var doc = loadYaml(&quot;YAML: YAML Ain't Markup Language&quot;);
print(doc['YAML']);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그래서 pubspect.yaml의 assets에 pubspec.yaml을 추가해서, rootBundle을 활용해 현재 버전에 대한 정보를 가져오면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;단, 현재 버전에 대한 정보는 0.0.0+0으로 표기되기 때문에 데이터 정제가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이 방법은 플랫폼에 구애 받지 않고 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;var originVersion = loadYaml(doc)['version'].toString().split('+')[0];
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  뒤로가기 버튼 막기&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;기존에 제공되었던 WillPopScope는 Android가 발전함에 따라 declared되었고, 현재 PopScope로 남아있다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://api.flutter.dev/flutter/widgets/PopScope-class.html&quot;&gt;https://api.flutter.dev/flutter/widgets/PopScope-class.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;canPop 속성을 활용하여 간단하게 막을 수 있으며, onPopInvoked를 통해 뒤로 가기가 호출되었을 때 사용자 지정 함수를 호출시킬 수 있다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@override
Widget build(BuildContext context) {
  return PopScope(
    canPop: false,
    child: SafeArea(
      child: Scaffold(
        body: Center(
          child: (Image.asset('assets/maplespy_icon.png')),
        ),
      ),
    ),
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  store_redirect&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://pub.dev/packages/store_redirect&quot;&gt;https://pub.dev/packages/store_redirect&lt;/a&gt;&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;flutter pub add store_redirect
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;store_redirect는 Google/Apple의 Store내 App 페이지로 Redirect 기능을 지원하는 Flutter 플러그인입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;사용 방법은 간단합니다! redirect 메소드를 활용해 androidAppId에 패키지 이름을 작성해주면 됩니다!&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;StoreRedirect.redirect(androidAppId: 'com.example.exam');
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;사용자에게는 앱 업데이트에 대한 안내문을 모달로 제공하고, 사용자의 선택에 따라 앱이 종료되거나 스토어로 이동할 수 있게 버튼을 추가해 주는 것이 향상된 UX를 제공할 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;하지만 store_redirect의 경우,&lt;/p&gt;
&lt;pre class=&quot;delphi&quot;&gt;&lt;code&gt;storeredirect\\StoreRedirectPlugin.java uses or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;와 같은 오류로 인해 다른 방법을 찾아야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  url_launcher&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://pub.dev/packages/url_launcher&quot;&gt;https://pub.dev/packages/url_launcher&lt;/a&gt;&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;flutter pub add url_launcher
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;사용 방법은 간단합니다!&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;if (Platform.isAndroid || Platform.isIOS) {
  final appId = Platform.isAndroid
      ? dotenv.get('ANDROID_PACKAGE_NAME')
      : dotenv.get('IOS_APP_ID');
  final url = Uri.parse(
    Platform.isAndroid
        ? 'market://details?id=${appId}'
        : '&amp;lt;https://apps.apple.com/app/id${appId}&amp;gt;',
  );
  launchUrl(
    url,
    mode: LaunchMode.externalApplication,
  );
}
&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;단, url_launcher 6.2.5 version의 경우, Android compilerVersion 34를 요구하므로, FlutterSDK 3.19.0 version을 사용해야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/248</guid>
      <comments>https://malangdidoo.tistory.com/248#entry248comment</comments>
      <pubDate>Tue, 5 Mar 2024 00:12:31 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] Firebase Remote Config</title>
      <link>https://malangdidoo.tistory.com/247</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;Firebase에서 제공하는 Remote Config는 사용자가 앱 업데이트를 다운할 필요 없이 앱의 동작과 모양을 변경할 수 있는 클라우드 서비스입니다. Firebase Console과 Remote Config backend API를 활용하여 모든 앱 사용자 또는 특정 사용자들에게 인앱 기본값을 재정의할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;업데이트를 적용할 시점에서 앱을 제어할 수 있으며, 성능에 거의 영향을 주지 않고 업데이트를 자주 확인하여 적용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Remote Config에 저장된 key-value로 구성된 매개변수에 저장해놓은 버전 정보와 packageManager를 활용해 현재 애플리케이션의 버전을 비교하여 앱을 Store로 Redirect 해주는 방식이라 생각하면 편합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;애플리케이션을 업데이트 하지 않아도, Remote Config에서 변경되는 최신 업데이트 버전에 대한 정보를 가져올 수 있으므로 현재 사용 중인 애플리케이션이 구 버전인지 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Remote Config는 어떻게 설정하는지, Flutter에서 Firebase는 어떻게 활용하는지 작성해 가보도록 하겠습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  Firebase Remote Config&lt;/h4&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://console.firebase.google.com/u/0/?hl=ko&quot;&gt;https://console.firebase.google.com/u/0/?hl=ko&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Firebase에 나의 프로젝트를 추가하고, 제품 카테고리 중 &quot;출시 및 모니터링&quot; 항목에서 Remtoe Config를 선택합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이후, &quot;구성 만들기&quot; 버튼을 선택하고 &quot;첫 번째 매개변수 만들기&quot;를 진행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;우리는 버전 정보를 관리할 것이므로 매개변수의 이름은 최신 업데이트 버전을 의미할 수 있도록 작성해줍니다. (latest_version)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이후 해당 매개변수에 대한 설명(description)과 값(현재 애플리케이션의 최신 버전, 0.0.0 etc&amp;hellip;)을 작성하고 저장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그러면 해당 key-value를 지닌 매개변수에 대해 &quot;변경사항 게시&quot;를 진행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Flutter에 Firebase 추가&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://firebase.google.com/docs/flutter/setup?hl=ko&amp;amp;platform=ios&quot;&gt;https://firebase.google.com/docs/flutter/setup?hl=ko&amp;amp;platform=ios&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Flutter 프레임워크에 Firebase를 연동하는 기본 작업을 진행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  필수 CLI 도구 설치 - Firebase CLI&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;npm을 활용해 Firebase CLI를 설치합니다.&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;npm install -g firebase-tools
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;또는 설치용 바이너리(&lt;a href=&quot;https://firebase.tools/bin/win/instant/latest&quot;&gt;https://firebase.tools/bin/win/instant/latest&lt;/a&gt;)를 통해 설치합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;설치 후 안내 되는 항목 &quot;Allow Firebase to collect CLI and Emulator Suite usage and error reporting information?&amp;rdquo;에 대해 Y/n을 선택합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;npm을 활용하여 설치한 경우 다음 명령어를 입력하고,&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;// VSCode의 경우 cmd 사용해야함
firebase login
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;CLI를 설치한 경우, 이후 안내 되는 URL을 통해 Firebase CLI에 Google 계정을 로그인합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  필수 CLI 도구 설치 - FlutterFire CLI&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Firebase를 사용할 수 있도록 앱을 구성할 때 도움을 주는 flutterfire_cli를 설치합니다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;dart pub global activate flutterfire_cli
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;결과로 특정 경로를 시스템 환경 변수에 등록하라는 문구가 나온다&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;Warning: Pub installs executables into C:\\Users\\{UserName}\\AppData\\Local\\Pub\\Cache\\bin, which is not on your path.
You can fix that by adding that directory to your system's &quot;Path&quot; environment variable.
A web search for &quot;configure windows path&quot; will show you how.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;내 컴퓨터&amp;gt;속성&amp;gt;고급 시스템 설정&amp;gt;환경변수 경로를 통해 사용자 변수에 이름은 FLUTTERFIRE_HOME, 값은 C:\\Users\\{UserName}\\AppData\\Local\\Pub\\Cache\\bin를 작성하여 추가해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이후 사용자 변수의 Path에 %FLUTTERFIRE_HOME%를 추가해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;환경 변수가 적용될 수 있도록, 사용 중인 터미널을 재시작하고(IDE 포함) flutterfire_cli를 재활성화 시켜준다&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;dart pub global deactivate flutterfire_cli
dart pub global activate flutterfire_cli
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;환경 변수를 등록하라는 문구가 뜨지 않으면 다음 단계를 진행할 수 있다.(그럼에도 불구하고 문구가 보인다면 재부팅을 권장한다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  앱 구성&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Flutter 프로젝트가 FlutterFire CLI를 사용하여 Firebase에 연결할 수 있도록 다음 명령어를 실행합니다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;flutterfire configure
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이후 프로젝트를 선택하고(Select a Firebase project to configure your Flutter application with ~), 지원하는 플랫폼을 선택합니다(Which platforms should your configuration support (use arrow keys &amp;amp; space to select)? ~).&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;web       ~
android   ~
ios       ~&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이후 제공되는 플랫폼 별 Firebase App ID를 기록합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  앱 Firebase 초기화&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;먼저 firebase_core 플러그인을 설치합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;flutter pub add firebase_core
flutter pub get
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이후 Firebase 구성이 최신 상태인지 확인합니다. 단, 이전 단계에서 진행했다면 넘어갑니다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;flutterfire configure
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;lib/main.dart파일에서 Firebase core 플러그인과 firebase_options.dart를 import하고, DefaultFirebaseOptions 객체를 활용하여 Firebase를 초기화합니다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_options.dart';

void main() async {
	...

  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  runApp(const ProviderScope(child: MaplespyApp()));
}
&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;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Flutter에서 Firebase Remote Config 사용하기&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://firebase.flutter.dev/docs/remote-config/get-started&quot;&gt;https://firebase.flutter.dev/docs/remote-config/get-started&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Flutter에서 Firebase의 Remote Config를 사용할 수 있도록 firebase_remote_config 라이브러리를 설치한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;flutter pub add firebase_remote_config
flutter pub add firebase_analytics
flutter pub get
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;firebase_remote_config는 원격 구성 싱글톤 객체를 가져올 수 있고, 자주 갱신될 수 있도록 최소 갱신 시간을 설정합니다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;final remoteConfig = FirebaseRemoteConfig.instance;
await remoteConfig.setConfigSettings(RemoteConfigSettings(
    fetchTimeout: const Duration(minutes: 1),
    minimumFetchInterval: const Duration(hours: 1),
));
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;인앱 기본 매개변수 값을 설정하면, 앱이 원격 구성 백엔드에 연결되기 전 의도한 대로 작동하고 백엔드에 설정된 값이 없는 경우에 기본값을 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;await remoteConfig.setDefaults(const {
    &quot;example_param_1&quot;: 42,
    &quot;example_param_2&quot;: 3.14159,
    &quot;example_param_3&quot;: true,
    &quot;example_param_4&quot;: &quot;Hello, world!&quot;,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앱에서 사용할 매개변수를 가져오기 위해선 인스턴스의 getBool/getDouble/getInt/getString 메소드를 활용하세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;만약 한번의 호출만으로 값을 가져오고 활성화하고 싶다면 fetchAndActivate 메소드를 활용하세요&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;await remoteConfig.fetchAndActivate();
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/247</guid>
      <comments>https://malangdidoo.tistory.com/247#entry247comment</comments>
      <pubDate>Mon, 4 Mar 2024 20:00:22 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] (Project) MapleApp: 33. 비공개 테스트-2</title>
      <link>https://malangdidoo.tistory.com/246</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;  천지인 키보드 지원&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;캐릭터를 검색하는 시점에 TextField에서 키보드 자판을 통해 문자를 입력받는 구간이 있다. 기존에는 무분별한 입력을 막기 위해 모음 선행 입력을 막아놨으나, 천지인 키보드에서는 입력이 되지 않는 다는 피드백을 받았다. 생각해보니, 입력 자체에는 문제가 없어야 하고 검색 시에는 무분별한 입력을 막아놓는 UX로 변경되어야 함을 깨달았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;또한, 천지인 키보드의 경우 Middle Dot이 있는데, 이 문자에 대한 입력을 신경써 주어야 함을 알게되었다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;// 기본 천지인키
ᆞᆢ
// 삼성 한손모아키
ㆍ
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;위 세가지 Middle Dot에 대해 정규표현식에 추가해주며, 천지인 키보드를 지원할 수 있게 만들었다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  데이터 요청 로직 변경&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;나는 Notifier를 사용하지 않을 때 자동으로 메모리 해제가 될 수 있도록 AutoDisposeNotifier를 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그러나 내가 기본 데이터를 변경하지 않아도, Notifier가 이미 해제되었기 때문에 기존의 NavigatorTab에 접근하여도 Notifier를 새로 생성하여 모든 API 요청을 다시금 불러오면서 로직을 변경해야겠다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그래서 AutoDisposeNotifier를 기본 Notifier로 변경하고, Notifier에서 특정 조건에 따라 데이터를 새로 갱신할 수 있도록 로직을 새로 구성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이 애플리케이션의 경우 기본 값인 닉네임이 변경되었을 때, 내부 데이터에 대한 새로운 업데이트를 진행해야 한다. 그래서, 기본적으로 Navigator를 선택하였을 때 닉네임에 대한 정보를 업데이트 하도록 구성해놓고, 내부 로직에 old/new 닉네임을 비교하여 업데이트를 진행에 대한 판단을 설정해 놓았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;대부분의 애플리케이션은 특정 페이지에 진입할 때, 해당 Notifier가 생성되고 그 페이지가 유지되는 한 Notifier는 지속되어 활용할 수 있으므로 현재 내 방식은 타 애플리케이션이나 백엔드 서버를 가지고 있는 애플리케이션과는 다른 양상을 보인다고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;나와 비슷한 현상을 가진 사람이 이 글을 보고 도움이 되길 바란다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  PageView Widget&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;GestureDetector를 사용하면, 사용자의 Swip 제스쳐에 따라 화면이 움직이지만 화면을 넘긴다는 생각은 들지 않는 UX이다. 그래서 추가적으로 AnimationWidget을 추가해야 하는데, 생각보다 개발 비용이 높다고 판단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이를 해결해줄 수 있는 부분이 PageView Widget이다. PageView는 네 방향으로 Swip제스쳐 기능을 제공하고, 페이지를 한장씩 넘기는듯한 UX를 경험할 수 있게 도와준다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  뒤로가기 버튼 제어&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;기기의 뒤로가기 버튼을 클릭했을 때, 네비게이터가 예상했던 대로 작동하지 않고 애플리케이션이 꺼져버리는 현상이 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;가장 근본적인 이유는, 설정해논 Router의 Nested 구조가 잘못되어있었기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;나는 Home을 비롯한 여러가지 탭을 네비게이터의 최상단으로 설정해 놓았었기 때문에, 다른 탭에서 Home으로 이동할 수 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Home 하위에 나머지 네비게이션 탭 정보를 추가함으로써, 원하던 결과를 만들어낼 수 있었다.&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/246</guid>
      <comments>https://malangdidoo.tistory.com/246#entry246comment</comments>
      <pubDate>Sat, 2 Mar 2024 16:35:14 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] (Project) MapleApp: 32. 비공개 테스트-1</title>
      <link>https://malangdidoo.tistory.com/245</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;비공개 테스트는 약 20명의 테스터를 선발하여 2주 동안 진행하는 과정이다. 신입 개발자 계정은 바로 프로모션으로 출시할 수 없고 이 과정을 반드시 거쳐야 한다는 흠이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2023년 12월 이후 개정된 사안으로, 애플리케이션 출시 전 앱 자체의 성능과 개선 사항을 해결하면서 사용자에게 맞는 앱으로 탄생할 수 있게 하자는 취지이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이런 비공개 테스트를 약 2주 동안 진행하면서, 내가 고쳐왔던 그리고 고치고 싶었던 내용과 보안 이슈 등에 관하여 서술할 예정이고, 추후 고도화를 위한 작업을 진행하면서 새로운 버전의 앱을 출시하고, 사용자들로 하여금 새로운 버전을 다운 받을 수 있게 하는 행위까지 경험해보려 한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  dio request exception&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;CRUD를 진행하면서 한 요청에 하나의 쿼리를 진행하는 과정엔 AsyncValue.guard를 사용하여 각 행동에 대해 value/error/loading을 지정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;하지만 내 프로젝트 같은 경우 Read request 밖에 없기 때문에, 한 함수에 모든 get 요청이 들어가 있다. 이 경우, 각 request에 대해 try/catch를 삽입하며 잘못된 경우 에러를 반환하고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;try/catch를 적용하기 전 코드는 이렇다. 다양한 구간이 포함되어 있지만, 간략하게 보여드리기 위함이다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;//AsyncNotifier&amp;lt;MainData&amp;gt;
Future&amp;lt;MainData&amp;gt; _fetch() async {
    Response response = await dio.get(dotenv.get('PATH'));
    final data = Data.fromJson(response.data);

    return MainData(data: data);
}

// outpage
final asyncMainData = ref.watch(asyncCharacterProvider);

return switch (asyncMainData) {
  AsyncData(:final value) =&amp;gt; DataInfo(info: value),
  AsyncError(:final error) =&amp;gt; MainErrorPage(message: error),
  _ =&amp;gt; const LoadingSpinner(),
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;함수의 특성 상 MainData 객체를 반환해야 하고, ref.watch로 Provider를 감시하는 경우 MainData가 반환 되기 전 까지 switch 구문의 _ &amp;rArr; 상태에 해당하며, MainData가 반환되면 AsyncData를 표출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그러나 이 구간에서는 AsyncError 개체를 반환하는 구간이 없어, AsyncNotifier에서 request 요청에 실패하는 경우 앱 구동이 멈추게 된다. 이 경우를 try/catch를 적용하여 error가 생기는 경우 AsyncError에 맞는 데이터로 반환하여 MainErrorPage를 표출하려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;AsyncError가 수신하기 위한 Future _fetch 함수의 반환 방법은 다음을 사용하면 됨을 찾았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Future.error('message')&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그러나 우리는 dio 객체에서 Response를 가로채 모든 오류에 대해 DioException을 반환하도록 하였고, Future 객체를 반환하는 Notifier를 AsyncValue를 활용하여 관리하고 있기 때문에 Future.error에 어떤 객체를 실어 보내도 AsyncError로 넘겨줄 수 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그 이유는 해당 값을 바라보는 NotifierProvider가 fetch 함수에서 에러가 발생할 때, 우리는 Future에 특정 개체를 담아서 보내기 때문이다. 그래서 AsyncValue는 개체에 대한 정보가 없는 상태이므로, AsyncError에서 Exception: variable is not found 오류를 표출하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그래서 AsyncError의 error 객체를 사용하지 않고, 최대한 각 요청 페이지를 분리하여 메세지를 넘겨주는 방식으로 처리했다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  TextField 입력 제한&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Play Console에 appbundle을 등록하면, 사전 출시 보고서를 받을 수 있다. 안정성/실적/접근성/스크린샷/보안 및 신뢰에 관하여 오류/경고/사소한 문제로 나누어 알려준다. 그 중, TextField에 이모지가 입력되면서, 이모지에 대해 콘텐츠 라벨이 지정되지 않는 문제에 대해 경고했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이모지는 446개가 넘고, TextField에 입력해야 할 정보는 영대소문자, 숫자, 한글이 끝이기 때문에 이외 문자는 입력하지 못하게 막아야했다. 그래서 찾아낸 속성이 inputFormatter이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://api.flutter.dev/flutter/services/TextInputFormatter-class.html&quot;&gt;https://api.flutter.dev/flutter/services/TextInputFormatter-class.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;inputFormatter는 TextInputFormatter class를 List로 받는 개체이다. 지정한 TextInputFormatter에 따라 달라지는데, TextInputFormatter에서 제공되는 withFunction method를 사용하면 TextFiled에 입력되는 oldValue/newValue를 비교하고 사용자의 요구 조건에 따라 처리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://api.flutter.dev/flutter/services/FilteringTextInputFormatter-class.html&quot;&gt;https://api.flutter.dev/flutter/services/FilteringTextInputFormatter-class.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;비슷한 행위를 진행하는 class의 예제를 보면 withFunction method의 활용을 볼 수 있다. 내가 지정한 패턴에 맞춰 newValue를 판단하고, old/new Value를 반환하도록 설정할 수 있다. 내가 사용한 코드는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;TextInputFormatter.withFunction(
    (oldValue, newValue) {
  return newValue.text.contains(
          RegExp(r'[^0-9a-zA-Zㄱ-ㅎ가-힣]'))
      ? oldValue
      : newValue;
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  콘텐츠 라벨 지정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;아이콘/이미지에는 스크린 리더가 읽을 수 있는 라벨이 있어야 합니다. 대표적인 뒤로 가기 버튼인 경우 해당됩니다. 애플리케이션 내 아이콘/이미지를 사용하고 있다면 모든 Widget에 관해 진행하는 것을 추천합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://api.flutter.dev/flutter/widgets/Image/semanticLabel.html&quot;&gt;https://api.flutter.dev/flutter/widgets/Image/semanticLabel.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;콘텐츠 라벨 지정은 Icon/Image Widget의 semanticLabel 속성을 활용하여 다음과 같이 할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;Icon(
  Icons.navigate_before_rounded,
  color: Colors.white,
  semanticLabel: '뒤로 가기 버튼',
),
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;또한 콘텐츠 라벨은 Semantic Widget을 활용하여 설정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://api.flutter.dev/flutter/widgets/Semantics-class.html&quot;&gt;https://api.flutter.dev/flutter/widgets/Semantics-class.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Image나 Icon과 같은 Widget은 내부 semanticLabel을 활용하여 지정할 수 있지만, 대게 많은 요소는 존재하지 않고 설명할 수 있는 방법도 많지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그래서 Flutter 에서는 Semantic Widget을 제공하는 것이다. 내부 label 속성을 활용하여 요소에 대해 설명하고, 해당 요소가 button인지 readOnly인지 등의 내용을 assitive technologies에게 알려줄 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  GestureDetector 활용하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;애플리케이션을 사용하다 보면, NavigationBar는 접근성이 뛰어나지만 상단 SelectTabBar는 높은 위치에 존재하여 접근성에 매우 불리하다. 사용자로 하여금 ViewArea에 존재해야 하기 때문이다. 그래서 사용자 제스쳐를 통해 간단하게 SelectTabBar를 조정할 수 있도록 만드려고 한다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;GestureDetector(
    onPanUpdate: (details) {
      if (details.delta.dx &amp;lt; -3) {
        ref.read(skillSelectTabProvider.notifier).update((state) =&amp;gt; 'v');
      }
    },
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;onPanUpdate 속성은 사용자가 포인터를 변경 시킬 때마다 호출된다. 원하는 임계값에 도달했을 때, SelectTabBar의 속성 값을 변경하여 주면 된다.&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/245</guid>
      <comments>https://malangdidoo.tistory.com/245#entry245comment</comments>
      <pubDate>Thu, 29 Feb 2024 13:24:41 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] (Project) MapleApp: 31. 심사 통과</title>
      <link>https://malangdidoo.tistory.com/244</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;리젝을 해결하기 위한 기간이 꽤나 오랜 기간이 걸렸다. 실질적으로 Play Console에서 주는 답변은 특정 항목란에 문제가 있음을 제기하지만, 정확히 어떤 것이 문제인지는 알려주지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;나는 메이플스토리라는 게임의 애플리케이션을 만들었기 때문에, 관련 단어를 자주 사용했다. 하지만, 스토어 등록정보 권장사항을 바라봤을 때 앱 설명란에 다음의 항목이 존재함을 알아차릴 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot;&gt;  다른 사용자 및 앱과 관계를 오해할 만한 단어 및 문구를 사용하지 않습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;즉 메이플스토리라는 게임과의 관계를 오해할 수 있다 판단하여, 적절치 못한 사용이라고 내게 계속 안내해준 것 같았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그래서 설명란의 해당 단어를 한쪽은 유지하고, 한쪽은 배제한 채 작성한 채 다시 심사를 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이후 배제한 항목은 리젝 사유에서 벗어날 수 있었기 때문에 관련 모든 단어를 삭제함으로써, 또는 명시적으로 표기하지 않으면서 심사에서 통과할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앞으로 이 앱은 정식 출시를 향해 비공개 테스트를 진행해야한다. 비공개 테스트는 어떻게 진행해야 하는 것이며, 또 어떤 방식으로 수정하고 발전시켜 나갈지에 대해 기술하도록 하겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;대부분의 방향성은 출시를 향한, 즉 프로덕션으로 엑세스가 되기 위한 과정을 밟아나갈 예정이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1047&quot; data-origin-height=&quot;366&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/reoLe/btsFjYih1a0/77G8oVw8Nz4GKy8G1e9czK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/reoLe/btsFjYih1a0/77G8oVw8Nz4GKy8G1e9czK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/reoLe/btsFjYih1a0/77G8oVw8Nz4GKy8G1e9czK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FreoLe%2FbtsFjYih1a0%2F77G8oVw8Nz4GKy8G1e9czK%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;1047&quot; height=&quot;366&quot; data-origin-width=&quot;1047&quot; data-origin-height=&quot;366&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/244</guid>
      <comments>https://malangdidoo.tistory.com/244#entry244comment</comments>
      <pubDate>Thu, 29 Feb 2024 10:53:31 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] Infinity Carousel (무한 캐러셀)</title>
      <link>https://malangdidoo.tistory.com/243</link>
      <description>&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;무한 캐러셀에 대한 Demo 및 Code는 다음 경로를 참고하세요.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://github.com/Jisup/flutter_sample/tree/main/lib/page/infinityCarousel&quot;&gt;https://github.com/Jisup/flutter_sample/tree/main/lib/page/infinityCarousel&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;무한 캐러셀은 사용자가 처음부터 끝까지 스크롤하지 않고도 계속해서 다음 콘텐츠를 볼 수 있도록 설계된 디자인을 말합니다. 웹 또는 앱에서 여러 이미지나 콘텐츠를 순환하거나 끊임없이 UI요소를 보여주는 UI 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;InfinityScroll과 가장 큰 차이점은 사용자가 정확히 하나의 콘텐츠를 인지하고 넘길 수 있는 UX를 제공하는 것입니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  PageView&lt;/h4&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://api.flutter.dev/flutter/widgets/PageView-class.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://api.flutter.dev/flutter/widgets/PageView-class.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;PageView는 위젯이 가득한 화면을 넘겨보게 만들 수 있는 위젯입니다. 또한,&amp;nbsp;PageController를 사용하여 스와이프를 감지하고 애니메이션을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;PageView로 InfinityCarousel을 만들고 싶다면, onPageChanged 속성을 활용하여 원하는 임계값에 도달했을 때 새로운 콘텐츠를 불러올 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;// StatefulWidget
PageController _pageController;

// StatefulWidget.initState
_pageController = PageController(initialPage: 1);

// StatefulWidget.build
PageView(
  controller: _pageController,
  onPageChanged: (value) {
    if (photoList.length - 2 &amp;lt; value) {
      _fetchPhotos();
    }
  },
  children: photoList.isNotEmpty
      ? photoList
          .map((photo) =&amp;gt; Container(
                child: Image.network(photo.url!),
              ))
          .toList()
      : [],
),&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  smooth_page_indicator&lt;/h4&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://pub.dev/packages/smooth_page_indicator&quot;&gt;https://pub.dev/packages/smooth_page_indicator&lt;/a&gt;&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;flutter pub add smooth_page_indicator
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;PageView를 사용할 때, UX 향상을 위해 필요한 indicator는 Carousel의 현재 위치를 확인하거나 원하는 위치로 이동하기 위해 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그러나 InfinityCarousel을 구현할 때는 indicator의 존재가 무의미할 수 있으므로 추가하는 것을 고려해보아야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;smooth_page_indicator의 effect 속성을 활용하면 다양한 옵션에 대한 customize를 진행할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;SmoothPageIndicator(
  controller: _pageController,
  count: itemList.length,
  effect: ScrollingDotsEffect(),
  onDotClicked: (index) =&amp;gt; _pageController.animateToPage(
    index,
    duration: const Duration(milliseconds: 300),
    curve: Curves.bounceInOut,
  ),
),&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  carousel_slider&lt;/h4&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ refernce : &lt;a href=&quot;https://pub.dev/packages/carousel_slider&quot;&gt;https://pub.dev/packages/carousel_slider&lt;/a&gt;&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/243</guid>
      <comments>https://malangdidoo.tistory.com/243#entry243comment</comments>
      <pubDate>Fri, 23 Feb 2024 12:56:25 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] Infinity Scroll (무한 스크롤)</title>
      <link>https://malangdidoo.tistory.com/242</link>
      <description>&lt;p style=&quot;color: #333333; text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;무한 스크롤에 대한 Demo 및 Code는 다음 경로를 참고하세요.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/Jisup/flutter_sample/tree/main/lib/page/infinityScroll&quot;&gt;https://github.com/Jisup/flutter_sample/tree/main/lib/page/infinityScroll&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;무한 스크롤은 사용자 경험 디자인 패턴 중 하나로, 사용자가 스크롤을 활용하여 콘텐츠를 제공받을 때 일정량의 정보를 소비하고 나면 자연스럽게 다음 콘텐츠로 넘어갈 수 있도록 새로운 콘텐츠를 자동으로 추가하여 페이지를 넘기는 번거로움 없이 연속적인 내용을 볼 수 있게 해주는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;개발을 진행함에 있어 신경 써야 하는 부분은 Viewport, Content의 크기 관리와 사용자의 스크롤 제스처 조작입니다. 사용자가 Viewport 내에서 Content의 개수, 크기와 관련되어 일정량의 콘텐츠를 소비하였을 때 새로운 콘텐츠를 불러올 수 있도록 로직을 설계하는 것에 집중해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Flutter에서 무한 스크롤을 구현하기 위한 방법은 무엇이 있는지 UX 순서에 따라 기술하겠습니다. 구현에 사용되는 데이터는 {JSON} Placeholder를 활용하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;사용자가 콘텐츠를 소비하는 UX는 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  사용자가 콘텐츠를 스크롤 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  사용자의 스크롤이 Viewport의 마지막에 도달하거나, 사용자의 스크롤이 특정량의 콘텐츠를 소비한 Viewport에 도달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  사용자의 스크롤 제스처(아래에서 위로 당기기)를 통해 새로운 콘텐츠를 불러온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  사용자가 새로운 콘텐츠를 스크롤 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  ScrollController&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;ScrollController에는 position 속성을 통해 스크롤의 현재 위치을 나타내는 pixel와 스크롤의 최대 크기를 나타내는 maxScrollExtent 값이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이 두 값을 활용하여 사용자의 스크롤이 컨텐츠의 어느 부분에 존재하는지 파악할 수 있고, 개발자는 특정 임계값에 새로운 콘텐츠를 불러오기 위한 로직을 추가할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;StatefulWidget을 활용하여 ScrollController를 생성하고 Listener를 할당하여 원하는 동작을 이끌어냅니다. 핵심 코드는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;  @override
  void initState() {
    super.initState();
    _scrollController = ScrollController();
    _scrollController.addListener(_onScroll);
  }

  void _onScroll() async {
    final maxScroll = _scrollController.position.maxScrollExtent;
    final currentScroll = _scrollController.position.pixels;

    // if (maxScroll != currentScroll) {
    if (maxScroll - currentScroll &amp;lt; contentHeight) {
      if (!isLoading) {
        setState(() {
          pullHeight = contentHeight;
        });
        await _fetchPhotos();
      }
      setState(() {
        pullHeight = 0;
      });
    }
  }&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  Listener + ScrollController&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;ScrollController의 Listener를 사용하지 않고, Flutter의 Listener Widget을 활용해도 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;onPointerMove 속성을 통해 마우스의 움직임을 확인하고, onPointerUp 속성을 통해 마우스의 움직임이 멈춤을 확인할 수 있습니다. 하지만, ScrollControlller에서 현재 스크롤 정보와 최대 스크롤 크기 정보를 활용해야 하므로, 기존 ScrollController의 Listener를 사용하는 것이 낫습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그러나, Listener Widget을 활용하면 사용자의 스크롤 속도(scrollVelocity)에 맞춰 로딩중인 애니메이션 컨테이너를 조작할 수 있습니다. 마치, 배달의 민족 애플리케이션의 땡겨요 처럼 말입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;스크롤이 작동하는 Viewport를 자식으로 가지는 Listener Widget을 생성하고, onPointerMove/onPointerUp 속성을 구현하여 사용자의 스크롤 제스쳐(땡겨요)를 통해 새로운 데이터를 불러올 수 있습니다. 기존 ScrollController처럼 사용자가 새로운 콘텐츠를 부드럽게 경험하게 만들기 위해서는 임계값의 설정을 조정해주면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;Listener(
  onPointerMove: (event) {
    if (event.delta.dy &amp;gt;= 0) return;
    // if (_scrollController.position.maxScrollExtent - contentHeight &amp;lt;
    if (_scrollController.position.maxScrollExtent !=
        _scrollController.position.pixels) return;
    setState(() {
      pullHeight += -event.delta.dy;
    });
  },
  onPointerUp: (event) async {
    if (pullHeight &amp;gt; contentHeight &amp;amp;&amp;amp; !isLoading) {
      setState(() {
        pullHeight = contentHeight;
      });
      await _fetchPhotos();
    }
    setState(() {
      pullHeight = 0;
    });
  },
  child: Scaffold(...),
);&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  infinity_scroll_pagination Library&lt;/h4&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://pub.dev/packages/infinite_scroll_pagination&quot;&gt;https://pub.dev/packages/infinite_scroll_pagination&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;무한 스크롤 페이지 매김, 끝없는 스크롤 페이지 매김, 자동 페이지 매김, 지연 로딩 페이지 매김, 프로그레시브 로딩 페이지 매김 등으로 알려진, 사용자가 화면을 아래로 스크롤할 때 작은 항목 덩어리를 천천히 로드하고 표시하는 데 도움이 되는 독립적이고 확장 가능하며 고도로 사용자 정의 가능한 패키지입니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;$ flutter pub add infinite_scroll_pagination&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;라이브러리 내부 PagingController와 PageListView Widget 그리고 PagedChildBuilderDelegate Widget을 활용하여 요소를 간편하게 구성해갈 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  FlatList Library&lt;/h4&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://pub.dev/packages/flat_list&quot;&gt;https://pub.dev/packages/flat_list&lt;/a&gt;&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/242</guid>
      <comments>https://malangdidoo.tistory.com/242#entry242comment</comments>
      <pubDate>Tue, 20 Feb 2024 18:31:52 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] TDD (Test Driven Development) - Riverpod</title>
      <link>https://malangdidoo.tistory.com/241</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;이 테스트는 상태관리를 검증하는 역할을 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;상태관리를 검증하는 이유는, 코드의 동작을 확인하고 안정성을 보장하여 예측 가능한 애플리케이션을 만들어가기 위해서 진행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Notifier 구현&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Riverpod의 Notifier를 활용한 상태관리를 구현하는 부분을 추가합니다. Notifier는 TodoUseCase를 활용하여 Todo데이터를 State에 저장하고 관리합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;final todoProvider = StateNotifierProvider&amp;lt;TodoNotifier, AsyncValue&amp;lt;List&amp;lt;Todo&amp;gt;&amp;gt;&amp;gt;((ref) {
  final todoUseCase = ref.read(todoUseCaseProvider);
  return TodoNotifier(todoUseCase: todoUseCase);
});

class TodoNotifier extends StateNotifier&amp;lt;AsyncValue&amp;lt;List&amp;lt;Todo&amp;gt;&amp;gt;&amp;gt; {
  final TodoUseCase todoUseCase;

  TodoNotifier({required this.todoUseCase}) : super(AsyncValue.loading()) {
    fetchTodoList();
  }

  Future&amp;lt;void&amp;gt; fetchTodoList() async {
    state = AsyncValue.loading();
    try {
      final todos = await todoUseCase(NoParams());
      state = AsyncValue.data(todos);
    } catch (_) {
      state = AsyncValue.error('Failed to fetch todos');
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  객체 선언 및 초기화&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Riverpod의 경우, Provider를 활용하기 때문에 ProviderContainer와 비즈니스 로직인 UseCase가 필요합니다. 이후 container의 Provider에 UseCase의 결과 값을 추가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이 준비를 함으로써, 우리는 Provider를 활용하는 Container의 동작에 대해 검증하고 상태관리가 정확히 진행되는 지 파악할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;class MockTodoUseCase extends Mock implements TodoUseCase {}

void main() {
  late ProviderContainer container;
  late MockTodoUseCase mockTodoUseCase;

  setUp(() {
    container = ProviderContainer();
    mockTodoUseCase = MockTodoUseCase();
    when(mockTodoUseCase(NoParams())).thenAnswer((_) async =&amp;gt; [
      Todo(id: 1, title: 'Todo 1'),
      Todo(id: 2, title: 'Todo 2'),
    ]);
    container.readProvider.overrideWithProvider(todoUseCaseProvider.overrideWithValue(mockTodoUseCase));
  });

	...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  테스트 선언/준비/실행/검증&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  테스트 선언&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;테스트 이름을 설정합니다. 해당 테스트가 어떤 동작을 검증하는지 쉽게 이해할 수 있도록 작성해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;test('should emit data state when fetchTodoList succeeds', () async {});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  테스트 준비/실행/검증&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;테스트는 arrange/act/assert 즉, 준비/실행/검증 단계로 구조화되어 작성됩니다. 코드의 가독성을 높이고 어떤 부분에서 테스트가 실패했는지 명확하게 파악할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;특히 여러 테스트 케이스를 작성할 때 일관성을 유지하고 테스트의 목적을 명확히 전달하는 것에 도움을 줄 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;test('initial state should be loading', () {
  expect(container.read(todoProvider), equals(const AsyncValue.loading()));
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;상태관리 테스트에서는 이전과는 다른 새로운 테스트도 있습니다. 위 코드는 Container가 정확히 로딩 중인지 상태를 확인하는 테스트입니다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;test('should emit data state when fetchTodoList succeeds', () async {
  // act
  await container.read(todoProvider.notifier).fetchTodoList();
  
  // assert
  expect(container.read(todoProvider), equals(const AsyncValue.data([
    Todo(id: 1, title: 'Todo 1'),
    Todo(id: 2, title: 'Todo 2'),
  ])));
});
test('should emit error state when fetchTodoList fails', () async {
  // arrange
  when(mockTodoUseCase(NoParams())).thenThrow(Exception('Failed to fetch todos'));

  // act
  await container.read(todoProvider.notifier).fetchTodoList();
  
  // assert
  expect(container.read(todoProvider), equals(const AsyncValue.error('Failed to fetch todos')));
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;위 두 예제는 Provider의 상태 값과, fetch를 통해 결과 값으로 생성한 TodoList의 값이 동일한지 판단하고 fetch에 실패했을 경우에 대한 결과입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  테스트 그룹&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;여러개의 테스트를 그룹화 할 수 있습니다. 관련된 테스트 케이스를 묶어서 Test Suite를 형성하고, 코드의 특정 부분이나 기능에 대한 다양한 측면을 검증하기 용이하게 만들어줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;보통 가독성 향상/유사한 행위에 대한 통합 검증/효율적인 테스트 수행/코드 수정 용이성에 대한 이점을 얻을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&quot;group&quot; 메소드를 활용하고 전반적인 테스트를 통합하여 표현할 수 있는 문구 또는 함수명을 작성하는 것이 보편적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;전체 소스코드는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;class MockTodoUseCase extends Mock implements TodoUseCase {}

void main() {
  late ProviderContainer container;
  late MockTodoUseCase mockTodoUseCase;

  setUp(() {
    container = ProviderContainer();
    mockTodoUseCase = MockTodoUseCase();
    when(mockTodoUseCase(NoParams())).thenAnswer((_) async =&amp;gt; [
      Todo(id: 1, title: 'Todo 1'),
      Todo(id: 2, title: 'Todo 2'),
    ]);
    container.readProvider.overrideWithProvider(todoUseCaseProvider.overrideWithValue(mockTodoUseCase));
  });

  group('TodoNotifier', () {
    test('initial state should be loading', () {
      expect(container.read(todoProvider), equals(const AsyncValue.loading()));
    });

    test('should emit data state when fetchTodoList succeeds', () async {
      // act
      await container.read(todoProvider.notifier).fetchTodoList();
      
      // assert
      expect(container.read(todoProvider), equals(const AsyncValue.data([
        Todo(id: 1, title: 'Todo 1'),
        Todo(id: 2, title: 'Todo 2'),
      ])));
    });

    test('should emit error state when fetchTodoList fails', () async {
      // arrange
      when(mockTodoUseCase(NoParams())).thenThrow(Exception('Failed to fetch todos'));

      // act
      await container.read(todoProvider.notifier).fetchTodoList();
      
      // assert
      expect(container.read(todoProvider), equals(const AsyncValue.error('Failed to fetch todos')));
    });
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  테스트 진행&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;테스트는 루트 디렉토리에서 다음 명령어를 활용하여 실행할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;flutter test {filePath}/{fileName}.dart
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;각 테스트 케이스에 대한 결과와 통과 여부에 대한 정보가 터미널에 출력됩니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;00:10 +3: All tests passed!
00:10 +3 -1: Some tests failed.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;테스트 결과는 시간/개수/내용으로 구분되어 표기되며 성공한 테스트는 &quot;+&quot;, 실패한 테스트는 &quot;-&quot;로 표기됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;테스트 코드에서 print 함수를 활용하여 명시적으로 작성한 내용과 함께 실패의 원인이 되는 에러 메세지가 터미널에 표기되므로, 참고하여 테스트 준비/실행/검증 단계를 반복해가면 됩니다.&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/241</guid>
      <comments>https://malangdidoo.tistory.com/241#entry241comment</comments>
      <pubDate>Thu, 8 Feb 2024 11:59:14 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] TDD (Test Driven Development) - UseCase</title>
      <link>https://malangdidoo.tistory.com/240</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Clean Architecture에서 UseCase는 Repository를 활용하여 실질적인 비즈니스 로직을 구현하는 것에 초점을 맞추고 있습니다. UseCase Test는 결과를 잘 가져오는지 그리고 MockRepository를 활용한 결과 값과 UseCase의 결과 값이 같은지 비교합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  객체 선언 및 초기화&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;class MockTodoRepository extends Mock implements TodoRepository {}

void main() {
  late TodoUseCase todoUseCase;
  late MockTodoRepository mockTodoRepository;

  setUp(() {
    mockTodoRepository = MockTodoRepository();
    usecase = TodoUseCase(mockTodoRepository);
  });
	
	...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  더미 데이터 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;UseCase의 결과 값에 대한 더미 데이터를 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;final List&amp;lt;Todo&amp;gt; testTodos = [
  Todo(id: 1, title: 'Todo 1'),
  Todo(id: 2, title: 'Todo 2'),
];
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  테스트 선언/준비/실행/검증&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  테스트 선언&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;테스트 이름을 설정합니다. 해당 테스트가 어떤 동작을 검증하는지 쉽게 이해할 수 있도록 작성해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;vbnet&quot;&gt;&lt;code&gt;test('should get a list of Todo entities from the repository', () async {});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  테스트 준비/실행/검증&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;테스트는 arrange/act/assert 즉, 준비/실행/검증 단계로 구조화되어 작성됩니다. 코드의 가독성을 높이고 어떤 부분에서 테스트가 실패했는지 명확하게 파악할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;특히 여러 테스트 케이스를 작성할 때 일관성을 유지하고 테스트의 목적을 명확히 전달하는 것에 도움을 줄 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;test('should get a list of Todo entities from the repository', () async {
  // arrange
  when(mockTodoRepository.getTodoList()).thenAnswer((_) async =&amp;gt; testTodos);

  // act
  final result = await todoUseCase(NoParams());

  // assert
  expect(result, equals(testTodos));
  verify(mockTodoRepository.getTodoList());
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;초반에 설계 했던 테스트 목적에 대한 각 arrange/act/assert는 다음과 같은 모습으로 설계할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  arrange&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Repository Mock 객체를 활용하여 결과 값을 수신합니다. 결과 값은 &amp;ldquo;thenAnswer&quot; 메소드를 활용해 생성해 논 더미데이터로 대체합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  thenAnswer&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;thenAnswer 메소드는 mockito 라이브러리에서 제공하는 기능입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  act&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;todoUseCase를 활용한 결과 값을 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  assert&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;mock 객체를 활용하였으므로 mockito 라이브러리의 &quot;verify&quot; 메소드를 활용해 mock 객체가 정확히 동작하는지 확인합니다. 또한, 두 결과 값이 일치하는지 비교하기 위해 flutter_test 라이브러리의 &quot;expact&quot; 메소드를 활용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  테스트 그룹&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;여러개의 테스트를 그룹화 할 수 있습니다. 관련된 테스트 케이스를 묶어서 Test Suite를 형성하고, 코드의 특정 부분이나 기능에 대한 다양한 측면을 검증하기 용이하게 만들어줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;보통 가독성 향상/유사한 행위에 대한 통합 검증/효율적인 테스트 수행/코드 수정 용이성에 대한 이점을 얻을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&quot;group&quot; 메소드를 활용하고 전반적인 테스트를 통합하여 표현할 수 있는 문구 또는 함수명을 작성하는 것이 보편적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;전체 소스코드는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;class MockTodoRepository extends Mock implements TodoRepository {}

void main() {
  late TodoUseCase todoUseCase;
  late MockTodoRepository mockTodoRepository;

  setUp(() {
    mockTodoRepository = MockTodoRepository();
    usecase = TodoUseCase (mockTodoRepository);
  });

  group('GetTodoList', () {
    final List&amp;lt;Todo&amp;gt; testTodos = [
      Todo(id: 1, title: 'Todo 1'),
      Todo(id: 2, title: 'Todo 2'),
    ];

    test('should get a list of Todo entities from the repository', () async {
      // arrange
      when(mockTodoRepository.getTodoList()).thenAnswer((_) async =&amp;gt; testTodos);

      // act
      final result = await todoUseCase(NoParams());

      // assert
      expect(result, equals(testTodos));
      verify(mockTodoRepository.getTodoList());
    });

    test('should throw an exception if the repository call fails', () async {
      // arrange
      when(mockTodoRepository.getTodoList()).thenThrow(Exception('Repository error'));

      // act
      final call = usecase;

      // assert
      expect(() =&amp;gt; call(NoParams()), throwsException);
      verify(mockTodoRepository.getTodoList());
    });
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  테스트 진행&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;테스트는 루트 디렉토리에서 다음 명령어를 활용하여 실행할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;flutter test {filePath}/{fileName}.dart
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;각 테스트 케이스에 대한 결과와 통과 여부에 대한 정보가 터미널에 출력됩니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;00:10 +3: All tests passed!
00:10 +3 -1: Some tests failed.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;테스트 결과는 시간/개수/내용으로 구분되어 표기되며 성공한 테스트는 &quot;+&quot;, 실패한 테스트는 &quot;-&quot;로 표기됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;테스트 코드에서 print 함수를 활용하여 명시적으로 작성한 내용과 함께 실패의 원인이 되는 에러 메세지가 터미널에 표기되므로, 참고하여 테스트 준비/실행/검증 단계를 반복해가면 됩니다.&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/240</guid>
      <comments>https://malangdidoo.tistory.com/240#entry240comment</comments>
      <pubDate>Wed, 7 Feb 2024 14:10:27 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] TDD (Test Driven Development) - Repository</title>
      <link>https://malangdidoo.tistory.com/239</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;이 테스트는 data폴더에서 구현한 todo_repository를 검증하는 역할을 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;repository는 가장 바깥쪽 계층인 Driver, 즉 외부 API와 통신하는 DataSource의 의존성을 주입 받기 때문에, UseCase&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  객체 선언 및 초기화&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;class MockTodoRemoteDataSource extends Mock implements TodoRemoteDataSource {}

void main() {
  late TodoRepository repository;
  late MockTodoRemoteDataSource mockRemoteDataSource;

  setUp(() {
    mockRemoteDataSource = MockTodoRemoteDataSource();
    repository = TodoRepositoryImpl(remoteDataSource: mockRemoteDataSource);
  });

	...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  더미 데이터 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;테스트 성공 사례에서 반환 될 객체에 대한 더미데이터를 생성합니다. 만약 테스트 성공 시 반환 될 결과 값이 존재하지 않는다면 시행하지 않아도 됩니다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;final List&amp;lt;Todo&amp;gt; testTodos = [
  Todo(id: 1, title: 'Todo 1'),
  Todo(id: 2, title: 'Todo 2'),
];
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  테스트 선언/준비/실행/검증&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  테스트 선언&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;테스트 이름을 설정합니다. 해당 테스트가 어떤 동작을 검증하는지 쉽게 이해할 수 있도록 작성해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;test('should return a list of Todo entities from the remote data source', () {});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  테스트 준비/실행/검증&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;테스트는 arrange/act/assert 즉, 준비/실행/검증 단계로 구조화되어 작성됩니다. 코드의 가독성을 높이고 어떤 부분에서 테스트가 실패했는지 명확하게 파악할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;특히 여러 테스트 케이스를 작성할 때 일관성을 유지하고 테스트의 목적을 명확히 전달하는 것에 도움을 줄 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;test('should return a list of Todo entities from the remote data source', () async {
  // arrange
  when(mockRemoteDataSource.fetchRemoteTodos()).thenAnswer((_) async =&amp;gt; testTodos);

  // act
  final result = await repository.getTodoList();

  // assert
  expect(result, equals(testTodos));
  verify(mockRemoteDataSource.fetchRemoteTodos());
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;현재는 dataSource로부터 최근 Todo의 리스트를 반환받는 테스트를 진행하고 있으므로, 목적에 대한 각 arrange/act/assert는 다음과 같은 모습으로 설계할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  arrange&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Mock을 활용하여 생성한 mockRemoteDataSource 객체에서 Todo에 대한 최근 목록을 불러오는 함수를 실행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그에 대한 결과는 &quot;mockito&quot; 테스트 라이브러리에서 제공하는 &quot;thenAnswer&quot; 메소드를 활용하여 이전에 생성한 더미데이터를 항상 반환하도록 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  act&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Repository를 활용해서 최근 Todo 목록을 불러옵니다. 이 방법은, Repository가 정상적으로 내부 getTodoList 메소드를 통해 remoteDataSource에 접근하고 fetchRemoteTodos를 실행하는지 판단합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  assert&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;arrange와 act에서 생성한 두 결과에 대해 검증합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;우리는 mockRemoteDataSource가 정상적으로 실행되는지, mock과 repository의 결과가 동일한지 검증을 해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;mockRemoteDataSource의 경우 mock 객체의 메소드 호출을 검증해야 하므로 mockito 라이브러리의 verify를 사용하고, mock과 repository에 대한 결과를 비교하기 위해 flutter_test 라이브러리의 expact를 사용합니다. verify와 expact는 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  expact&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;flutter_test 라이브러리에 속한 메소드로 예상 결과를 검증할 때 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;actual (테스트 대상)과 matcher (기대 결과 값)을 매개변수로 가지며, 조건을 만족하지 않을 시 테스트를 실패로 표기합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  verify&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;mockito 라이브러리에 속한 메소드로 Mock 객체의 메소드 호출을 검증할 때 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  테스트 그룹&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;여러개의 테스트를 그룹화 할 수 있습니다. 관련된 테스트 케이스를 묶어서 Test Suite를 형성하고, 코드의 특정 부분이나 기능에 대한 다양한 측면을 검증하기 용이하게 만들어줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;보통 가독성 향상/유사한 행위에 대한 통합 검증/효율적인 테스트 수행/코드 수정 용이성에 대한 이점을 얻을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&quot;group&quot; 메소드를 활용하고 전반적인 테스트를 통합하여 표현할 수 있는 문구 또는 함수명을 작성하는 것이 보편적입니다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;group('getTodoList', () {
	final List&amp;lt;Todo&amp;gt; testTodos = [
	  Todo(id: 1, title: 'Todo 1'),
	  Todo(id: 2, title: 'Todo 2'),
	];
	
	test('should return a list of Todo entities from the remote data source', () async {
	  // arrange
	  when(mockRemoteDataSource.fetchRemoteTodos()).thenAnswer((_) async =&amp;gt; testTodos);
	
	  // act
	  final result = await repository.getTodoList();
	
	  // assert
	  expect(result, equals(testTodos));
	  verify(mockRemoteDataSource.fetchRemoteTodos());
	});
	
	test('should throw an exception if the remote data source call fails', () async {
	  // arrange
	  when(mockRemoteDataSource.fetchRemoteTodos()).thenThrow(Exception('Server error'));
	
	  // act
	  final call = repository.getTodoList;
	
	  // assert
	  expect(() =&amp;gt; call(), throwsException);
	  verify(mockRemoteDataSource.fetchRemoteTodos());
	});
	
	test('should throw a specific exception if the remote data source returns an empty list', () async {
	  // arrange
	  when(mockRemoteDataSource.fetchRemoteTodos()).thenAnswer((_) async =&amp;gt; []);
	
	  // act
	  final call = repository.getTodoList;
	
	  // assert
	  expect(() =&amp;gt; call(), throwsA(isA&amp;lt;EmptyListException&amp;gt;()));
	  verify(mockRemoteDataSource.fetchRemoteTodos());
	});
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;전체 소스코드는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;class MockTodoRemoteDataSource extends Mock implements TodoRemoteDataSource {}

void main() {
  late TodoRepository repository;
  late MockTodoRemoteDataSource mockRemoteDataSource;

  setUp(() {
    mockRemoteDataSource = MockTodoRemoteDataSource();
    repository = TodoRepositoryImpl(remoteDataSource: mockRemoteDataSource);
  });

  group('getTodoList', () {
    final List&amp;lt;Todo&amp;gt; testTodos = [
      Todo(id: 1, title: 'Todo 1'),
      Todo(id: 2, title: 'Todo 2'),
    ];

    test('should return a list of Todo entities from the remote data source', () async {
      // arrange
      when(mockRemoteDataSource.fetchRemoteTodos()).thenAnswer((_) async =&amp;gt; testTodos);

      // act
      final result = await repository.getTodoList();

      // assert
      expect(result, equals(testTodos));
      verify(mockRemoteDataSource.fetchRemoteTodos());
    });
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  테스트 진행&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;테스트는 루트 디렉토리에서 다음 명령어를 활용하여 실행할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;flutter test {filePath}/{fileName}.dart
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;각 테스트 케이스에 대한 결과와 통과 여부에 대한 정보가 터미널에 출력됩니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;00:10 +3: All tests passed!
00:10 +3 -1: Some tests failed.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;테스트 결과는 시간/개수/내용으로 구분되어 표기되며 성공한 테스트는 &quot;+&quot;, 실패한 테스트는 &quot;-&quot;로 표기됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;테스트 코드에서 print 함수를 활용하여 명시적으로 작성한 내용과 함께 실패의 원인이 되는 에러 메세지가 터미널에 표기되므로, 참고하여 테스트 준비/실행/검증 단계를 반복해가면 됩니다.&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/239</guid>
      <comments>https://malangdidoo.tistory.com/239#entry239comment</comments>
      <pubDate>Tue, 6 Feb 2024 13:19:10 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] TDD (Test Driven Development) - DataSource</title>
      <link>https://malangdidoo.tistory.com/238</link>
      <description>&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ 글의 순서는  &amp;gt; &amp;gt; &amp;gt; &amp;gt; &amp;gt;  순서로 하위 내용을 구성하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;DataSource는 외부 API와 통신을 진행하는 Framework &amp;amp; Driver 계층에 속합니다. 이 계층에서는 HTTP 통신에 대한 객체 생성과 관리, 요청에 대한 테스트 진행 방법에 대해 기술하려 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  todo_remote_data_source_test&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이 테스트는 TodoList를 구현할 때, HTTP 통신을 위한 Dio 라이브러리를 활용하여 외부 API와 통신하는 TodoRemoteDataSource Class의 메소드를 검증하는 역할을 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  객체 선언 및 초기화&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Create Mock Object of HTTP Client&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;네트워크 요청을 시뮬레이션 하기 위해서 먼저, DataSource Class에서 전반적으로 사용되는 HTTP 요청 라이브러리인 Dio에 대한 객체를 Mock 객체로 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;class MockDioClient extends Mock implements Dio {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Initialize Mock Objects&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Mock 객체와 테스트 대상 객체에 대한 초기화를 진행합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;  late TodoRemoteDataSource dataSource;
  late MockDioClient mockDioClient;

  setUp(() {
    mockDioClient = MockDioClient();
    dataSource = TodoRemoteDataSourceImpl(dio: mockDioClient);
  });&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  더미 데이터 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;HTTP 요청을 통해 반환 될 데이터의 더미 데이터 값을 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;final List&amp;lt;Map&amp;lt;String, dynamic&amp;gt;&amp;gt; todoListJson = [
  {&quot;id&quot;: 1, &quot;title&quot;: &quot;Todo 1&quot;},
  {&quot;id&quot;: 2, &quot;title&quot;: &quot;Todo 2&quot;},
];&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  테스트 선언/준비/실행/검증&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  테스트 선언&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;테스트 이름을 설정합니다. 해당 테스트가 어떤 동작을 검증하는지 쉽게 이해할 수 있도록 작성해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;일반적으로 &quot;should get ~ from repository&quot; / &quot;should return ~ when ~&quot;과 같은 형태로 사용됩니다. 테스트가 어떤 결과를 예상하고 있는지 명시적으로 나타낼 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;vbnet&quot;&gt;&lt;code&gt;test('should perform a GET request on the given URL', () async {});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  테스트 준비/실행/검증&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;테스트는 arrange/act/assert 즉, 준비/실행/검증 단계로 구조화되어 작성됩니다. 코드의 가독성을 높이고 어떤 부분에서 테스트가 실패했는지 명확하게 파악할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;특히 여러 테스트 케이스를 작성할 때 일관성을 유지하고 테스트의 목적을 명확히 전달하는 것에 도움을 줄 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;test('should perform a GET request on the given URL', () async {
  // arrange
  when(mockDioClient.get(any)).thenAnswer(
    (_) async =&amp;gt; Response(data: jsonEncode(todoListJson), statusCode: 200),
  );

  // act
  await dataSource.fetchRemoteTodos();

  // assert
  verify(mockDioClient.get('&amp;lt;https://api.example.com/todos&amp;gt;'));
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;dataSource에서 get요청을 진행할 경우에 대한 테스트를 진행하고 있으므로, 목적에 대한 각 arrange/act/assert는 다음과 같은 모습으로 설계할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  arrange&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&quot;when&quot; 그리고 &quot;thenAnswer&quot; 메소드를 활용하여 HTTP 요청과 요청에 대한 응답을 설정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;설정된 값의 반환은 Response 객체를 생성하여 data와 statusCode를 할당합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  thenAnswer&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;thenAnswer 메소드는 mockito 라이브러리에서 제공하는 기능입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;정형화된 코드는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;when(mockObject.methodName(argument)).thenAnswer((_) async =&amp;gt; result);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  act&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;실제 dataSource에서 get요청을 담당하는 함수를 실행합니다. 결과가 존재하거나, 에러가 발생할 것으로 예상하는 경우 새로운 변수에 결과를 할당합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  assert&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&quot;expact&quot;와 &quot;verify&quot; 메소드를 활용해 테스트가 예상대로 결과를 반환했는지 검증합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  expact&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;flutter_test 라이브러리에 속한 메소드로 예상 결과를 검증할 때 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;actual (테스트 대상)과 matcher (기대 결과 값)을 매개변수로 가지며, 조건을 만족하지 않을 시 테스트를 실패로 표기합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  verify&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;mockito 라이브러리에 속한 메소드로 Mock 객체의 메소드 호출을 검증할 때 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  테스트 그룹&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;여러개의 테스트를 그룹화 할 수 있습니다. 관련된 테스트 케이스를 묶어서 Test Suite를 형성하고, 코드의 특정 부분이나 기능에 대한 다양한 측면을 검증하기 용이하게 만들어줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;보통 가독성 향상/유사한 행위에 대한 통합 검증/효율적인 테스트 수행/코드 수정 용이성에 대한 이점을 얻을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&quot;group&quot; 메소드를 활용하고 전반적인 테스트를 통합하여 표현할 수 있는 문구 또는 함수명을 작성하는 것이 보편적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;전체 소스코드는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;class MockDioClient extends Mock implements Dio {}

void main() {
  late TodoRemoteDataSource dataSource;
  late MockDioClient mockDioClient;

  setUp(() {
    mockDioClient = MockDioClient();
    dataSource = TodoRemoteDataSourceImpl(dio: mockDioClient);
  });

  group('fetchRemoteTodos', () {
    final List&amp;lt;map&amp;lt;string, dynamic=&quot;&quot;&amp;gt;&amp;gt; todoListJson = [
      {&quot;id&quot;: 1, &quot;title&quot;: &quot;Todo 1&quot;},
      {&quot;id&quot;: 2, &quot;title&quot;: &quot;Todo 2&quot;},
    ];

    final List todoList = todoListJson
        .map((json) =&amp;gt; TodoModel.fromJson(json))
        .toList();

    test('should perform a GET request on the given URL', () async {
      // arrange
      when(mockDioClient.get(any)).thenAnswer(
        (_) async =&amp;gt; Response(data: jsonEncode(todoListJson), statusCode: 200),
      );

      // act
      await dataSource.fetchRemoteTodos();

      // assert
      verify(mockDioClient.get('&amp;lt;https://api.example.com/todos&amp;gt;'));
    });

    test('should return a list of TodoModel when the response code is 200', () async {
      // arrange
      when(mockDioClient.get(any)).thenAnswer(
        (_) async =&amp;gt; Response(data: jsonEncode(todoListJson), statusCode: 200),
      );

      // act
      final result = await dataSource.fetchRemoteTodos();

      // assert
      expect(result, equals(todoList));
    });

    test('should throw a DioError when the response code is not 200', () {
      // arrange
      when(mockDioClient.get(any)).thenAnswer(
        (_) async =&amp;gt; Response(data: 'Not Found', statusCode: 404),
      );

      // act
      final call = dataSource.fetchRemoteTodos;

      // assert
      expect(() =&amp;gt; call(), throwsA(isA()));
    });
  });
}&lt;/code&gt;&lt;/pre&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;pre class=&quot;bash&quot;&gt;&lt;code&gt;flutter test test/data/datasources/todo_remote_data_source_test.dart 
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 테스트 케이스에 대한 결과와 통과 여부에 대한 정보가 터미널에 출력됩니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;00:10 +3: All tests passed!
00:10 +3 -1: Some tests failed.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 결과는 시간/개수/내용으로 구분되어 표기되며 성공한 테스트는 &quot;+&quot;, 실패한 테스트는 &quot;-&quot;로 표기됩니다.&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;테스트 코드에서 print 함수를 활용하여 명시적으로 작성한 내용과 함께 실패의 원인이 되는 에러 메세지가 터미널에 표기되므로, 참고하여 테스트 준비/실행/검증 단계를 반복해가면 됩니다.&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/238</guid>
      <comments>https://malangdidoo.tistory.com/238#entry238comment</comments>
      <pubDate>Sat, 3 Feb 2024 00:12:01 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] TDD (Test Driven Development)</title>
      <link>https://malangdidoo.tistory.com/237</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;Test Driven Development라 불리는 소프트웨어 개발 방법론 중 하나 입니다. 개발자가 코드를 작성하기 전에 테스트 케이스를 먼저 작성하고, 그 후에 해당 테스트 케이스를 통과하는 코드를 작성하는 방식으로 진행됩니다. 진행 과정은 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Test: 테스트 작성&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;새로운 기능이나 수정된 기능에 대한 테스트 케이스를 작성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이 테스트는 아직 구현되지 않은 기능이나 버그가 있는 부분을 검증하는 역할을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Fail: 테스트 실패&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;작성한 테스트가 현재 코드에서 실패하도록 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;아직 구현되지 않았기 때문에 당연한 결과입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Code: 코드 작성&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;실패한 테스트 코드가 통과하도록 재 작성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;목표는 테스트를 통과하는 최소한의 코드를 작성하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Pass: 테스트 통과&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;작성한 테스트가 통과하는지 판단합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Refactor: 리팩토링&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;작성한 코드를 리팩토링하여 코드의 가독성, 유지보수성, 성능 등을 개선합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;개선된 코드 또한 테스트는 통과해야 합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  테스트 라이브러리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Flutter Framework에서 테스트를 진행할 땐, flutter_test와 mockito를 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  flutter_test&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Flutter 애플리케이션의 단위 테스트와 통합 테스트를 작성하고 실행하기 위한 도구를 제공합니다. 비동기 테스트, Widget 검색, Mocking 등 다양한 테스트도 작성할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  mockito&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Mock 객체를 생성하고 테스트에서 외부 의존성을 가짜로 대체하여 테스트를 용이하게 만드는 도구입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;데이터베이스나 네트워크 호출 같은 외부 의존성이 있는 코드를 테스트할 때, 의존성을 Mock 객체로 대체하여 테스트를 진행할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;두 라이브러리를 활용하여 전체적인 테스트 흐름을 설계하고 구현해갈 수 있습니다. 전체적인 흐름으로는 객체 선언 및 초기화, 더미 데이터 생성, 테스트 선언/준비/실행/검증, 테스트 종료를 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;관련된 예시로는 TodoList를 구현할 때 사용되는 각 계층의 entity, datasource, repository, usecase, bloc에 대한 전체적인 테스트 케이스를 어떻게 설계하고 만들어가는지 흐름에 맞춰서 보여드리도록 하겠습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  테스트 코드 작성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  파일 구조&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;테스트 코드를 작성하기 전, TDD를 진행하기 위한 전체적인 파일 구조는 다음과 같습니다. Clean Architecture를 접목하여 core/data/domain/presentation 계층으로 나뉘어져있습니다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;lib
├── core
│   ├── error
│   │   └── failure.dart
│   └── usecases
│       └── todo_use_case.dart
│
├── data
│   ├── datasources
│   │   └── todo_remote_data_source.dart
│   └── repositories
│       └── todo_repository.dart
│
├── domain
│   ├── entities
│   │   └── todo.dart
│   ├── repositories
│   │   └── todo_repository.dart
│   └── usecases
│       └── todo_use_case.dart
│
├── presentation
│   ├── blocs
│   │   └── todo_bloc.dart
│   ├── pages
│   │   └── todo_list_page.dart
│   └── widgets
│       └── todo_list_widget.dart
│
└── main.dart
test
├── data
│   └── datasources
│       └── todo_remote_data_source_test.dart
├── domain
│   ├── repositories
│   │   └── todo_repository_test.dart
│   └── usecases
│       └── get_todo_list_test.dart
└── presentation
    └── blocs
            └── todo_bloc_test.dart&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;테스트가 진행 될 각 항목에 대한 글이 길어지고, 같은 구조를 쌓아올라가도 내부의 설계는 다르게 작성되므로 새로운 칼럼에서 내용을 진행하도록 하겠습니다.&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/237</guid>
      <comments>https://malangdidoo.tistory.com/237#entry237comment</comments>
      <pubDate>Fri, 2 Feb 2024 20:12:56 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] MVVM</title>
      <link>https://malangdidoo.tistory.com/236</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;Model, View, ViewModel로 구성하는 소프트웨어 아키텍처 디자인 패턴입니다. 사용자 인터페이스(UI)와 비즈니스 로직을 효과적으로 분리하기 위해 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;MVVM 디자인 패턴의 가장 큰 특징은 양방향 데이터 바인딩 방식에 있습니다. Model의 변화가 자동으로 View에 반영되고, View의 사용자 입력이 자동으로 ViewModel과 Model에 전달되어 일관된 상태를 유지할 수 있습니다. 그럼으로써, 코드의 재사용성과 유지보수성을 향상시킬 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;또한, ViewModel의 재사용성으로 인해 코드 중복을 최소화 하고 모듈성을 증가시킬 수 있습니다. ViewModel은 View와 Model의 중간 매개체로서 비즈니스 로직에 대한 단위 테스트에 용이합니다. 덕분에 View 또한, UI 테스트에 집중할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Model&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;데이터와 비즈니스 규칙을 관리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;데이터의 상태를 유지하고 조작하며, 데이터베이스 및 외부 API와 상호작용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;애플리케이션의 상태와 동작을 관리하며, 데이터의 유효성 검사와 업데이트를 처리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  View&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;사용자에게 직접적으로 보여주는 인터페이스를 담당합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;사용자의 입력을 받아 ViewModel에 전달하고, Model의 변경 사향을 반영하여 화면을 갱신합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  ViewModel&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;View에 표시할 데이터를 제공하거나 포맷, 가공하는 역할을 진행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;View에서 사용자 입력을 받아 Model에 전달해줍니다. 이렇게 View와 Model 사이의 의존성을 끊어주어 View를 독립적으로 만들어줍니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  구현&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;MVVM 디자인 패턴과 Flutter를 활용해서 TodoList에 대한 예제를 구현하여, 실질적으로 MVVM 디자인 패턴에 해당하는 항목은 무엇인지 살펴보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Model&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;class Todo {
  final int id;
  final String title;
  final bool isCompleted;

  Todo({
    required this.id,
    required this.title,
    required this.isCompleted,
  });

  factory Todo.fromJson(Map&amp;lt;String, dynamic&amp;gt; json) {
    return Todo(
      id: json['id'],
      title: json['title'],
      isCompleted: json['completed'],
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  ViewModel&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;외부 API를 활용해 todoList를 갱신하고, 사용자 정의 함수 add/remove/toggle를 활용해 비즈니스 로직을 구현합니다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;final todoViewModelProvider = AsyncNotifierProvider&amp;lt;TodoViewModel, List&amp;lt;Todo&amp;gt;&amp;gt;(() {
  return TodoViewModel();
});

class TodoViewModel extends AsyncNotifier&amp;lt;List&amp;lt;Todo&amp;gt;&amp;gt; {
  Future&amp;lt;List&amp;lt;Todo&amp;gt;&amp;gt; _fetchTodo() async {
    final json = await http.get('api/todos');
    final todos = jsonDecode(json) as List&amp;lt;Map&amp;lt;String, dynamic&amp;gt;&amp;gt;;
    return todos.map(Todo.fromJson).toList();
  }

  @override
  Future&amp;lt;List&amp;lt;Todo&amp;gt;&amp;gt; build() async {
    return _fetchTodo();
  }

  Future&amp;lt;void&amp;gt; addTodo(Todo todo) async {
    state = const AsyncValue.loading();
    state = await AsyncValue.guard(() async {
      await http.post('api/todos', todo.toJson());
      return _fetchTodo();
    });
  }

  Future&amp;lt;void&amp;gt; removeTodo(String todoId) async {
    state = const AsyncValue.loading();
    state = await AsyncValue.guard(() async {
      await http.delete('api/todos/$todoId');
      return _fetchTodo();
    });
  }

  Future&amp;lt;void&amp;gt; toggle(String todoId) async {
    state = const AsyncValue.loading();
    state = await AsyncValue.guard(() async {
      await http.patch(
        'api/todos/$todoId',
        &amp;lt;String, dynamic&amp;gt;{'completed': true},
      );
      return _fetchTodo();
    });
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  View&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;class TodoListView extends ConsumerWidget {
  const TodoListView({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final asyncTodos = ref.watch(todoViewModelProvider);

    return switch (asyncTodos) {
      AsyncData(:final value) =&amp;gt; ListView(
          children: [
            for (final todo in value)
              CheckboxListTile(
                value: todo.completed,
                onChanged: (value) {
                  ref.read(todoViewModelProvider.notifier).toggle(todo.id);
                },
                title: Text(todo.description),
              ),
          ],
        ),
      AsyncError(:final error) =&amp;gt; Text('Error: $error'),
      _ =&amp;gt; const Center(child: CircularProgressIndicator()),
    };
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;MVVM 디자인 패턴을 적용한다면, 상태 관리 라이브러리를 활용하던 부분이 ViewModel로써 활용된다고 생각하면 편하다. 그러나, MVVM 디자인 패턴을 단독으로 사용했을 때, 외부 API와 연동하고 관리하는 부분을&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;ViewModel이 강력하게 의존하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그렇기 때문에, 시스템 아키텍처와 디자인 패턴을 혼합하여 가져올 수 있는 이점들을 충분히 가져오려는 것 같다.&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/236</guid>
      <comments>https://malangdidoo.tistory.com/236#entry236comment</comments>
      <pubDate>Wed, 31 Jan 2024 19:09:55 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] Clean Architecture</title>
      <link>https://malangdidoo.tistory.com/235</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;  Clean Architecture?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Robert C. Martin이 제시한 아키텍처 패턴입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;소프트웨어 시스템을 독립적인 계층으로 분리하고, 각 계층 간의 의존성을 최소화하여 유지보수성, 테스트 용이성, 확장성을 향상시키기 위해 만들어 졌습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  계층&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;소프트웨어 시스템을 안정적이고 유연하며 변화에 적응하기 쉽게 만드는 것을 목표로 Entity, Use Case, Interface Adapter, Framework and Drivers 계층으로 구성되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Entity&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;핵심 비즈니스 규칙과 데이터를 포함하는 가장 안쪽 계층입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;애플리케이션의 핵심 데이터 구조를 포함하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Use Case&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;비즈니스 규칙과 로직을 구현하는 계층입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Entity에 접근하여 특정 비즈니스 기능을 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Interface Adapter&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;외부와 통신을 담당하는 계층입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;데이터의 변환 및 형식을 조정하여, Use Case와 외부의 결합을 진행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;UI, Database 및 외부 서비스 등과 상호작용 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Framework and Driver&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;가장 바깥쪽 계층입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Framework 또는 Library, Databse 등과 연결을 관리하며, 구체적인 기술 구현이 이루어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;위 계층 구조는, 의존성 규칙에 따라 하위 레이어에 대한 의존성만 허용하도록 설계되어 있습니다. 그로 인해 코드의 유지보수와 테스트를 쉽게 진행할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  의존성 규칙&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;의존성 규칙의 가장 기본은 안쪽 계층이 바깥쪽 계층에 종속되면 안된다는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;즉, 안쪽 계층이 변경되어도 바깥쪽 계층에는 아무런 영향을 미치지 않도록 설계하여 시스템의 안정성을 높입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  의존성 역전 원칙&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;고수준 모듈은 저수준 모듈에 의존해서는 안되며, 양쪽 모두 추상화에 의존해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;추상화를 통해 의존성을 역전 시키면, 시스템을 더 유연하고 확장 가능하게 만들 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  의존성 주입 (Dependency Injection)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;고수준 모듈이 필요로 하는 저수준 모듈을 외부에서 주입받는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;의존성이 외부에서 주입되므로 고수준 모듈은 구체적인 구현이 아닌 추상화에 의존할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  서비스 로케이터 (Service Locator)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;애플리케이션 전반에서 서비스를 제공하는 서비스 로케이터를 활용하여, 고수준 모듈이 필요로 하는 서비스를 얻는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;서비스 로케이터를 활용해 고수준 모듈이 필요로 하는 정보를 제공 받으므로, 고수준 모듈이 추상화에 의존할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  의존성 교환 원칙&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;고수준 정책이 저수준 세부사항에 의존하지 않도록 인터페이스를 활용하여 의존성을 교환 가능하게 만들어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;의존성 교환을 통해 안쪽 계층의 구현을 분리하여 테스트 용이성을 향상시키고, 변경을 쉽게 진행할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  안정된 추상화 원칙&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;안정된 인터페이스와 추상화를 설계해야 합니다. 시스템이 변화에 대응하기 쉬워지도록 하고, 안정성을 유지하는 데 도움이됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  경계를 향한 지향성 원칙&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;시스템의 구성 요소는 일반적인 방향으로 진행되어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;안쪽 계층에서 바깥쪽 계층으로 의존성이 향하도록 만드는 것이 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이런 규칙과 원칙에 따라 Clean Architecture는 견고하고, 유지보수 가능한 구조를 갖출 수 있습니다. 또한, 계층을 구분하고 계층 간의 의존성을 관리하여 유연성과 확장성을 확보합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  파일 구조&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Entity, Use Case, Interface Adpater, Framework and Driver 계층과 관련한 파일들을 어떤 구조로 쌓아올려야 하는지 처음부터 감을 잡긴 어렵습니다. 여기서 제시하는 가이드는 가장 기초적이고 대중적으로 사용되는 구조로서, 개발 팀 마다 다른 모습의 구조를 가지고 있을 수 있으니 유연하게 학습하길 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;관련 파일 구조는 TodoList를 구현하는 것에 초점을 맞추어 아래 구현 항목과 일치하도록 생성하겠습니다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;lib
├── core
│   └── error
│       └── failure.dart
│
├── data
│   ├── datasources
│   │   └── todo_remote_data_source.dart
│   └── repositories
│       └── todo_repository_impl.dart
│
├── domain
│   ├── entities
│   │   └── todo.dart
│   ├── repositories
│   │   └── todo_repository.dart
│   └── usecases
│       └── todo_use_case.dart
│
├── presentation
│   ├── blocs
│   │   └── todo_bloc.dart
│   ├── pages
│   │   └── todo_list_page.dart
│   └── widgets
│       └── todo_list_widget.dart
│
└── main.dart&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  구현&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Entity, Use Case, Interface Adapter, Framework and Driver 계층을 활용하여 Flutter에서 각 단계별 구현 방법은 다음과 같습니다. 구현은 TodoList를 활용한 기본적인 예시를 들어드리겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Entity&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;class Todo {
	final String id;
	final String title;
	final bool isCompleted;

	Todo({required this.id,
		required this.title,
		this.isCompleted = false,
	)};

	Todo copyWith({ ... });
	factory Todo.formJson() {};
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Use Case&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;외부 API로부터 데이터를 받아온다면, Use Case가 어떤 방식으로 데이터를 가져오는지 추상화를 진행하는 것이 좋습니다. 추상화를 통해 유지보수성과 확장성을 향상시키며, 의존성을 더욱 역전시킬 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Repository&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Framework and Driver 계층의 역할을 하는 Repository를 추가한다면, Use Case가 Repository 인터페이스에만 의존하게 만들어 외부 API로부터 데이터를 가져오는 방식을 더 유연하게 변경할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;아래 방식과 같이 외부 API로부터 데이터를 가져오는 Repository를 구현한다면, 이 Repository는 Framework and Driver 계층에 속합니다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;abstract class TodoRepository {
	Future&amp;lt;List&amp;lt;Todo&amp;gt;&amp;gt; getTodos();
	Future&amp;lt;Todo&amp;gt; addTodo(Todo newTodo);
}

class TodoRepositoryImpl implements TodoRepository {
	@override
  Future&amp;lt;List&amp;lt;Todo&amp;gt;&amp;gt; getTodos() async {
    final response = await http.get(Uri.parse('url'));

    if (response.statusCode == 200) {
      final List&amp;lt;dynamic&amp;gt; todoData = json.decode(response.body);
      return todoData.map((json) =&amp;gt; Todo.fromJson(json)).toList();
    } else {
      throw Exception('Failed to load todos');
    }
  }

	@override
  Future&amp;lt;Todo&amp;gt; addTodo(Todo newTodo) async {
    final response = await http.post(
      Uri.parse('url'),
      headers: {'Content-Type': 'application/json'},
      body: json.encode({'title': newTodo.title, 'completed': newTodo.isCompleted}),
    );

    if (response.statusCode == 201) {
      final Map&amp;lt;String, dynamic&amp;gt; todoData = json.decode(response.body);
      return Todo.fromJson(todoData);
    } else {
      throw Exception('Failed to add todo');
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;위 방법과 같이 Repository에 외부 API와 연결되는 코드를 직접 작성할 수 있지만, DataSource를 활용하여 외부 API에 대한 구현을 분리하고 DataSource로부터 의존성을 주입받아 활용할 수 있는 방법도 존재합니다. 단, 이 경우 간단한 UseCase와 Repository를 구현한다면 역할이 중첩되기 때문에 UseCase에서 정확한 비즈니스 로직을 처리하고, Repository에서는 데이터 엑세스에 대한 책임만을 구현해야 하는 것이 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이후 구현한 Repository를 활용해 UseCase를 구현한 모습은 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Use Case Class를 구현할 땐, 외부 API와의 연결을 구현한 Repository를 사용한다는 목적에 집중하면 됩니다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;abstract class TodoUseCase {
  Future&amp;lt;List&amp;lt;Todo&amp;gt;&amp;gt; fetchTodos();
  Future&amp;lt;void&amp;gt; addTodo(Todo newTodo);
}

class TodoUseCase {
  final TodoRepository _todoRepository;

  TodoUseCase(this._todoRepository);

  Future&amp;lt;List&amp;lt;Todo&amp;gt;&amp;gt; fetchTodos() async {
    try {
      return await _todoRepository.getTodos();
    } catch (e) {
      print('Error fetching todos: $e');
      rethrow;
    }
  }

  Future&amp;lt;void&amp;gt; addTodo(Todo newTodo) async {
    try {
			await _todoRepository.addTodo(newTodo);
    } catch (e) {
      print('Error adding todo: $e');
      rethrow;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  State Management&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;프레임워크에서 사용하는 여러 상태의 경우 전역으로 관리합니다. 여기서는 Riverpod의 Notifier와 Provider를 사용하여 상태를 관리하고, Use Case를 직접적으로 사용한다고 생각하면 좋습니다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;final asyncTodosProvider = AsyncNotifierProvider&amp;lt;AsyncTodosNotifier, List&amp;lt;Todo&amp;gt;&amp;gt;(() {
  return AsyncTodosNotifier();
});

class AsyncTodosNotifier extends AsyncNotifier&amp;lt;List&amp;lt;Todo&amp;gt;&amp;gt; {
	final TodoUseCase _todoUseCase;

  @override
  Future&amp;lt;List&amp;lt;Todo&amp;gt;&amp;gt; build() async {
    return _todoUseCase.fetchTodos();
  }

	Future&amp;lt;void&amp;gt; getTodos() async {
		state = const AsyncValue.loading();
		state = await AsyncValue.guard(() async {
			await _todoUseCase.fetchTodos();
    });
  }

**  Future&amp;lt;void&amp;gt; addTodo(Todo todo) async {
    state = const AsyncValue.loading();
    state = await AsyncValue.guard(() async {
			await _todoUseCase.addTodo(state, todo);
      return _todoUseCase._fetchTodos();
    });
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Interface Adatper&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Flutter를 활용하여 Todo의 List를 보여주는 UI를 제작합니다.&lt;/p&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;class TodoListScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final AsyncValue&amp;lt;List&amp;lt;Todo&amp;gt;&amp;gt; todos = ref.watch(asyncTodosProvider);

    return todos.when(
      loading: () =&amp;gt; CircularProgressIndicator(),
      error: (error, stack) =&amp;gt; Text('Error: $error'),
      data: (todoList) {
        return ListView.builder(
          itemCount: todoList.length,
          itemBuilder: (context, index) {
            final todo = todoList[index];

            return ListTile(
              title: Text(todo.title),
              trailing: Checkbox(
                value: todo.isCompleted,
                onChanged: (_) {
                  ref.read(asyncTodosProvider.notifier).toggleTodoCompletion(todo.id);
                },
              ),
            );
          },
        );
      },
    );
  }
}

void main() {
  runApp(ProviderScope(
    child: MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('Todo List')),
        body: TodoListScreen(),
      ),
    ),
  ));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이런 과정을 기반으로 Entity, Use Case(+State Management), Interface Adapter(UI), Framework &amp;amp; Driver(+Repository) 계층에 대한 직접적인 구현을 진행해 보았습니다.&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/235</guid>
      <comments>https://malangdidoo.tistory.com/235#entry235comment</comments>
      <pubDate>Tue, 30 Jan 2024 19:30:31 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] (Project) MapleApp: 30. 리젝</title>
      <link>https://malangdidoo.tistory.com/234</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;간단하게 통과할 줄 알았던 애플리케이션이 비공개 테스트 진행을 거부 받았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;관련된 내용은 정책 적용 범위 정책 위반이었다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  정책 적용 범위 정책 위반&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앱의 스토어 등록정보에 사용 권한이 없을 수 있는 단어, 문구, 이미지, 동영상이 포함되어 있는 것 같습니다. 앱의 스토어 목록에서 이 문제가 식별된 위치에 대한 자세한 내용은 아래의 '문제 세부정보'를 참조하세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;나는 설명 문구 란에 포함되면 안되는 단어가 포함된 것 같다. 그 문구가 무엇인지는 정확하게 통보되지 않았다. 하지만 문제에 대한 해결을 진행하는 방법은 두 가지로 추가 제시해주었기 때문에 그 방법을 따라가려 해본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  앱이 규정을 준수하도록 하려면 다음 단계를 따르세요&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이 콘텐츠 사용 허가에 관한 서면 문서를 제출한 경우 사전 통지 양식 제출에 대한 응답을 기다려 주십시오.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  내 앱의 콘텐츠를 사용할 수 있는 권한이 있으면 어떻게 되나요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앱 내 콘텐츠 사용 권한이 있다는 증거나 기타 법적 정당성 등 사용에 대한 정당성이 있는 경우 사전 통지 양식을 통해 확인 서류를 제출하세요.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://support.google.com/googleplay/android-developer/contact/adv_note&quot;&gt;https://support.google.com/googleplay/android-developer/contact/adv_note&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;근데 위 두 내용은 내가 애플리케이션에서 사용한 모든 자료와 일치하지 않았다. 그래서 스토어 리젝에 대한 다양한 정보를 얻기 위해 돌아다녔고, 결국 끝은 공식문서에 다다랐다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  스토어 등록정보 권장사항&lt;/h4&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://support.google.com/googleplay/android-developer/answer/13393723?hl=ko&quot;&gt;https://support.google.com/googleplay/android-developer/answer/13393723?hl=ko&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ video reference : &lt;a href=&quot;https://support.google.com/googleplay/android-developer/topic/9877064?sjid=9411679766893353207-AP&quot;&gt;https://support.google.com/googleplay/android-developer/topic/9877064?sjid=9411679766893353207-AP&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;사용자는 스토어 등록정보를 통해 앱이나 게임에 관해 알아보고 사용해 볼지 결정합니다. 중요한 정보를 전달하는 설명을 잘 작성하면 앱과 플랫폼 모두에서 사용자에게 양질의 경험을 제공할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;글을 읽다 보니, 릴리즈 노트가 아닌 내가 출시할 앱의 설명과 자세한 설명에 대해 문제가 있는 것임을 확인했습니다. 그래서 비공개 테스트 내 버전의 릴리즈 노트로 찾아가는 것이 아닌, 게시 개요 내 기본 스토어 등록정보를 찾아가 간단한/자세한 설명에 대해 재 작성을 시도했습니다. 재 작성은 아래 제시되는 스토어 등록 정보 권장사항에 맞춰 진행했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  앱 설명&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  기능 및 콘텐츠를 정확하게 설명해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  장점을 강조합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;흥미로운 사실을 포함하여 사용자가 앱의 장점을 이해하도록 돕습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  간단 명료한 설명을 유지합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  명확한 출처를 제공하세요&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앱 설명에 제공자가 표시되지 않거나, 익명인 사용자 후기를 포함해서는 안됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  모든 연령대에 적합하도록 작성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  욕설/저속한 언어 및 특정 고객에게 부적절한 언어 사용은 피합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  지나치게 길거나, 자세하거나, 단어가 반복되면 안됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  다른 사용자 및 앱과 관계를 오해할 만한 단어 및 문구를 사용하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  사람/동물에 관한 폭력 및 폭력적인 위협을 생생하게 묘사한 설명은 사용하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  스토어 실적 및 순위를 나타내는 텍스트를 포함하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이제 나에게는 다시 기다리는 일만 남았다.&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/234</guid>
      <comments>https://malangdidoo.tistory.com/234#entry234comment</comments>
      <pubDate>Thu, 25 Jan 2024 15:11:50 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] (Project) MapleApp: 29. 배포 전 준비</title>
      <link>https://malangdidoo.tistory.com/233</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;이번 칼럼은 배포 전 해야 할 일에 대해서 작성하겠다. 개인적으로 준비한 프로젝트이며, 실질적으로 appbundle을 준비하는 것 이외의 모든 부분이 개발자 밖의 영역이라고 생각되고, 또 그렇게 진행되는 모습을 알아갈 수 있었다. 하지만, 앱 출시를 준비하고 등록하는 모든 과정을 직접 경험해봐야 한다는 생각에 이 글을 준비했다. 물론 모든 내용을 전달해 드릴 순 없지만, 큰 문제 없이 진행되는 모습을 알려드리려 한다. 문단 첫 마디에 사용된 아이콘의 경우             순서이므로, 글의 흐름에 영향을 크게 미치진 않지만 토끼나 당근이 보인다면 새로운 문단이 시작됨을 알아주셨음한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;또한, 이 글은 내 개인적으로 배포 전 해야할 일에 대해 정리하여 지속적으로 확인하기 위한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  프로젝트 이름 변경&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;일단 프로젝트의 이름을 변경해야 한다. com.example 패키지를 사용하는 경우, Play Console에서 appbundle을 등록할 때 반려당할 수 있으니 꼭 변경해주자. 변경해야 할 파일의 위치는 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  lib/pubspec.yaml&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  name&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  description&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  android/app/src&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  main/kotlin/com/example/의 folder/package name&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  main/AndroidManifest.xml&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;android:label&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  build.gradle&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;android의 namespace&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;android의 defaultConfig의 applicationId&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  ios/Runner/info.plist&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;중간 중간에 기존 project 기본 이름을 변경된 이름으로 수정&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  README&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;기존 proejct 기본 이름을 변경된 이름으로 수정&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  저작권에 위배되지 않는 아이콘 설정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  android ic_launcher.png 생성&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://www.appicon.co/&quot;&gt;https://www.appicon.co/&lt;/a&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  지적 재산권 저작권 표시&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  데이터&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  서체&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  아이콘 및 이미지&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;애플리케이션 내 이미지 명 + 플랫폼 + 저작자에 대한 내용을 기술할 것&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  Android App build/release&lt;/h4&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://docs.flutter.dev/deployment/android&quot;&gt;https://docs.flutter.dev/deployment/android&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;안드로이드 앱을 빌드 하고 출시하기 전 확인해야 할 사항입니다. reference를 참고하여 자신이 숙지할 때 까지 더 자세히 알아보는 것을 추천합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  adding launcher icon&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Flutter 앱이 생성될 때, 기본 런처 아이콘이 표시됩니다. 애플리케이션을 나타낼 수 있는 런처 아이콘을 먼저 설정해야 합니다. 자동으로 맞춤 설정을 하고 싶으면 &lt;a href=&quot;https://pub.dev/packages/flutter_launcher_icons&quot;&gt;flutter_launcher_icons&lt;/a&gt; library를 확인하시고, 수동으로 진행하는 경우 다음과 같이 진행할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Material Design Icon의 지침 준수하기&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://m3.material.io/styles/icons&quot;&gt;https://m3.material.io/styles/icons&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  {proejctName}/android/app/src/main/res&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://www.appicon.co/&quot;&gt;https://www.appicon.co/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;경로의 지정된 폴더, mipmap- 에 아이콘 파일을 배치합니다. 각 해상도에 맞는 ic_launcher icon을 만들기 위해선 위 reference를 사용하세요&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  enabling material component&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;애플리케이션이 FlatformView를 사용하는 경우, Android 시작하기 가이드에 따라 Material 구성 요소를 활성화 시킬 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Android Material 종속성 추가&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;lt;my-app&amp;gt;/android/app/build.gradle에서 Android Material에 대한 dependency를 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;dependencies {
    // ...
    implementation 'com.google.android.material:material:&amp;lt;version&amp;gt;'
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;최신 버전을 확인하려면 GoogleMaven을 방문하세요&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  테마 적용&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;밝은 테마 : &amp;lt;my-app&amp;gt;/android/app/src/main/res/values/styles.xml&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;어두운 테마 : &amp;lt;my-app&amp;gt;/android/app/src/main/res/values-night/styles.xml&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  signing app&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://developer.android.com/studio/publish/app-signing&quot;&gt;https://developer.android.com/studio/publish/app-signing&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Google PlayStore에 게시하려면 디지털 인증서로 앱에 서명을 해야 합니다. Android는 &quot;업로드&quot;와 &quot;앱 사인&quot;이라는 두 가지 서명 키를 사용합니다. 개발자는 &quot;업로드&quot;키로 서명된 .aab 파일을 PlayStore에 ..apk로 업로드합니다. 최종 사용자는 &quot;앱 사인&amp;rdquo;키로 서명된 .apk 파일을 다운로드 합니다. 전체 진행 과정은 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  업로드 키 및 키 저장소 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;업로드 키와 저장소를 생성하는 행위는 AndroidStudio와 PowerShell에서 진행할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://developer.android.com/studio/publish/app-signing?hl=ko#generate-key&quot;&gt;https://developer.android.com/studio/publish/app-signing?hl=ko#generate-key&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;AndroidStudio는 위 reference를 참고하여 Key를 생성할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;하지만, 우리는 Flutter를 사용하고 있으므로 AndroidStudio에서 해당 지침을 따라갈 수 없습니다. 그래서 Flutter 공식문서에서 제공하는 수행 방법을 따라가면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  KeyStore 생성&lt;/p&gt;
&lt;pre class=&quot;elm&quot;&gt;&lt;code&gt;keytool -genkey -v -keystore {path}\\upload-keystore.jks -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias upload
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;해당 명령문을 입력하면 다음과 같은 진행 절차를 겪을 수 있습니다. {path}의 경우 자신이 저장하고 싶은 곳에 자유롭게 저장해도 된다. 단, keystore파일은 비공개로 유지해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  키 저장소 비밀번호 입력/새 비밀번호 다시 입력&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  이름과 성 입력&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  조직 단위 이름 입력&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  조직 이름 입력&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;조직 단위나 조직 이름이 없는 경우 빈칸으로 제시해도 된다. 추후에 Unknown으로 표기되나, 이후 이 행위로 인해 일어날 파장이 존재한다면 어떻게 진행되는 지는 추가로 작성하겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  구/군/시 이름 입력&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  시/도 이름 입력&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  두 자리 국가 코드 입력&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;시/도/구/군/시의 경우 자유롭게 입력하고 두 자리 국가 코드는 대한민국이므로 &quot;82&quot;입력하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;모두 영어로 작성하였으나, 한글로 작성하면 어떻게 되는지 나중에 알아보겠다. 위 행위를 모두 마치면 다음과 같은 글귀가 나오게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;다음에 대해 유효 기간이 10,000일인 2,048비트 RSA 키 쌍 및 자체 서명된 인증서(SHA256withRSA)를 생성하는 중 : CN=Name, OU=Unknown, O=Unknown, L=Bucheon, ST=Gyeonggi, C=82&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  upload에 대한 키 비밀번호 입력&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Enter를 누르시면 위에서 입력한 키 저장소 비밀번호/새 비밀번호 항목에 입력한 비밀번호와 동일하게 설정됩니다. 비밀번호의 경우 추후 앱 서명시 필요하기 때문에 잊어버리지 않도록 주의하세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;※ Warning: JKS 키 저장소는 고유 형식을 사용합니다. &quot;keytool -importkeystore -srckeystore E:\keys\upload-keystore.jks -destkeystore E:\keys\upload-keystore.jks -deststoretype pkcs12&quot;를 사용하는 산업 표준 형식인 PKCS12로 이전하는 것이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;지정한 경로에 가면 keystore.jks 파일이 생성된 것을 확인할 수 있고, Flutter Project의 android 하위에 key.properties 파일을 생성하고 생성된 keystore 파일을 옮겨 놓으면 됩니다. android project는 기본적으로 local.properties 파일과 ~keystore 파일에 대해 gitignore가 적용되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  업로드 키로 앱에 서명&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;생성한 {projectName}/android/key.properties에 다음과 같이 속성 값을 삽입합니다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;storePassword=
keyPassword=
keyAlias=upload
storeFile=.\upload-keystore.jks&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;storePassword와 keyPassword는 keystore를 생성할 때 입력한 키 저장소 비밀번호와 upload에 대한 키 비밀번호를 작성하시면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  {proejctName}/android/app/build.gradle&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이제 생성한 업로드 키를 사용하도록 Android의 build.gradle에 새로 추가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  키 저장소 정보 추가&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;다음으로 key.properties에 대한 정보를 등록해야합니다.&lt;/p&gt;
&lt;pre class=&quot;ruby&quot;&gt;&lt;code&gt;def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
   keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  buildTypes 추가&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;build.gradle에 debug 모드로 작성되어 있는 부분입니다. 우리는 signingConfigs를 추가하고, release 모드로 실행할 수 있도록 다음과 같이 변경해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;signingConfigs {
   release {
       keyAlias keystoreProperties['keyAlias']
       keyPassword keystoreProperties['keyPassword']
       storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
       storePassword keystoreProperties['storePassword']
   }
}
buildTypes {
   release {
       signingConfig signingConfigs.release
   }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;다음과 같이 변경하면, 캐시된 빌드로 인해 서명 프로레스에 영향을 미칠 수도 있습니다. 다음 명령문을 통해 flutter proejct를 재구성 하세요&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;flutter clean
flutter pub get
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  shrinking code with R8&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;R8은 Google의 코드 축소기로서 APK/AAB를 build할 때 기본적으로 활성화 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  enabling multidex support&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;span&gt;&lt;/span&gt;&lt;a href=&quot;https://developer.android.com/build/multidex?hl=ko&quot;&gt;https://developer.android.com/build/multidex?hl=ko&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://docs.flutter.dev/deployment/android#enabling-multidex-support&quot;&gt;https://docs.flutter.dev/deployment/android#enabling-multidex-support&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;API version 20 이하로 targeting 하는 애플리케이션의 경우, Android의 dex제한인 64k method에 직면할 수 있습니다. 또한, flutter run 축소가 활성화 되지 않은 앱의 디버그 버전을 실행할 때도 이 문제가 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Flutter 도구는 MultiDex를 쉽게 활성화 할 수 있도록 지원합니다. 이와 관련된 내용을 진행하시려면 reference를 참고하세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  reviewing app manifest&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;{projectName}/android/app/src/main 경로에 있는 AndroidManifest.xml 파일을 검토하세요. 검토할 내용은 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  application&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앱의 최종 이름을 반영하도록 application 태그의 android:label을 편집하세요&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  uses-permission&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;애플리케이션 코드에 인터넷 엑세스가 필요한 경우 android.permission.INTERNET 권한을 추가해주세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;표준 탬플릿에는 포함되어 있지 않지만, 개발 중에는 인터넷 엑세스에 허용 가능하도록 되어있습니다. 따라서, 적용하지 않을 시 배포되는 apk에는 인터넷 권한이 적용되지 않을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  reviewing build configuration&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;{proejctName}/android/app 경로의 build.gradle을 검토하세요. 검토할 항목은 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  defaultConfig 하위 항목&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  applicationId&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;최종 고유의 applicationId를 지정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  minSdkVersion&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;기본값은 flutter.minSdkVersion으로 앱이 실행되도록 디자인한 최소 API 수준을 지정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  targetSdkVersion&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;기본값은 flutter.targetSdkVersion으로 앱이 실행되도록 디자인한 대상 API 수준을 지정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  versionCode&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;내부 버전 번호로 사용되는 양의 정수입니다. 이 숫자는 한 버전이 다른 버전보다 최신인지 확인하는 데에만 사용되며, 숫자가 높을수록 최신 버전을 나타냅니다. 사용자에게 표기되는 내용은 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  versionName&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;사용자에게 표시되는 버전 번호로 사용되는 문자열입니다. 원시 문자열 또는 문자열 리소스에 대한 참조로 지정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  buildToolsVersion&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Gralde Plugin은 Project에서 사용하는 buildtools의 기본 버전을 지정합니다. 이 옵션을 사용하여 다른 버전의 빌드 도구를 지정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  android 항목 아래&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  compileSdkVersion&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;기본값은 flutter.compileSdkVersion으로 Gradle이 앱을 컴파일 하는데 사용해야 하는 API수준을 지정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  building app for release&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Google Play Store는 AppBundle을 더 선호하지만, 아직 AppBundle이 적용되지 않는 Store는 APK를 사용한다고 합니다. 일단 두 가지 모두 정리하고, AppBundle을 활용하여 Play Store에 게시를 진행하려는 절차를 밟을 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  AppBundle 빌드&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;서명 단계를 완료하면 앱 번들이 서명됩니다.&amp;nbsp;이 시점에서는 리버스 엔지니어링을 더 어렵게 만들기 위해&amp;nbsp;Dart 코드를 난독화하는 것을&amp;nbsp;고려할 수 있습니다 .&amp;nbsp;코드를 난독화하려면 빌드 명령에 몇 가지 플래그를 추가하고 스택 추적을 난독화하기 위해 추가 파일을 유지 관리해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;flutter build appbundle
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;실행은 flutter build의 릴리즈 빌드로 설정됩니다. 앱의 릴리즈 번들은 {proejctName}/build/app/outputs/bundle/release/app.aab로 생성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;기본적으로 AppBundle에는 armeabi-v7a, arm64-v8a, x86-64용으로 컴파일 된 Dart 코드와 Flutter 런타임이 포함되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  bundletool을 활용하여 오프라인 테스트&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Google Play를 이용한 온라인 테스트&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;내부 테스트 트랙이나 알파 또는 베타 채널을 사용하여 번들을 프로덕션에 출시하기 전에 테스트할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://developer.android.com/studio/publish/upload-bundle?hl=ko&quot;&gt;https://developer.android.com/studio/publish/upload-bundle?hl=ko&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Play 스토어에&amp;nbsp;번들을 업로드하려면 위 레퍼런스를 참고하세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  APK 빌드&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;APK를 빌드할 땐, 리버스 엔지니어링을 더 어렵게 만들기 위해 Dart 코드 난독화를 고려할 수 있습니다. 코드 난독화를 위해선 build command에 몇 가지 플래그를 추가해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;flutter build apk // --release 기본 옵션 포함된, 가장 기본이 되는 빌드 방법
flutter build apk --split-per-abi
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이 명령을 실행하면 다음 3개의 APK파일이 생성됩니다.&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;[project]/build/app/outputs/apk/release/app-armeabi-v7a-release.apk
[project]/build/app/outputs/apk/release/app-arm64-v8a-release.apk
[project]/build/app/outputs/apk/release/app-x86_64-release.apk
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;플래그를 제거하면 모든 ABI 대상이 --split-per-abi에 대해 컴파일 된 코드가 포함된 큰 APK가 생성됩니다. 이 APK는 분할된 APK보다 더 크기 때문에 사용자는 기기 아키텍처에 적용할 수 없는 기본 바이너리를 다운로드하게 되므로 주의하세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  publishing to the google play store&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  updating app version number&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앱 기본 버전 번호는 1.0.0 입니다. 업데이트 하려면 pubspec.yaml으로 이동하여 version: 1.0.0+1로 업데이트 하세요. version에 작성된 1.0.0은 애플리케이션의 versionName을 의미하며, +1은 versionCode를 나타냅니다. 해당 값은 android 파일의 localproperties에 자동으로 업데이트 됩니다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://developer.android.com/studio/publish/versioning?hl=ko&quot;&gt;https://developer.android.com/studio/publish/versioning?hl=ko&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앱 버전에 대한 유지보스와 관련하여는 위 reference를 참고하세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  android release FAQ&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  Play에 업로드&lt;/h4&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://developer.android.com/studio/publish/app-signing?hl=ko#enroll&quot;&gt;https://developer.android.com/studio/publish/app-signing?hl=ko#enroll&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Play Console에 업로드 하기 위해선, Play 앱 서명에 등록해야 합니다. 이전 단계에서 발급한 upload-key를 활용하여 서명할 수 있습니다. 그럼 이제 Play Console에 진입해서 진행해야 할 단계에 대해 알아가봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Google Play Console 시작하기&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://play.google.com/console/signup&quot;&gt;https://play.google.com/console/signup&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;일단 개발자 계정으로 전환해야 합니다. Google Console이 제시하는 대로 2단계 설정을 완료하고, 다시 Google Play Console 사이트에 접속합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이후 저는 개인용 계정이므로, 개인을 선택하여 시작합니다. 이후 내 정보를 입력하고 개발자 계정을 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;개발자 계정을 생성하기 위해서는 다음 정보가 필요합니다. 항목에 알맞는 사항을 넣고, 개발자 계정 생성을 완료하세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  개발자 계정 이름&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Google Play에서 공개 될 개발자의 이름입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  담당자 이름, 연락처 이메일/주소/전화번호&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;해당 내용은 Google에서 계정에 연락해야 하기 위한 수단으로 Google Play에는 표시되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  선호 언어&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이메일로 커뮤니케이션을 진행할 때 사용 될 국가적 언어를 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이후 앱 관련한 질문에 대답합니다. 추후 수정 가능하며, 앞으로 게시할 실제 행동에 제약이 생기진 않습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ agreement : &lt;a href=&quot;https://play.google.com/about/developer-distribution-agreement.html&quot;&gt;https://play.google.com/about/developer-distribution-agreement.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이용 약관을 확인하고, 동의하여 개발자 계정을 생성합니다. 개발자 계정을 생성하는 데는 최초 1회 $25, 한화 약 3만원의 결제 금액이 생깁니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  앱 버전 준비 및 출시&lt;/h4&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://support.google.com/googleplay/android-developer/answer/9859348?hl=ko&amp;amp;visit_id=638404560066143377-1402164544&amp;amp;rd=1&quot;&gt;https://support.google.com/googleplay/android-developer/answer/9859348?hl=ko&amp;amp;visit_id=638404560066143377-1402164544&amp;amp;rd=1&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;버전을 사용하면 앱의 Android App Bundle을 관리하고 앱을 특정 트랙에 출시할 수 있습니다. 항목은 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  공개 테스트&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Google Play에서 테스테에게 제공됩니다. 사용자는 스토어 등록 정보를 통해 테스트에 참여할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  비공개 테스트&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;개발자가 선택하는 제한된 수의 테스터에게 제공되며, 이 테스터는 앱의 출시 전 버전을 테스트하고 의견을 제출할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  내부 테스트&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;개발자가 선택하는 최대 100명의 테스터에게 제공됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  프로덕션 테스트&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;선택한 국가의 모든 GooglePlay 사용자에게 제공됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;새 버전을 만드려면 앱을 테스트 트랙으로 출시할 권한이 있어야 합니다. 2023년 11월 13일 이후 개인 계정을 만든 개발자는 특정 테스트 요구사항을 충족해야 Google Play에 앱을 게시할 수 있습니다. 그 항목은 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  최소 지난 14일 동안 지속적으로 동의한 최소 20명의 테스터를 대상으로 앱에 대한 비공개 테스트를 진행해야 합니다. 이 기준을 충족해야 Play Console의 대시보드에서 프로덕션 엑세스를 신청하여 최종적으로 Google Play에 앱을 배포할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://support.google.com/googleplay/android-developer/answer/14151465?sjid=841654535308975600-AP&quot;&gt;https://support.google.com/googleplay/android-developer/answer/14151465?sjid=841654535308975600-AP&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  테스터 모집&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;누구나 참여자가 될 수 있습니다. 내 주변 지인들에게 연락하여 앱 베타 테스터가 되어 달라고 요청할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  테스터 참여&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;테스터 그룹이 확인하고 보고하는 방법에 대한 명확한 지침을 제공하는 것이 중요합니다. 어떤 유형의 피드백을 찾고 있느지 알려주고, 전체적인 피드백을 받을 수 있도록 테스터가 앱 기능을 최대한 많이 사용하도록 권장하세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Google Play를 활용하여 비공개 피드백을 제공할 수도 있습니다. 단, 테스터에게 최소 14일동안 지속적으로 비공개 테스트를 선택한 상태로 유지해야 한다는 점을 강조하세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  사용자 피드백 수집 및 보기&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Play Console에서 평가 및 리뷰 &amp;gt; 테스트 피드백으로 이동하여 관리할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  사용자 피드백에 대한 조치&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앱 테스트 기간 동안 테스터의 피드백에 응답하고 그들이 발견한 버그를 반드시 수정해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;관련된 모든 사항에 대해 자세히 알고 싶으면 위 reference를 참고하세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  새 앱 만들기&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Google Play Console에서 본인 인증을 진행하면 앱을 만들 수 있습니다. 추후 본인 인증이 확인되면 앱을 게시할 수 있습니다. 본인 인증이 확인 되기 전, 앱 만들기를 진행해보려 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  앱 세부정보 입력&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앱 이름/기본 언어/카테고리/가격/선언에 대한 앱의 세부 정보를 입력해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;선언 항목의 경우 다음 과 같은 내용에 대해 동의해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  개발자 정책&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://play.google.com/about/developer-content-policy/&quot;&gt;https://play.google.com/about/developer-content-policy/&lt;/a&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;지적 재산&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;/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;size18&quot;&gt;  기본 스토어 등록정보&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;요구하는 항목에 맞게 모든 내용을 삽입하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  비공개 테스트&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;프로덕션을 제출하기 전, 비공개 테스트 버전을 개시하고 최소 20명 이상의 테스터가 14일 이상 테스트를 진행해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  새 버전 출시하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;가장 중요하게 볼 항목은 flutter build appbundle을 통해 AppBundle을 제작할 때, Android project/app/src/main/res 경로에 있는 AndroidManifest 파일의 android/namespace를 확인해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;기본적으로 com.example.{projectName}으로 설정되어 있는데, Play Console에 업로드 될 appbundle은 com.example 사용을 지양하고 있기 때문에 허용되지 않습니다. namespace 명명 규칙에 따라 새롭게 작성하고 모든 패키지를 개선한 뒤 시도하세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;※ namespace 규칙:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션 ID는 두 개 이상의 세그먼트(한 개 이상의 점)로 구성해야 합니다.&lt;/li&gt;
&lt;li&gt;각 세그먼트는 문자로 시작해야 합니다.&lt;/li&gt;
&lt;li&gt;모든 문자는 영숫자나 밑줄[a~z, A~Z, 0~9 또는 _]이어야 합니다.&lt;/li&gt;
&lt;li&gt;애플리케이션 ID를 네임스페이스와 동일하게 유지합니다. 두 속성의 차이가 약간 혼란스러울 수 있지만 동일하게 유지한다면 아무것도 걱정할 필요가 없습니다.&lt;/li&gt;
&lt;li&gt;앱을 게시한 후에는 애플리케이션 ID를 변경하지 마세요. 변경하면 Google Play 스토어에서 후속 업로드를 새 앱으로 간주합니다.&lt;/li&gt;
&lt;li&gt;애플리케이션 ID를 명시적으로 정의합니다.&amp;nbsp;applicationId&amp;nbsp;속성을 사용하여 애플리케이션 ID가 명시적으로 정의되지 않으면 자동으로 네임스페이스와 동일한 값이 사용됩니다. 즉, 네임스페이스를 변경하면 애플리케이션 ID가 변경되는데 이는 일반적으로 바람직하지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  기존의 bundle을 올려버렸을 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Play Console에는 App Bundle 탐색기가 존재합니다. 출시를 하지 않았으면, App Bundle에 있는 항목을 삭제할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;pubspec.yaml의 version&lt;/li&gt;
&lt;li&gt;android/local.properties의 versionName/versionCode&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  기존 bundle을 유지하고, 새로운 Version의 bundle을 추가하는 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;비공개 테스트 애플리케이션의 트랙 관리에 진입하면, 우측 상단에 새 버전 만들기 버튼이 존재합니다. 이 경로를 통해 내 새로운 버전의 appbundle을 추가할 수 있고, 전체적인 관리가 가능합니다. appbundle을 추가하는 방법은 위에 작성되었던 내용과 동일합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Android 13에 광고 ID 변경사항 도입&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;기존 에뮬레이터도 API 33을 사용하고 있었기 때문에, 광고 ID에 대해 설정해주어야 한다. 내 앱 같은 경우, 광고를 도입하지 않을 것이므로 &quot;광고 ID 사용을 하지 않음&quot;을 선택하여 해결했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  게시 개요 전 안내사항&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앱의 변경 사항을 Google에 전송하여 검토를 받아야 합니다. 검토 항목은 현재까지 설정된 모든 항목에 대하여 진행됩니다. 추후 검사 결과를 파악하여 다시 작성해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  게시 개요 페이지에서 검토를 위해 변경사항을 전송할 시점을 관리하세요&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Play Console에서 앱에 적용한 대부분의 변경사항은 검토를 위해 Google에 전송하여 승인을 받은 후 게시해야 합니다. 게시 개요 페이지에서 앱 변경사항을 확인하고 준비가 되면 Google에 전송하여 검토를 받으면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  승인된 변경사항이 게시되는 시점을 관리하려면 관리형 게시를 사용 설정하세요&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;게시 개요 페이지에서 관리형 게시를 사용 설정 또는 사용 중지할 수 있습니다. 관리형 게시를 사용 설정하면 승인된 변경사항을 Google Play에 게시할 시기를 관리할 수 있습니다. 관리형 게시가 사용 중지된 경우 변경사항이 승인되는 즉시 게시됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  앱 변경사항 추적&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;언제든지 게시 개요로 이동하여 앱 변경사항의 개요를 살펴보고 변경사항이 검토를 위해 전송할 준비가 되었는지, 검토 중인지 또는 게시할 준비가 되었는지 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  테스터 모집&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Play Console에서 요구하는 모든 기본 사항을 마치면 테스터를 추가할 수 있습니다. 단, 테스터의 이메일을 추가할 때 google email만 사용 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이 모든 작업을 완료하면 Play Console에서 검토가 진행됩니다. 개시 개요를 통해 확인할 수 있으며, 상당한 기간이 소모될 수 있습니다. 테스터를 위해서 어떤 내용을 준비하고 진행해야 하는지, 14일 테스트를 위한 작업은 무엇을 해야 하는지에 대해 유지보수 측면으로 다음 칼럼에서 작성하도록 하겠습니다.&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/233</guid>
      <comments>https://malangdidoo.tistory.com/233#entry233comment</comments>
      <pubDate>Wed, 24 Jan 2024 10:13:48 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] (Project) MapleApp: 28. 예외 처리 작업-마무리</title>
      <link>https://malangdidoo.tistory.com/232</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;모든 예외를 처리했다고 생각했으니, 앞으로의 개선사항과 오류 및 버그사항은 앱 테스트가 실질적으로 이루어질 때 진행하려 한다. 그래서 이번 시간에는 정상적으로 apk가 만들어지는지 실제 폰에선 잘 다운로드하여 작동하는지 검증하는 시간을 가지려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  Build mode&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  성능분석 profile mode&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  출시 준비 release mode&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;최대 최적화 및 최소 설치 공간 크기를 원하는 경우 앱 배포를 위해 사용합니다. 모바일의 경우 릴리즈 모드는 다음을 의미합니다.(에뮬레이터에서는 지원되지 않음)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  Assertion이 비활성화 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  디버깅 정보가 제거되고, 디버깅이 비활성화 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  컴파일은 빠른 시작/실행 및 작은 패키지 크기에 최적화되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  서비스 확장이 비활성화 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;다음 명령어를 통해 컴파일할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;flutter run --release
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  APK build&lt;/h4&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://docs.flutter.dev/deployment/android#build-an-apk&quot;&gt;https://docs.flutter.dev/deployment/android#build-an-apk&lt;/a&gt;&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;flutter build apk
flutter build apk --release --target-platform=android-arm64
flutter build apk --split-per-abi
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  --release&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  --split&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  Application Test&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;가볍게 테스트하고, 중간 점검을 하기 위해 apk를 만들었다. 실제 폰에서는 어떻게 동작했는지 서술해보려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  WidgetsFlutterBinding.ensureInitialized&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://api.flutter.dev/flutter/widgets/WidgetsFlutterBinding/ensureInitialized.html&quot;&gt;https://api.flutter.dev/flutter/widgets/WidgetsFlutterBinding/ensureInitialized.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;runApp을 호출하기 전 바인딩을 초기화 해야 하는 경우 메서드를 호출합니다. 그 전 async/await가 있다면 필수적으로 진행해 주는 것도 좋은 방법이라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;저는 dotenv를 활용하고 있었기 때문에 동작에 어떻게 영향을 주는지 정확히 파악하지 않았지만, ensureInitialized의 내용을 보고 추가로 작성해 주었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  AndroidManifest&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;안드로이드에서 가장 기본이 되는 파일입니다. 사용자로 하여금 필수 권한을 요청하는 내용을 담고 있습니다. 가장 기본적으로 앱에서 http 통신을 진행할 때 필요한 속성은 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  &amp;lt;uses-permission android:name=&quot;android.permission.INTERNET&quot;/&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이 속성이 없는 경우, 애플리케이션의 http 통신은 이루어지지 않습니다. flutter project의 android/app/src 내의 debug/profile 폴더의 manifest에는 존재하지만, 애플리케이션의 기본이 되는 main 폴더의 manifest에는 존재하지 않는 구문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;따라서, http 통신이 정상적으로 작동하도록 만들기 위해 android/app/src/main/AndroidManifest.xml 폴더의 manifest tag 안 그리고 application tag 밖에 위치하도록 위 구문을 삽입해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이로써 내가 설계한 모든 부분이 정상적으로 작동함을 인지할 수 있었다. 내부 화면에서 몇 부분이 현재 화면보다 큰 경우 SingleChildScorllView가 진행되지 않는 부분은, 외부에 Conatiner를 Center로 둠으로써 해결했다. 앞으로는 배포 전 준비는 어떻게 하는지, 앱 스토에 등록은 어떻게 하는지 정리해보려한다.&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/232</guid>
      <comments>https://malangdidoo.tistory.com/232#entry232comment</comments>
      <pubDate>Tue, 23 Jan 2024 15:12:27 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] (Project) MapleApp: 27. 예외 처리 작업-6</title>
      <link>https://malangdidoo.tistory.com/231</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  navigator key 사용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그렇게 모든 로직을 Contorller로 옮겼을 때, 우리는 Notifier에 의해 ref를 자유자재로 사용할 수 있지만 Widget의 context를 사용하려면 매개변수로 넘겨주어야 한다. 하지만 이런 경우, 코드 간 응집도가 높아질 수 있기 때문에 다른 방법을 찾고 싶었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그 중 한 방법이 GoRouter의 navigatorKey를 사용하는 것이다. GlobalKey로 GoRotuer에 navigatorKey를 설정하면 key의 currentContext를 통해 가장 최근의 context를 가져올 수 있고, 그 context로 go/push 행위를 진행할 수 있다. 사용은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;GlobalKey&amp;lt;NavigatorState&amp;gt; navigatorkey = GlobalKey&amp;lt;NavigatorState&amp;gt;();

GoRouter mainRouter = GoRouter(
  navigatorKey: navigatorkey,
	...
);

navigatorkey.currentContext.go('/');
&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;b&gt;  SingleChildScrollView Scroll 위치 초기화&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Scroll에 대한 컨트롤을 하고 싶으면 ScrollController를 사용하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;※ reference : &lt;a href=&quot;https://api.flutter.dev/flutter/widgets/ScrollController-class.html&quot;&gt;https://api.flutter.dev/flutter/widgets/ScrollController-class.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 강제적으로 위치를 0으로 옮기고 싶으므로, reference에서 찾을 수 있는 jumpTo method를 활용하면 간단하게 사용할 수 있다.&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/231</guid>
      <comments>https://malangdidoo.tistory.com/231#entry231comment</comments>
      <pubDate>Mon, 22 Jan 2024 11:35:11 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] (Project) MapleApp: 26. 예외 처리 작업-5</title>
      <link>https://malangdidoo.tistory.com/230</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;  notifier 안에 stateProvider로 사용자 흐름 제어하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;입력 받은 텍스트에 대해 유효성 검사를 진행 한 후에, 모든 결과를 만족하면 다음 페이지로 넘어가도록 설정해주는 작업을 해야 했다. 그러기 위해서 현재 http request를 진행 중인지, Response는 무엇인지, Error는 없는지 확인해야 했다. 그래서 http 통신을 위한 fetch 기반 AsyncNotifier에서, 현재 페이지의 여러 상태들을 관리하기 위한 Notifier를 Controller 형식으로 설계하여 UI와 로직을 분리하기 위한 노력을 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;현재 진행 중인지를 나타내는 isLoading, 에러가 있는지 확인하는 hasError, 에러가 존재하는 경우 사용자를 위한 문구를 제공하는 errorMessage 등의 변수를 StateProvider로 Notifier안에 선언하고, Notifier를 사용하려는 페이지에서 controller로 불러와 사용하였다. 간단한 구조는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;class MainPage {
	...
	Widget build() {
		final mainController = ref.watch(mainControllerProvider.notifier)
		...
	}
}

class MainController extends Notifier {
	final isLoading = StateProvider((_) =&amp;gt; false);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;controller의 내부 변수들에 접근하기 위해 notifier까지 불러와야 하고, 내부에서 정적인 값을 사용할 때는 그냥 사용하면 되지만, 동적인 값으로 StateProvider를 주었다면 다음과 같이 활용해야 한다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;ref.watch(mainController.isLoading)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;화면과 ui로직을 분리하는 것은 코드 품질을 유지하기에 아주 좋은 행위이므로 항상 유념하길 바란다. 또한 MainPage에서 작동하는 클릭 이벤트 이외 추가 로직 함수들 모두 Controller에 작성하여 관리하는 것이 좋다.&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/230</guid>
      <comments>https://malangdidoo.tistory.com/230#entry230comment</comments>
      <pubDate>Sun, 21 Jan 2024 21:19:26 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] (Project) MapleApp: 25. 예외 처리 작업-4</title>
      <link>https://malangdidoo.tistory.com/229</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  DioException 예외 처리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;사용자의 닉네임이 정확히 입력되지 않은 경우, 서버에서 BadResponse, error 400 code를 보내왔다. 그래서 DioException의 error.type에 대한 모든 분기 처리를 진행했으나, 위 오류가 DioException으로 반환되는 것이 아닌 assureDioException을 타고 외부에서 에러를 throwing하고 있었다. 어떻게 해결해야 하나 엄청난 서칭 끝에 git issue에 등록된 글로 해결할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;가장 중요한 내용은, dio에서 전달받는 status code에서 성공하는 코드를 제외하고 나머지 에러는 인터셉터를 활용하여 DioException으로 보내버리는 행위를 해버리면 된다. dio의 상태에 대한 저장과, 인터셉터를 구현하고, Exception을 어떻게 관리하는지 까지 서술하겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  status 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;dio의 유효성 검사는 status가 null이 아닐 때만 진행하도록 설정해줍니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;dio = Dio();
dio.options.baseUrl = dotenv.get('NEXON_API_URL');
dio.options.validateStatus = (status) =&amp;gt; status != null;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  interceptor 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;dio의 interceptor를 상속 받는 class를 생성하고, Response에 대한 설정을 재 정의합니다. statusCode가 성공적인 20x코드가 아닌 모든 경우에 모든 에러 사항을 DioException의 badResponse로 재 구축하여 전달하도록 합니다. 이후 dio option의 interceptor에 customInterceptor를 할당해주면 끝입니다.&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;import 'package:dio/dio.dart';

class DioErrorInterceptor extends Interceptor {
  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    final status = response.statusCode;
    final isValid = status != null &amp;amp;&amp;amp; status &amp;gt;= 200 &amp;amp;&amp;amp; status &amp;lt; 300;
    if (!isValid) {
      throw DioException.badResponse(
        statusCode: status!,
        requestOptions: response.requestOptions,
        response: response,
      );
    }
    super.onResponse(response, handler);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이러면 서버에서 DioException을 반환하지 않고, 그 외 오류로 벗어난다 해도 인터셉터가 모든 오류를 다시 DioException의 badResponse 개체로 만들어 throwing 하므로 우리는 try/catch 구문을 통해 에러를 핸들링할 수 있게 됩니다.&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/229</guid>
      <comments>https://malangdidoo.tistory.com/229#entry229comment</comments>
      <pubDate>Sat, 20 Jan 2024 19:51:51 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] (Project) MapleApp: 24. 예외 처리 작업-3</title>
      <link>https://malangdidoo.tistory.com/228</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  TextFormField가 focus에서 focusout 상태가 됐을 때, validator 호출&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;일단 validator를 호출하기 전에 TextFormField에서 focusout 상태를 만드는 것이 중요하다. 앱 키보드는 뒤로 가기, 또는 done을 눌러야 키보드가 사라지는데 보통의 사용자는 화면의 빈 부분을 클릭하여 focus를 해제하려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://api.flutter.dev/flutter/widgets/FocusScope-class.html&quot;&gt;https://api.flutter.dev/flutter/widgets/FocusScope-class.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;FocusScope class를 활용하면 Widget에 대해 unfocus를 진행할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;두 요소를 결합하여, 전체 화면을 GesturDetector로 감싸주고 배경이 클릭 됐을 시, unfocus를 위한 method를 실행해주면 된다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;FocusScope.of(context).unfocus(),
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;여기서 중요한 점은, 전체 화면에 대해 GesturDetector가 작동해야 되기 때문에, Page Widget의 최상단에 배치되어야 한다는 것을 조심하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;하지만 나는 계속 이벤트 버블링 되는 효과가 났다. 그러나 이벤트 버블링은 JavaScript에만 존재하지 Dart에는 존재하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;결국 이유는, TextFormField에 할당하는 key가 build method 내부에서 선언되었기 때문이다. 특정 이야기로는 StatefulWidget 내부에서는 GlobalKey를 사용하지 말라 선언합니다. 조심하십시오.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  자세한 Validation 구현 예시&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;if (value == null || value.isEmpty) {
  return '닉네임을 입력해주세요.';
} else if (value.contains(RegExp(r'[^0-9a-zA-Z가-힣]'))) {
  return '올바른 닉네임을 입력해주세요.';
} else if (value.contains(RegExp(r'[0-9a-zA-Z]')) &amp;amp;&amp;amp; !value.contains(RegExp(r'[가-힣]')) &amp;amp;&amp;amp; value.characters.length &amp;lt; 4) {
  return '영문과 숫자조합 닉네임은 4글자 이상 입력해주세요.';
} else if (value.contains(RegExp(r'[0-9a-zA-Z]')) &amp;amp;&amp;amp; value.contains(RegExp(r'[가-힣]')) &amp;amp;&amp;amp; value.characters.length &amp;lt; 3) {
  return '조합된 닉네임은 3글자 이상 입력해주세요.';
} else if (value.length &amp;lt; 2) {
  return '닉네임은 2글자 이상 입력해주세요.';
}
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/228</guid>
      <comments>https://malangdidoo.tistory.com/228#entry228comment</comments>
      <pubDate>Fri, 19 Jan 2024 07:08:49 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] (Project) MapleApp: 23. 예외 처리 작업-2</title>
      <link>https://malangdidoo.tistory.com/227</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  TextFieldForm&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;아이디에 영문, 숫자, 한글을 제외하고 입력되지 않기 때문에, 정규 표현식 조건을 걸어주려 한다. 하지만 기존 TextFiled의 경우, controller에 들어오는 값에 대해 실시간으로 감지하고 오류 텍스트를 띄워주어야 했기 때문에, 예외 처리를 진행하는 동안에는 개발 비용이 비싸다고 생각했다. 그래서 TextFiled Widget을 FormFiled로 Wrapping하는 TextFiledForm Widget을 활용했다. 사용 방법은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;TextFormField(
  decoration: const InputDecoration(
    icon: Icon(Icons.person),
    hintText: 'What do people call you?',
    labelText: 'Name *',
  ),
  onSaved: (String? value) {
    // This optional block of code can be used to run
    // code when the user saves the form.
  },
  validator: (String? value) {
    return (value != null &amp;amp;&amp;amp; value.contains('@')) ? 'Do not use the @ char.' : null;
  },
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;기존의 TextFiled의 Widget이름만 바꿔도 충분히 사용 가능했다. 그 곳에 onSaved/validator 등의 원하는 속성을 추가 설정해주면 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://api.dart.dev/stable/3.2.4/dart-core/RegExp-class.html&quot;&gt;https://api.dart.dev/stable/3.2.4/dart-core/RegExp-class.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이제 validator에 우리가 하고 싶었던 정규 표현식 조건을 걸어주자. 일단 Dart의 정규 표현식에 대해 알아가기 위해 reference를 참고했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Dart의 정규 표현식은 Javscript의 정규 표현식과 동일한 구문과 의미를 갖는다. 특수문자가 존재하는 경우를 찾기 위한 방법을 찾았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  key 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;대부분의 예제가 Form과 TextFormField 그리고 Submit button을 예시로 들었다. 하지만 나는 Form이라는 필요 없는 위젯을 사용하고 싶지 않았고, 공식 문서에서 해답을 찾을 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://api.flutter.dev/flutter/material/TextFormField-class.html&quot;&gt;https://api.flutter.dev/flutter/material/TextFormField-class.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Form을 사용하고 싶지 않다면, GlobalKey&amp;lt;FormFieldState&amp;gt;를 생성자에 전달하고, GlobalKey.currentState를 사용하여 양식을 저장하거나 재 설정 하라고 한다. 즉, Form에 key를 전달하는 것이 아닌 TextFormField에 직접 key를 전달하여 활용할 수 있다는 이야기다. 설정은 다음과 같다&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;@override
  Widget build(BuildContext context) {
    late GlobalKey&amp;lt;FormFieldState&amp;gt; textFormFieldKey =
        GlobalKey&amp;lt;FormFieldState&amp;gt;();

		TextFormField(
        key: textFormFieldKey,
        decoration: const InputDecoration(),
      ),
    ),
  ),
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;위 코드처럼 key를 직접 할당할 수 있다. 그렇다면 기존 TextFormField의 validator를 어떻게 활용해야 하는가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  key.currentState.validate()&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;위에서 언급했던 GlobalKey.currentState를 통해 TextFormField에 접근할 수 있고, 내부의 validate를 실행할 수 있다. 이 validate method는 다음과 같은 특징을 가지고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  [FormField.validator]를 호출하여 [errorText]를 설정합니다. 오류가 없는 경우 true를 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;즉 내가 설정 해 놓은 유효성 검사를 통과하면 true를 반환하고, 오류가 생기면 false를 반환하면서 validator에서는 반환된 문자열을 자동으로 errorText에 할당해준다. 결과 코드는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;@override
  Widget build(BuildContext context) {
    late GlobalKey&amp;lt;FormFieldState&amp;gt; textFormFieldKey =
        GlobalKey&amp;lt;FormFieldState&amp;gt;();

        return Row(
            children: [
                TextFormField(
                    key: textFormFieldKey,
                    autofocus: true,
                    focusNode: _focusNode,
                    controller: _textFieldController,
                    decoration: const InputDecoration(),
                ),
                GestureDetector(
                    behavior: HitTestBehavior.translucent,
                    onTap: () =&amp;gt;
                        textFormFieldKey.currentState?.validate() ?? false
                            ? trueAction()
                            : _focusNode.requestFocus(),
                    child: Container(
                  	  child: Text('검색'),
                    ),
                ),
            ],
        ),
      ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;focusNode의 requestFocus method는 할당해 놓은 focusNode개체(지금은 TextFormField)에 사용자 포커싱을 진행해준다. 웹 앱 모두 유용하게 작동할 수 있다. 하위에 버튼에서 validate method를 호출하면 유효성 검사 결과에 따라 다음 행동을 정해 놓을 수 있다.&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/227</guid>
      <comments>https://malangdidoo.tistory.com/227#entry227comment</comments>
      <pubDate>Thu, 18 Jan 2024 14:52:48 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] (Project) MapleApp: 22. 예외 처리 작업-1</title>
      <link>https://malangdidoo.tistory.com/226</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;테스트를 진행하면서 예외 처리 작업 시 겪었던 내용을 적어 내려가려 한다. 작업을 진행하기 전, 테스트 항목에 대해 모두 기술하고, 그것을 기반으로 진행하면서 차근차근 변경하려 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  정보가 입력되지 않았을 때, 검색을 클릭하는 경우 TextFiled로 포커싱&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://docs.flutter.dev/cookbook/forms/focus&quot;&gt;https://docs.flutter.dev/cookbook/forms/focus&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;방법은 TextFiled에 autofocus 속성을 true로 설정하고, FocusNode class를 생성하는 것입니다. 그러면, 버튼을 클릭했을 대, FocusNode의 requestFocus method를 활용하여 강제로 TextFiled에 focusing할 수 있습니다. 자세한 예제는 reference를 참고해주세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;  페이지를 벗어났을 때, 텍스트 내 정보 삭제&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;간단하다! 우리는 TextFiled가 더 이상 필요하지 않기 때문에, textFiledController도 더 이상 사용하지 않는다. 때문에 controller의 dispose를 호출하여 사용하지 않는 모든 리소스를 삭제하면 된다!&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@override
void dispose() {
	_textFiledController.dispose();
	super.dispose();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;마찬가지로 새롭게 할당해 준 _focusNode도 dispose 시켜줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;하지만 삭제되지 않았다. 그 이유는 context에서 push로 넘어갔기 때문이다. push method의 경우 routeStack에 해당 페이지 정보를 남겨 놓은 상태이므로 dispose가 되지 않는다. 따라서 go method를 활용해 페이지를 넘어 가주도록 하자.&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/226</guid>
      <comments>https://malangdidoo.tistory.com/226#entry226comment</comments>
      <pubDate>Wed, 17 Jan 2024 11:02:04 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] (Project) MapleApp: 21. Stat Page 제작-2</title>
      <link>https://malangdidoo.tistory.com/224</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;  custom shapes&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  ClipPath&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://api.flutter.dev/flutter/widgets/ClipPath-class.html&quot;&gt;https://api.flutter.dev/flutter/widgets/ClipPath-class.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;직접 원하는 커스텀 도형을 만들어내고 싶다면, 위의 ClipPath reference를 활용해서 도전하라.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  flutter_custom_clippers&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://pub.dev/packages/flutter_custom_clippers/install&quot;&gt;https://pub.dev/packages/flutter_custom_clippers/install&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그러기에 다각형 도형을 구현하는 것에 너무 많은 비용이 든다 싶어 찾아낸 라이브러리다. 일반적으로 알려진 다양한 도형에 대해 얻고 싶다면, flutter_custom_clippers를 활용하는 것도 좋은 방법이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;flutter pub add flutter_custom_clippers
flutter pub get
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;하지만 실제로 적용해보니 내가 원하는 정 다각형의 모습을 보여주지 않았다. 그래서 그냥 직접 그리기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  CustomClipper&amp;lt;Path&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;내가 원하는 Path로 Clip할 수 있도록 도와주는 class라고 생각하면 편하다. 앞으로 내가 원하는 도형을 그려나가면서 알아낸 정보에 대해 작성하며 어떻게 그려나가는지 확인하겠다&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://medium.com/flutter-community/flutter-custom-clipper-28c6d380fdd6&quot;&gt;https://medium.com/flutter-community/flutter-custom-clipper-28c6d380fdd6&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Custom Clipper에 대한 설명을 자세하게 적어준 참고자료로서, 너무 유익한 정보를 담고 있으니 더 깊게 파헤치고 싶으면 위 reference를 찾아가길 바란다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  ..method&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;..구문은 &quot;cascades&quot; 또는 &quot;cascade notation&quot;라고 불리며 메소드 호출을 연결하여 동일한 객체에 대해 연이어 작업을 수행할 수 있게 해줍니다. 즉, class 내 여러 메소드를 동시에 사용하고 싶을 때 사용하는 구문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;기본적으로 선을 그릴 때, Path 객체를 다루는데, Path 객체를 생성할 때 moveTo, lineTo, closeTo와 같은 method에 대한 연이은 작업을 진행할 때 아주 유용하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  lineTo&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;가장 기본적으로 내가 원하는 도형을 직선을 활용해서 그리는 행위이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  addPolygon&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Offset으로 이루어진 여러개의 점을 한번에 이어주는 역할을 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;hextagonal.png&quot; data-origin-width=&quot;920&quot; data-origin-height=&quot;712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cVVNsS/btsC55wpbdT/gbT54ZglDApqis5aD7mAFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cVVNsS/btsC55wpbdT/gbT54ZglDApqis5aD7mAFK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cVVNsS/btsC55wpbdT/gbT54ZglDApqis5aD7mAFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcVVNsS%2FbtsC55wpbdT%2FgbT54ZglDApqis5aD7mAFK%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;424&quot; height=&quot;328&quot; data-filename=&quot;hextagonal.png&quot; data-origin-width=&quot;920&quot; data-origin-height=&quot;712&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;정 육각형은 아니지만, 정 육각형에 가까운 모습으로 최대한 그리기 위한 방법입니다. 이때, 대각변과 가로변의 길이 차는 0.22 정도입니다. 위 정보를 활용해서 OffsetList를 만들고 addPolygon method를 사용하면 다음과 같이 getClip을 진행할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;class CustomHexagonClipper extends CustomClipper&amp;lt;Path&amp;gt; {
  @override
  getClip(Size size) {
    double y = size.height;
    double x = size.width;

    var points = [
      Offset(0, y / 2),
      Offset(x / 4, y),
      Offset(x / 4 * 3, y),
      Offset(x, y / 2),
      Offset(x / 4 * 3, 0),
      Offset(x / 4, 0),
    ];

    Path path = Path()
      ..addPolygon(points, false)
      ..close();

    return path;
  }

	@override
  bool shouldReclip(covariant CustomClipper oldClipper) {
    return false;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그런 다음 화면에 적용 시켜 봅니다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;Flexible(
  flex: 1,
  fit: FlexFit.tight,
  child: AspectRatio(
      aspectRatio: 1,
      child: ClipPath(
        child: Container(color: Colors.red),
      )),
),
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;육각형으로 결과가 나오긴 하지만 아름다운 모습을 볼 순 없었습니다. 그래서 다시 한번 라이브러리를 찾아 다녔고 hexagon library를 찾을 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그래서 Path를 직접 찍으면서 찾아가는 방법은, 삼각함수에 대해 더 깊게 고민한 뒤 추후에 직접 만들어보려 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  hexagon&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://pub.dev/packages/hexagon&quot;&gt;https://pub.dev/packages/hexagon&lt;/a&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;flutter pub add hexagon
flutter pub get
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;사용 방법은 간단합니다!&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;HexagonWidget.flat(
	width: MediaQuery.of(context).size.width / 2,
	child: Container(),
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;만약 hexagon에 테두리를 만들고 싶다면, 부모 hexagonWidget보다 작은 hexagonWidget을 자식으로 두면 됩니다!&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;HexagonWidget.flat(
	width: MediaQuery.of(context).size.width / 2,
	child: HexagonWidget.flat(
		width: MediaQuery.of(context).size.width / 4,
		child: Container(),
	),
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  ShaderMask&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;우리는 기본 gradient로 LinearGradient를 알고 있습니다. 근데 LinearGradient에서 다양한 표현 방식에 대해 설명할 때, Text gradient에 대해서도 설명해줍니다. 그때 등장하는 Widget이 바로 ShaderMask입니다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://api.flutter.dev/flutter/widgets/ShaderMask-class.html&quot;&gt;https://api.flutter.dev/flutter/widgets/ShaderMask-class.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;ShaderMask는 정말 유용합니다! Widget에 gradients를 적용할 때 사용할 수 있고, Image를 적용할 때도 사용할 수 있습니다. 사용 방법은 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;ShaderMask(
  shaderCallback: (Rect bounds) {
    return RadialGradient(
      center: Alignment.topLeft,
      radius: 1.0,
      colors: [
				Colors.yellow, 
				Colors.deepOrange.shade900,
			],
      tileMode: TileMode.mirror,
    ).createShader(bounds);
  },
  child: const Text(
    'I&amp;rsquo;m burning the memories',
    style: TextStyle(color: Colors.white),
  ),
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;내가 원하는 Gradient효과를 shaderCallback에 넣어주고, gradient widget의 createShader method를 활용해 shader를 만들어줍니다. 그러면 ShaderMask Widget이 shader를 감지하고 자식의 배경으로 숨겨주죠!&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;opacity, CustomPaint, DecoratedBox, BackgroundFilter class들은 더 다양한 효과를 넣을 수 있는 방법들 입니다. 더 자세히 알고 싶으면 reference를 참고하세요.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  소수점 출력&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;double type의 값을 string으로 표현할 때, 소수점에 대해 신경써야 할 부분이 생겼다. 가장 간단한 방법은 toStringAsFixed method를 사용하는 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://api.flutter.dev/flutter/dart-core/num/toStringAsFixed.html&quot;&gt;https://api.flutter.dev/flutter/dart-core/num/toStringAsFixed.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;사용 방법은 간단하다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;value.toStringAsFixed(3); // result: 1.000
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/224</guid>
      <comments>https://malangdidoo.tistory.com/224#entry224comment</comments>
      <pubDate>Tue, 16 Jan 2024 00:28:47 +0900</pubDate>
    </item>
    <item>
      <title>[Flutter] (Project) MapleApp: 20. Stat Page 제작-1</title>
      <link>https://malangdidoo.tistory.com/223</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;  intl, NumberFormat&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;숫자를 표현해야 하는 텍스트가 많았다. 방법을 찾다 보니, 이전에 DateFormat을 위해 설치했던 intl library에 NumberFormat이 존재했다. reference를 더 자세히 읽어보니 날짜/숫자 형식 지정을 지원하고 있었다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://pub.dev/packages/intl&quot;&gt;https://pub.dev/packages/intl&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;intl의 NumberFormat을 활용해 내가 원하는 방식으로 다양하게 표현해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt; &lt;/b&gt; 콤마 구분&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;var mainFormatter = NumberFormat('#,###,###,###,###,###', 'ko_KR');
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  소수점 구분&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;var percentFormatter = NumberFormat('###.##', 'en_US');
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  한국 화폐 단위 적용&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;var measure = ['억', '만', ''];
var value =
    subFormatter.format(intValue).toString().split(',');
var len = measure.length - value.length;
newString = '';
for (var i = 0; i &amp;lt; value.length; i++) {
  newString = '${newString} ${value[i]}${measure[len + i]}';
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  Dart의 Type지정과 관리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;아래에 있는 코드를 기본 코드라고 하자.&lt;/p&gt;
&lt;pre class=&quot;prolog&quot;&gt;&lt;code&gt;static final List mainList = [
	['1','2','3'],
	['1','2','3'],
	['1','2','3'],
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그랬을 때, 내가 기본 코드와 같이 선언한 값을 사용하고 있을 때, Dart 자체의 문법과 컴파일러의 인식이 어떻게 되는지 몰라 헤매고 있었다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://stackoverflow.com/questions/49603021/type-listdynamic-is-not-a-subtype-of-type-listwidget&quot;&gt;https://stackoverflow.com/questions/49603021/type-listdynamic-is-not-a-subtype-of-type-listwidget&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그래서 stackoverflow를 통해 다양한 해결 방법을 찾아냈고. 그 방법을 정리하면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  List&amp;lt;List&amp;lt;dynamic&amp;gt;&amp;gt;으로 지정해주기&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;위 코드에서 List만 선언해 줬을 뿐, 다음 List에 대해서는 선언된 것이 없기 때문에 Dart compiler에서 mainList를 List&amp;lt;dynamic&amp;gt;으로 생각하고 있기 때문에 map method를 사용할 수 없던 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  dynamic으로 온 변수에 타입 명시해주기&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;기존에 제시되었던 기본 코드의 변수를 가져와 더블 map을 다음과 같이 구현했다고 해보자.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;mainList.map(
	(subList) =&amp;gt; Column(
		child: subList.map(
			(element) =&amp;gt; Container()).toList(),
)).toList(),
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이때 Dart Compiler는 mainList는 List&amp;lt;dynamic&amp;gt;으로 인지하고, subList를 dynamic 요소로 인지한다. 이번 방법은 이 subList를 List&amp;lt;String&amp;gt; 또는 List&amp;lt;dynmaic&amp;gt;으로 인지시키기 위한 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;List&amp;lt;~&amp;gt;로 subList의 타입을 명시해주는 이유는, Dart Compiler가 subList는 dynamic 개체가 아닌 List 개체로서 받아들여 map method를 사용할 수 있게 하기 위함이다. List로서 인지할 수 있게 만들어주면 map method를 사용할 수 있기 때문이다. 방법은 다음과 같이 2가지 방법이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  List&amp;lt;String&amp;gt;.from(subList).map(~)&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size18&quot;&gt;※ reference : &lt;a href=&quot;https://api.dart.dev/stable/3.2.4/dart-core/List/List.from.html&quot;&gt;https://api.dart.dev/stable/3.2.4/dart-core/List/List.from.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;List의 from method는 매개변수로 오는 반복 가능한 element 요소를 지정된 type형태를 지닌 List 목록으로 만들어 반환해준다. 그래서 subList가 List&amp;lt;String&amp;gt; subList로 새롭게 생성되었기 때문에 Dart compiler에서 List로 받아들이고 map method를 사용할 수 있게 된 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;  (subList as List&amp;lt;String&amp;gt;).map(~)&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Dart Compiler가 subList의 형태를 List&amp;lt;String&amp;gt;으로 인지할 수 있게 해주는 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;가장 훌륭한 방법은 선언 당시 List&amp;lt;List&amp;lt;dynamic&amp;gt;&amp;gt;으로 선언해주는 것이다. 물론 mainList의 type을 명시하지 않으면 Dart Compiler는 내부 요소를 확인하고 알아서 List&amp;lt;List&amp;lt;String&amp;gt;&amp;gt;으로 인지한다. 하지만 2차원 배열 내부에 있는 값이 1개라도 변하게 되면 String이 아닌 Object로 인지하게 된다. 또한, 내부 값이 변할 수 있다는 상황으로 인해 List&amp;lt;List&amp;lt;String&amp;gt;&amp;gt;으로 선언할 수 없게 되고, 결국 dynamic으로 선언하게 되는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;가장 중요한 문제는 List&amp;lt;List&amp;lt;dynamic&amp;gt;&amp;gt;을 List&amp;lt;dynamic&amp;gt;으로 인지하고 있었기 때문이므로, 내부 요소에 따라 계층구조라도 정확히 표현해주어 나와 같은 오류를 겪지 않도록 바란다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;  function example({required String a, int b, double c})&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Dart에서 함수를 사용하다 보면 위처럼 사용하는 경우가 많다. Dart에서는 중괄호를 사용하여 매개변수를 명명할 수 있으며, 함수 호출 시 해당 명명된 매개변수에 값을 할당할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;그래서 우리는 위와 같은 함수에 들어가는 매개변수를 &amp;ldquo;명명된 매개변수(named parameter)&amp;rdquo;이라 하고, 그 함수를 호출하는 것을 &amp;ldquo;명명된 매개변수 호출&amp;rdquo;이라고 합니다.&lt;/p&gt;</description>
      <category>개발/Flutter</category>
      <author>지수는말랑이</author>
      <guid isPermaLink="true">https://malangdidoo.tistory.com/223</guid>
      <comments>https://malangdidoo.tistory.com/223#entry223comment</comments>
      <pubDate>Mon, 15 Jan 2024 10:12:08 +0900</pubDate>
    </item>
  </channel>
</rss>