G.frege를 너무 사랑하는 holy가...

[flutter] daily record-whitebrew

[ document summary ]
    Title: [flutter] daily record-whitebrew
    date: 0001 1.1
    content:

oauth처리

Figure 1: oauth

Figure 1: oauth

참조: https://www.youtube.com/watch?v=Dh-cTQJgM-Q

const vs final

final은 runtime때 값이 정해지고, const는 compile때 값이 정해진다. widget 을 const로 생성하면, 생성자의 인자로 주어지는 것들은 const여야 한다. final이 있으면 error가 난다. Colors.grey는 const라서 상관없지만, 만일 Colors.grey[300]는 const로 하면 에러가 난다. 에러가 나는 이유는 Colors.grey[300]은 runtime때 rgb값이 결정되는 final이라서 그렇다.원래 lcd나 display를 보면 color를 위해서 color pallete라는게 있다. cpu가 display에 직접 접근할때는 console이라고 하는데, 이건 ascii에 해당하는 문자 그림이 display에 있듯이 color pallette도 display에 있다. 옛날에 cgv mode이런 graphics mode로 display를 사용할때 pallete를 사용했다. table로 되어 있는데 이건 system마다 다르다. compile 때 정해지는 const는 compile결과인…파일, 즉 object 파일에 rgb값이 명시 되어 있지만, [300]은 runtime때 display에서 그 값을 가져와야 한다.grey[400] 이런것들은 실행을 해서 display에 있는 color pallette를 꺼낸 rgb값을 사용하기 때문에, final이 될 수 밖에 없다.

final의 특징

동영상을 보면, type이 없이 final controller가 사용된다. 신기하다. type inference를 한다는 것이다. 좋은건 다 가져다가 쑤셔박은 느낌이랄까.ㅋ

final string name;
final name;

위와 같이 사용한다. 근데 난 flycheck에서 check를 해주는데, 동영상의 소스는 warning이 없다. 왜 그런걸까.

simulator에서 app 지우기

simulator에서 테스트를 너무 많이 하다보니 app이 많다. 지우는 법은 option을 누른상태로 click하면 지워진다.simulator에서 작업하다보면, hot reload하건 hot restart하건 화면 변화가 전혀 없을 수 있다. 이럴때 app을 지워줘야 한다.

sidecar의 사용

모니터가 하나라서 side car를 사용해야 한다.

const 생성자 error

Figure 2: const error1

Figure 2: const error1

const MaterialApp이란 생성자에서, LoginPage() 함수를 호출하는데 이것은 const construtor를 사용하지 않는다는 에러가 난다. const contructor에서 호출하는 객체들의 constructor는 const로 하던가, const Materialapp에서 const를 삭제해야 한다.

flutter의 naming

flutter의 naming이 있다.

file: snake_case,
class: UpperCamelCase,
변수명: lowerCamelCase,

figma design적용시

figma design을 보면 중복이 되는 component가 많다. 이런 component는 flutter에서 구현할때, widget class를 만들어서 재사용하는 경우가 많다. 예를 들면, 내경우 pages(screens)라는 폴더를 만들었고, 거기에 component도 만들었고, 아주 기본이 되는 text도 다 만들었다. 큰 project의 경우, 가장 많이 쓰이고 atom에 해당하는 Text를 Factory형태로 만들어 쓰는것도 코드를 단순화할 수 있다.

required parameter name

widget이나 class를 만들때 생성자에서 required.this.name으로 초기화할 때가 있다. 이렇게 초기화하면 ide에서 사용할때 name:이 자동으로 만들어진다. 그래서 값을 넣으면 된다. 이미 만들어진 widget들도 모두 이런형식으로 되어 있다. 예를들면, 위 name이름에 대한 값은 보통 Name이라는 object가 value로 들어간다. 그런데 padding:의 경우 Padding이 아니라, EdgeInsets()객체가 들어간다. decoration:의 경우도 BoxDecoration객체가 들어간다. Text의 style:도 TextStyle()객체가 들어간다.

둥근 container class에 대해서

둥근 container에는 PD BBC: padding decoration, border,border radius, color가 주로 기술된다.

Row와 Column MainAxis

Roma로 외우자. Row에는 MainAxis..가 들어간다. column도 그렇다.

row와 column 사용법

이게 했갈리는데, column을 선택하면 child가 서랍장처럼 배치된다. row는 옛날 desk처럼 가로로 서랍이 있다.

font에 대해서

flutter pub add google_fonts로 하면 google font를 사용할 수 있다. 사용법은 pubs.dev에 나와 있다.

layout처리

device마다 다른 화면을 가질수 있다. static한 좌표를 쓰지말고 mediaQuery를 사용해서 상대적인 width와 height를 써야 한다.

비동기 영상

https://www.youtube.com/watch?v=rk41rBXq3zQ

비동기 프로그램에 대한 이해 개념을 약간 비유적으로 한다면, 음식점으로 많이 하는데…. 손님이 음식점에 들어가서 음식주문하고 요리가 완료될때 까지 기다린다면 동기화다. 즉 그동안 다른 손님은 기다린다..이런 식당을 동기화…. 비동기화 식당은 손님이 기다리지 않고 음식주문하고, 또 다른 손님이 음식주문 하고 요리도 주방에서 하는 순서대로 나오고…이런게 비동기화… 개념을 모르는 사람은 없다. 다만, 이게 언어별로 코드화 되었을때 tricky한 면이 있다.

Future

Future가 처음보면 황당하다. Future가 뭐냐?를 우선 알기위해선 공식문서를 이용하자. https://api.flutter.dev/flutter/dart-async/Future-class.html

  • Future는 class다. generic 형태의 class다.

abstract와 interface라고 써져 있다. 보통 interface는 method의 signiture만 기술하고 implement는 interface를 사용하는 class에서 구현한다. abstract는 구현되지 않은 method가 하나라도 있으면, abstract class다. 상속을 하는곳에서 구현을 해야 한다. 근데 abstract class이며, interface다. 이건 좀 어렵다. 이해가 안된다. interface며 abstract라고 하는건, Future class의 모든 method들이 미구현되어 있다. 이렇게 이해하면 되는 것일까? 맞다. 우선 나는 Future는 generic형태로 만든 모든 method들이 미구현된 class로 이해하겠다. 다시 요약하면 future는 class고 미구현된 메소드들로 구성되어있다. 한번 생각해보자. Future<int> a = 3; 이거 가능할까? 에러날 것이다. 왜냐? Future<int>는 class다. 그리고 future가 아니라 Future다. class표기법을 따라야 하니까… class는 type system을 사용하는 언어에서는 type이라고 말해도 된다. haskell이나 swift처럼 strong한 type system을 갖는 언어에선 class라고 말하기 보다, type으로 말하는걸 선호하는거 같다. dart도 strong한 type system을 갖기 때문에, 그냥 type이라고 말해도 된다. 여튼, Future<int>는 generic형태의 type이다. 반면에 3은 Int type이다. 다른 type의 경우 assign이 안되기 때문이다. strong한 type system에선 type conversion도 엄격하다. 그래서 null safety라는 것도 나오고 ?도 나오고 그렇다. 여튼…Future<String> a = ‘asdfasdf’; 이런것도 안된다. 다른 type이기 때문이다. 그런데 Generic은 가능하지 않냐? Generic이라면 String으로 바꿔지니까…Future가 generic이라면 Future<string>은 String으로 바꿔져야 하잖냐? 그렇다. 녹는다. generic은 녹는다. 영상에선 이런 문장이 나온다. Future<String> name = Future.value(“test”); 이 문장을 이해하는게 어려웠다. hoyoul — 11/03/2023 10:01 AM Future.value()는 Future abstract class의 member method일 것이다. 아니다. abstract class 멤버함수가 아닌, Future.value()의 형태를 보니까, static method다. 그렇다면 구현은 되어 있을것이다. Future의 객체 메소드들은 미구현이겠지만…. static은 구현되어 있을것이다. 그리고 value()의 리턴은 Future instance일 것이다. print(name)을 하니, instance of Future라고 나온다. 맞다. 지금까지의 생각은 맞다. 그렇다면, 그냥 Generic type과 Future가 뭐가 틀리냐? 라고 질문할 수 있다. 그리고 Future와 비동기와 뭔관계냐? 라는 질문도 할 수 있다. 차이는 있다. Generic으로 정의된 type은 type이 인자로 들어오는 순간 generic의 type이 결정된다. 예를 들어서, G<String> name = “string” 은 String name = “string” 과 똑같다. Future<String> name = Future.value(“string”)은 String name = “string"과 다르다. name에 string값이 아니라 Future instance다. Generic type은 인자의 type으로 녹여지는 반면에 Future는 녹여지지 않는다. 언제 녹여지는가? hoyoul — 11/03/2023 10:17 AM 알수 없다. 미래에 벌어지기 때문에 Future를 사용하고 Future가 Generic과 다른것이다. 이렇게 동작하는게 좀 신기하긴 하다. 물론 동영상에선 이런 말을 하진 않는다. 재해석이나 이해는 원래 시청자들의 몫이다. 우선 Future가 Generic과 다르다는 건 확실히 이해했다. 그러면 Future와 비동기는 뭔 상관이냐?는 지금 시점에선 모르겠다. generic의 경우엔 G<string> name = “test"라고하면 name.toUpperCase()같이 멤버함수를 사용할 수 있다. 왜냐면 G<String>이 String으로 녹여지기 때문이다. String name = “test"와 동일한 문장이니까…그런데 Future<String> name = Future.value(“test”)라고 하고 name.toUpperCase()는 사용할 수 없다. 왜냐? 아직 Future instance이기 때문이다. 녹여지지 않는다.

Future.delayed() 강의에선 Future.value()에 대한 이론적인 설명을 거의하지 않고, 어찌되었던 Future는 class이고 그들의 함수가 있으니 함수를 살펴보자라는 식인거 같다. 그래서 delayed()라는 함수 사용법을 설명한다. 강사가 제시한 예제는 다음과 같다. breaking time: 차빼줘야 한다. hoyoul — 11/03/2023 10:54 AM void addNumbers(int number1, int number2){

print(‘계산시작: $number1 + $number2’);

Future.delayed(Duration(seconds: 2),(){ print(‘계산 완료: $number1 + $number2 = ${number1 + number2}’); } );

print(‘함수 완료’); } 이게 흥미롭다! 내가 궁금했던 것중에 하나가, Future instance가 언제 녹여지는가? 였다. 다시 반복하면, Generic인 G<String> name = “string”; 바로 녹여져서 String name = “string”; 과 같은 식이 된다고 했다. 그러면 Generic의 일종인 Future도 Future<String> name = Future.value(“string”); 도 언젠가는 String name = “string”;으로 녹여져야 한다. Future.value()는 녹여지는 시점이 정해지지 않았다. 영원히 녹여지지 않을 수 있다. 그런데 Future.delayed()는 녹여지는 시점을 정해준다. delayed라는 이름에 방점을 찍고서 이해해서는 안된다. Future가 제공하는 delayed()라는 함수가 있는데, 그것은 Future instance가 녹여주는 시점을 정해준다..라고 이해하면 된다. 이제 코드를 해석하면 다음과 같다.

  1. 어디선가 addNumbers(3,4)를 호출한다.
  2. print문을 실행한다.
    1. Future.delayed는 Future instance를 return한다.
  3. print문을 실행한다.

hoyoul — 11/03/2023 11:01 AM

  1. 2초가 지난뒤 Future.delayed()가 반환한 Future객체의 body가 수행된다. 이 부분이 녹여진다라고 보면 된다.

원래 모든 programming은 동기화가 default이다. 즉 (3)에서 실행이 완료된 후 (4)로 넘어가야 한다. 그런데 Future.delayed()는 Future instance를 return하고 끝이다. dart가 higher order인지 모르겠지만, 만일 higher order function을 지원한다면, Future.value()도 동일하게 Future.value(()=>{computation}) 이런식으로 시간이 지연되는 계산식을 포함하고 있을 수 있다. 이경우도 시간을 끌지 않고 바로 Future instance를 return할 것이다. 다만 차이가 있다면 Future.value()는 녹여주는 시점이 없기 때문에 (5)와같이 실행은 안될것이다. delayed()는 Future 를 녹여주기 까지 한다. hoyoul — 11/03/2023 11:14 AM 이게 바로 비동기다. 그리고 음식점 예와 동일하게 주문만 받는 걸로 이해가 될 수 있다. 비동기의 문제점 hoyoul — 11/03/2023 11:18 AM 비동기가 남발되면, code를 짜기도 어렵고 해석하기도 어렵다. 왜냐면 비동기는 계산이 언제 끝나는 지 알수 없기 때문이다. Future.value()처럼 영원히 녹여지지 않거나, Future.delayed()처럼 녹여지긴 하지만, 명시적일수도, 아닐수도 있다. 여튼 계산이 끝나야 그 계산값을 이용해서 또 다른 계산을 할텐데 말이다. 그런 문제점을 나타낸 코드가 공식 문서에 있다. https://dart.dev/codelabs/async-await 여기에 가면 다음과 같은 코드가 있다. 이미지를 추가하겠다. 그래서 일반적으로 program의 code는 동기가가 default이다. 동기라고 가정해야, 코드를 순서대로 flowchart에 맞춰서 짜는게 가능하기 때문이다. ps: 이미지를 추가하려고 했다가 모르고 thread를 눌렀는데, 그냥 놔두자. Thread 비동기가 남발되면, code를 짜기도 어렵고 해석하기도 어렵다. 왜냐면 1 Message › There are no recent messages in this thread. 그러면 비동기가 필요한가? 필수적인가? 안쓸수 없는가? 쓸수 밖에 없다. 왜냐하면 네트웍 통신을 할때라던지, gui에 그림을 그린다던지, system runtime resource를 사용하는 경우는 비동기로 이미 코드가 짜여져 있다. 내가 네트웍을 사용하지 않는다면 안 사용해도 된다. 그런데 http 통신을 한다, web socket을 사용한다. 이런것들은 이미 Future로 library화 되어 있다는 것이다. 비동기가 코드를 작성과 해석이 어려운데도 불구하고, 이미 비동기 코드가 만연해 있는 현실이다. 이것을 해결한게? async와 await다. 해결보다는 조화롭게 쓰는 문법적 요소다. hoyoul — 11/03/2023 11:29 AM async와 await async와 await는 keyword다. keyword라는건 순전히 문법적 요소다. 의미보단 용법에 집중해야 한다. hoyoul — 11/03/2023 12:01 PM 우선 제일 간단한 await keyword를 보자. hoyoul — 11/03/2023 12:09 PM 음…await를 보기전 좀 정리를 하자. Future는 class라고 했다. Future를 상속받아서 class를 만들고 이것의 member함수를 사용하진 않는다. 대부분 Future가 제공하는 static member function을 호출해서 사용한다. 예를 들어서, Future.delayed(), Future.value(), 아니면 Future<String> obref = A.b(), 와 같이 어떤 함수의 return값이 Future인경우 return받은 값을 참조한다던지…Future를 사용하는 경우는 대부분 이런경우인데, 이럴때 Future가 계산되어 녹여지지 않고, 바로 리턴한다고 했다. 비동기의 특징이자 문제점인데, 이것을 동기처럼 하려면 wait하면 된다. 그래서 await라는 keyword를 붙여준다. ps: 그리고 녹여준다라고 표현을 했는데, programming 용어인데… lambda를 공부할때 보면 syntactic sugar라는게 있다. 그 표현을 그대로 따라한것이다. hoyoul — 11/03/2023 12:17 PM 예를 들면, Future.delayed() 대신에 await Future.delayed()로 쓴다던지, Future.value()대신에 await Future.value()라고 사용하는 것이다. 이렇게 하면 behind scene에선 계산을 하던지, 통신하던지…background로 처리한 이후에 다음코드가 실행이 된다. 어..그러면 동기화하고 똑같잖아? 라고 말할 것이다. background처리? 그냥 동기화잖아. 다르다. 이것은 multi process나, thread를 사용할경우 해석이 달라진다. 예를 들어보자. void temp(){ await Future<String> t = Future.delayed(); print(“next sentence”); } thread가 1개일때는 동기화 코드와 동일하다. Thread가 2개라고 하자. A,B 두개의 thread가 있다고 할때, A thread는 await문장을 만나면, background에서 계산을 하고 제어권을 놓는다. 그래서 B라는 thread가 제어권을 받아서 await문장을 실행할수 있다. hoyoul — 11/03/2023 12:25 PM 동기화 코드라면, A thread는 await로 된 문장을 실행할때, 실행이 끝날때까지 제어권을 놓지 않는다. 따라서 B라는 thread는 await코드에 접근하지 못하고 기다려야 한다. 밥먹고 와서 다시… hoyoul — 11/03/2023 1:35 PM thread가 한개라고 생각한다면, await 키워드를 보고 당황할 일은 없다. 궁금한건, 거의 모든 Future code에 await가 붙는다고 보면 될것인가? 안붙이고도 사용하는 경우가 있는가? 하는 것이다. hoyoul — 11/03/2023 1:58 PM then 사용 await를 사용하는 이유는 await의 결과가 다음 수행될 code들에서 사용되기 때문이다. 다음 수행될 코드들과 의존관계가 있기 때문에 await로 동기처럼 사용한다. 만일 의존되는 코드가 많지 않다면 then을 사용해도 될듯하다. Future<String> name = fetchData().then(result){ }, 이런식으로 사용되는데, call back함수다. 즉 완료되면 then 이하가 호출되어 실행된다. async 비동기 코드를 나타내는 keyword 비동기가 코드가 있는 code block들은 async를 붙여준다. 그것이 함수가 되었던, 뭐가 되었던… 예를 들어보자. Future<String> asyncfunction(){ await Future<String> name = Future.delayed(); print(“test”); return name; } 이런 함수가 있다고 하자. 이 함수를 호출하는 temp()가 있다고 하자. void temp(){ print(asyncfunction()); } hoyoul — 11/03/2023 2:07 PM temp처럼 asynfuction을 사용하는 입장에선 asyncfunction이 Future함수인지 모른다. 그래서 Future를 사용하는 function이나 code에서는 이것이 Future를 포함하는 code를 알려주기 위해서 async를 body앞에 붙여준다. Future<String> asyncfunction() async{ } 왜 알려줘야 하냐? await를 사용하던, then을 사용하던, 내가 사용하려는 함수가 Future인지 아닌지 알수 없기 때문에 async를 표기하면, 아…이것은 http에서 data fetch하니까…async작업이 일어나겠군…이걸 await로 처리하자.. 이게 되는 것이다. hoyoul — 11/03/2023 2:16 PM 요약하면 await는 비동기지만, 값을 받고 “그 이후” 코드들을 실행하겠다. 라는 것이다. 비동기지만 처리후에 그 값을 이용하는 코드들이 있기 때문에 await를 붙여주는것이다. 강사의 설명과 내설명은 다르다. 하지만, 본질은 같다. 그리고 강사가 잘 설명한다. 좋은 강사라는 생각이 든다. hoyoul — 11/03/2023 2:31 PM Future 요약 Future는 abstract class이다. 우리가 Future를 상속받아서 구현해서 쓸 일은? 많지 않다. 왜냐면 우리는 sdk에 들어가는 library를 직접 만들기보단 사용할 일이 더 많기 때문이다. 예를 들어 내가 만든 arduino를 제어해야 한다면 이때는 만들어야 한다. arduino와 연결되어 어떤 값을 가져와야 한다면, 그 값이라는 것은 code로 볼때 runtime때 결정되고 언제 값을 가져올 지 알수가 없기 때문에 Future를 상속한 class와 api를 작성해야 한다. 우리는 runtime때 다른 device에 연결된 값을 가져올때, flutter에선 Future를 사용하기도 하지만, Stream이란걸 사용하기도 한다. 예를들어서 http통신같은 경우는 끊어지는 protocol이다. session처리를 하지 않는다면 함수 호출로 끝난다. 따라서 이경우는 그냥 Future로 처리하면 된다. 아니, 물론 우리가 처리하는건 아니고, http관련 package는 아마도 다 Future로 api를 제공했을 것이다. 그러면 future로 구성된 sdk를 사용하면된다 . 그런데 web socket을 보자. web socket에서 tcp를 쓴다고 하자. 매번 함수 호출을 한다? call stack은? 자원 낭비가 심하다. 한번 연결하고 계속 필요에 따라 값을 가져올 방법은 없는가? 그럴때 사용하려고 만든 것이 Stream이다. DB를 보자. DB를 사용하면 인증을 거친 이후에는 계속 data를 가져온다. 따라서 Stream을 사용한다. Stream도 generic형태의 class다. Future와 마찬가지로 상속해서 구현할 일은 없다. 그냥 가져다 쓰는거다. 다만, db라이브러리던 socket 라이브러리던, 구현이 Stream으로 되어 있기에 Stream 사용법을 알아야 할 뿐이다. 이제 flutter의 꽃 stream을 좀 살펴보자. 위에서 Future나 Stream을 데이터를 꺼내오는것에 중점을 뒀지만, 보내는것도 동일하게 해당되는 것이다. 어차피 Future나 Stream은 외부에 있는 device나 cloud에 연결해서 resource를 가져오는데 생기는 비동기문제를 처리할려고 등장한 개념이니까 말이다. Stream

공식문서를 보자. https://api.dart.dev/stable/3.1.5/dart-async/Stream-class.html Stream class - dart:async library - Dart API API docs for the Stream class from the dart:async library, for the Dart programming language. mixin이다. 다중상속이다. java에는 없는..python에는 있었던거 같은데…원래 oop는 다중상속이 가능하다. small talk엔 있었던거 같다. 뭐 중요하진 않다. java야 primitive type도 객체가 아니라서 oop언어라고 하기에도 모호하니까…특정 언어 implementation이 oop를 규정하진 않는다. Stream도 Future와 같은 generic형태의 class다. hoyoul — 11/03/2023 3:15 PM 음…stream이란 용어는 flutter에 처음 등장하는 게 아니다. flutter란 언어는 짬뽕밥이다. 기존언어에 있던거 다 합쳐놓은 것이라서 stream도 당연히 다른 언어에 있던거다. c언어에도 i/o 입출력에 stream을 사용했고, c++, java, 너무나 많은 언어에서 stream들이 있기 때문에 뻔한 얘기들이다. 개념보다 중요한건 용법이다. 보통 다른 언어에선 stream을 얻어오고, stream 객체에서 method를 처리해서 device와 i/o를 했다. flutter에선 약간 다르다. event방식을 사용하는데, 이게 보통은 framework에 숨겨져있는데, flutter는 user에게 노출된 형태다. 간단히 flutter의 stream을 pseudo code로 본다면 다음과 같다. stream = stream을 만들던 가져오던한다. stream에 data가 없을수 있지만, 있다고 가정한다. eventlistener(call back)를 만들어서 stream과 연결한다. 이렇게 하면 callback이 호출되면서 stream에서 데이터를 얻어오게 된다. Future하고는 다르다. callback이 사용되었다는것은 명시적으로 framework가 호출한다는 것이다. Future도 내부적으로는 framework가 데이터를 전달하지만, implicit하다. hoyoul — 11/03/2023 3:37 PM Stream의 용법 Stream도 Future와 같은 class라고 했다. 따라서 Future처럼 사용하면 된다. 예를 들어보자. hoyoul — 11/03/2023 3:47 PM import ‘dart:async’; void main() { print(a()); } Stream<String> a() async* { yield “e3”; } 간단하게 만들어봤다. Future<String> name = Future.value(‘hoyoul’); print(name); 이렇게 하면 future instance가 출력되는걸 확인 했었다. Stream에서는 value()같이 이미정해진 함수가 있는지 잘 몰라서 만들었다. 위 간단한 코드의 결과는 Stream instance다. Stream을 사용할 때 알아야 할것을 정리하면 다음과 같다.

  1. import ‘dart:async’; 가 필요하다.
  2. async* 도 async처럼 이 함수가 비동기 코드를 포함하고 있다는 것을 알려준다.
  3. yield: Future객체나 Future함수나 녹기전까진 Future instance였다. 마찬가지로 Stream객체도 녹기전까지는 Stream instance다. 따라서 yield로 return되는 값은 “e3"라는 문자열이 아니라 문자열로 녹기전 Stream 객체로 보면 된다.

hoyoul — 11/03/2023 3:54 PM Stream의 또다른 예제 강의에서 말하는 예제가 있다. 이것만 알면 아주 아주 기초적인 Stream을 알수 있을거 같다. 솔직히 stream은 그냥 사용하는 도구라서, 이해보다는 자주 사용해야 하고, 원래 자주 사용된다. hoyoul — 11/03/2023 4:09 PM import ‘dart:async’; void main() { final controller = StreamController(); final stream = controller.stream;

final stListener = stream.listen((val) => {print(val)});

controller.sink.add(3); } 강사의 강의에서 stream은 좀…그렇다. 대상이 처음 언어를 접하는 사람이 듣기엔 어려울 듯하다. 그러면 언어를 많이 접해본 사람에 입장이라면? 별루다. 경력이 많건, 초보이던, 강의는 쉬워야 한다. 근데 어려운 내용이라서 어렵게 가르친다면, 그냥 생략하면 안되나? 이런 생각이 들었다. 여튼 import문으로 가져오는건, 설명했고, StreamController가 나온다. Stream이 DB던 socket이던, bluetooth가 되었던 가져와야 한다. 근데 여기선 StreamController라는 controller에서 stream을 가져온다. 어떤 device와 연결된게 아니라는 것이다. 가상 device라고 보면 좀 오바일수도 있다. hoyoul — 11/03/2023 4:17 PM 여튼 이 예는 controller를 통해서 stream을 가져오고, 여기에 listener를 달아놨다. 만일 DB였다면, DB package에 stream을 꺼내는 함수가 있을것이다. controller가 있을 수도 있고, 마찬가지로 꺼내서 listener를 연결하면 위코드와 동일할 것이다. 마지막에 controller.sink.add()가 있다. 이것은 send하는 것이다. send는 언제 도착하는지 중요하지 않다. 그래서 함수를 사용하는 것이다. db도 마찬가지일것이다. insert로 레코드를 쓰는건 함수처리 할것이다. 여튼 비동기는 여기까지만 할려고 한다. 비동기를 공부한 이유는 한가지다. 내가 dev.pub에서 package를 설치해서 사용할려고 하는데 예제를 보면 Future일색이다. google signedin package도 예제를 보고 이해하고 적용해야 하는데, 지금 막혔다. 막힌 이유는 비동기도 있지만, stateful life cylce이 명확하지 않다. 왜냐면 flutter라는 framework가 없으면 안돼? 꼭 있어야 해? 라는 질문으로 부터 내 공부가 시작했기 때문이다. 여튼 헛소리는 집어치우고….stateful에 대해서…아! 확실히 알아..하고 간단한 예제로 언제 어디서든 설명할수 있기만 하면, flutter의 기초는 마련될꺼 같다. hoyoul — 11/03/2023 4:27 PM 10분만 breaking time. hoyoul — 11/03/2023 4:41 PM 괜찮은 강좌가 있을지 모르겠지만, 한번 대충봐야겠다.

생명주기

앱을 보면 ios가 됐던, android가 됐던 생명주기라는게 있다. 왜 있나? 없으면 안돼? [5:02 PM] 일반 프로그램은 entry point를 호출해서 끝나면 끝이다. [5:03 PM] 근데, app은 아니다. 왜냐? app은 daemon이라서 그렇다. [5:03 PM] daemon은 간단히 말해서 내부 코드에 while(1){ 생명주기}가 있다고 보면 된다. [5:03 PM] automata라고 부르는 것이다. [5:04 PM] flutter라는 framework도 daemon이다. app은 daemon위에 daemon이 떠있는 형태.. (edited) [5:05 PM] framework은 여러 설명이 있겠지만, inversion of control로 한큐에 설명이 된다. [5:06 PM] framework을 만드는 이유는 한가지다. 모았다가 처리다. [5:06 PM] 그걸 하기위해서 framework을 만든다. rails도 그렇고, flutter도 마찬가지다. [5:06 PM] flutter는 rendering을 위해서 모았다가 처리할 필요가 있다. [5:07 PM] gpu때문에 하는거다. [5:08 PM] rails는 더 포괄적이긴 한데…network…이 주이긴 하지만, [5:08 PM] rails는 진짜…너무 극단적? hoyoul — 11/03/2023 5:10 PM 여튼 내가 궁금한건, fundamental한 이런 내용이 아니다. 좀 더 구체적이고 code base인데…fundamental한 내용을 녹여서 만든 강의가 있는지 찾고 있다.

hoyoul — 11/03/2023 5:30 PM 여튼 framework을 사용해서 개발할때는, 많은 코드가 framework에 숨겨져 있기때문에 code를 이해하기 어려울 수 있다. [5:31 PM] 즉 우리가 만드는 program입장에서 바라보면 안되고 framework입장에서 code를 바라봐야 이해가 되기 때문이다. [5:31 PM] 밥먹고 다시….

hoyoul — 11/03/2023 7:36 PM 공식문서를 보고 있는데, 음… [7:37 PM] 좀 더 보고 정리할께 있으면 정리하자.

hoyoul — 11/03/2023 7:48 PM widget’s state [7:49 PM] 모든 객체는 상태가 있다. 보통 property 혹은 멤버변수가 상태라고 보면 된다. 그런데 flutter에서 상태는 생성자에 들어가는 인자값이 상태로 보는것이 더 와닿는다. [7:50 PM] stateless widget의 life cycle과 state [7:52 PM] 버튼과 container를 갖는 stateless widget을 하나 만들어보자. 이 widget의 property로 color값을 갖게 하자. [7:52 PM] 버튼을 누르면 color의 값이 바뀌게 할려고 한다. 상태값이 변경된다는게 어떤 의미를 갖는지 알고 싶기 때문이다.

hoyoul — 11/03/2023 8:15 PM 코드를 짜는데, 내가 key에대해서도 개념이 없다는 걸 알았다. 그냥 java에서 class짜듯이 멤버변수 만들고, 생성자에서 입력받게 하니까…ㅠㅠ

stateless1.png 내가 원한건 pressed me를 누르면 container의 색을 변경하려고 했다. [8:31 PM] 변경이 안된다. [8:32 PM] 코드는 간단하다. 화면 page class에 멤버변수로 mycolor가 있고, 버튼을 누르면 멤버변수의 값을 brown으로 변경하는 코드를 짰다. (edited) [8:33 PM] 버튼을 누르면 멤버변수의 값이 바뀌므로 즉, 상태가 바뀌면 반응을 할꺼라고 생각했다. 그런데 왜 안되는 것일까? [8:37 PM] 이게 stateless위젯의 특성이다. 한번 생성되면 바뀌지 않는다. 그래서 stateful widget을 만들어서 처리 해야 한다. [8:40 PM] 그러면 stateless widget으로 만든 화면은, 버튼과 같이 상태를 변경시키는 widget들을 배치할 필요조차 없는것 아닌가? 어차피 page가 stateless widget이라면 반응조차 안할테니 말이다. [8:42 PM] 그러면 stateless widget으로 화면 page작업을 하면 안되는거 아닌가? 그런데 나는 stateless widget으로 kholdem page들을 만들었다. 거기도 소셜로그인 버튼이 있다. [8:43 PM] 버튼이 눌려지면 consent화면을 통해서 다른 page로 redirection이 될텐데, 다른 page로 변경되는것도 상태변환이라면, stateless widget page이기때문에 변경도 안되는 것인가?

hoyoul — 11/03/2023 8:46 PM 아..좀 더 문서를 보자.

hoyoul — 11/03/2023 8:54 PM 주말까지 해서 상태정보를 얻어오는것까지 해보자. 오늘은 여기까지 하자.

11.4

==================== [ 11. 4 ] =================== [9:59 AM] 버튼을 눌러서 container의 색을 바꾸는 프로그램을 stateless widget으로 작성할때는 다른 방법을 써야 한다. 위에 처럼 page를 stateless custom widget으로 만들고 widget들을 안에 배치하고 container의 color속성만 바꾸는 식은 안된다. (edited) [10:01 AM] container를 custom stateless widget으로 만들고, 매번 버튼이 눌려질때 마다, container widget을 새로 만들어야 한다.

hoyoul — 11/04/2023 10:18 AM 내가 잘못 생각하고 있던것: page만 custom widget으로 만들고, page에 들어가는 widget을 customize하지 않고 lego block으로 조립하듯 만들어야 한다고 생각했다. 그런데, 적극적으로 custom widget을 만들어 사용 해야한다는 것이다. 조금이라도 state변화가 있다고 생각되면 만들어야 한다. (edited) [10:21 AM] 그래서 어떤 project를 시작하더라도, Pages폴더와 Components폴더를 만들고 시작한다. 대부분 그렇게 한다.

hoyoul — 11/04/2023 10:33 AM 동영상마다 flutter page를 만드는 방식은 제각각이다. 그래서 나도 별뜻없이 빠른식으로 작성하기 위해서, 1 page짜리는 바로 작성했다. pseudo code로 쓰자면, void main(){ runApp(MyHome()); } class MyHome extends statelessWidget{ widget build(){ return MaterialApp( home: Scaffold( body: SafeArea() ),) } } (edited) [10:35 AM] 이런식으로 page를 만들었는데, page를 떼어내서 별도로 만드는게 맞는거 같다.

hoyoul — 11/04/2023 10:45 AM —점심먹기전까지 겨울이불 빨고 밥먹고 다시하자. 내가 정확하게 모르는것: stateless로 page를 만들었다. page안에 button을 누르면 page안의 container의 색이 변하게 하고 싶었다. stateless로 만든 page가 생성이 되면, build()를 통해서 widget tree가 만들어진다. 이렇게 만들어진 widget tree는 변경되지 않는다. 따라서 wiget tree에 있는 button을 눌렀을때, container의 색은 변하지 않는다. 왜냐면 widget tree의 root가 stateless하기 때문이다. 그러면 버튼이 눌렸을때,root의 build()를 호출해서 새롭게 작성하면 될 줄 알았다. 그런데 안된다.

hoyoul — 11/04/2023 1:41 PM button을 눌렸을때 color를 주어 container를 생성시키는 것도 동작되지 않는다. hoyoul — 11/04/2023 2:24 PM 물론 stateful을 사용하면 간단하게 된다. 그런데 내가 지금 하고 싶은건, 사람들이 말로만, stateless는 재생성해야 한다고 말만하고 test해본 사람이 없는거 같아서 stateless widget으로 하면 얼마나 불편하고 안좋길래 stateful을 사용하는지 직접 해볼려는 것이다. 이것에 해답은 key에 있는거 같다. key를 근데 잘 모른다. key좀 공부해보자. hoyoul — 11/04/2023 2:47 PM 아! 확실히 왜 상태관리를 해야되고 inherited widget을 사용해야 하는지 알겠다. stateless widget으로 page를 만들고, page의 container와 button을 만들고, button을 눌렀을때 container의 색이 변하게 하고 싶었다. 이것을 할려면 button을 눌렀을때 widget tree를 다 바꿔주는 방식 밖에 없다. 즉 onPressed: (runApp(color))을 해준다.

이렇게 하면, container만 새로 생성하는게 아니고, widget tree를 아예 새로 생성하는 것이다. 즉, container만 새로 생성하는 방법은 stateless를 사용하면 애초부터 없다. 불가능하다. 유일한 방식이 widget tree를 새로 만드는 것이다. container만 새로 생성하는 방식이 stateful방식이였다. stateless로 page를 만들면 page안에 포함된 widget의 상태를 반영하는 방식은, runApp을 다시 호출해서 widget트리를 새로 만들던가, 아니면 stateful을 사용해야 한다. stateless로 page를 만들다던가, 상태변환이 필요한 widget을 포함하는 container를 stateless로 만든다면, 변화된 상태를 반영하기 위해서 새롭게 widget tree를 만들고, widget tree의 top에서부터 말단 container까지 color 데이터 값을 전달할 수 밖에 없게 되는것이다. 데이터의 이동길이가 너무 길다. 그리고 변경될 데이터가 10개라면 10개 모두 root widget 생성시에 넣어줘야 한다. 따라서 inherited widget을 사용하거나, 상태관리를 해야한다. 직접 코딩해 보니, 너무 불편하다. runApp()을 호출한다는 것은 page, Center, Column 다 생성자로 전달해줘야 한다. 코드도 복잡하고 시간도 많이 걸린다. 즉 stateless에서 상태를 반영한다는게 너무 힘들다. 빨간색이던 stateless page의 container가 blue로 바뀌었다. 이것은 onPressed: runApp(mycolor: Colors.blue) 처럼 widget tree를 다시 만들기 위해서 runApp()을 다시 호출한것이다.

stateless2.png stateless widget이나 stateful widget은 widget을 상속받고 widget은 immutable이다. 이말은 위에서 봤듯이 widget tree를 만들면 widget tree변경이 안된다. 상태를 반영하기 위해서 runApp()을 호출했던것 처럼, widget tree의 재생성이다. 이것을 하지 않기 위해서 stateful widget을 사용하는데, stateful의 state 객체가 이런일을 한다. 즉, root에서 데이터 전달하지도 않고 처리가 가능하게 해준다. (edited) [3:29 PM] stateful의 lifecycle을 좀 살펴보자. [3:29 PM] 이 문서를 우선 보자.

hoyoul — 11/04/2023 4:48 PM 내가 stateless widget으로 page를 만들고, button을 눌렀을때 container의 색깔을 바꿀때, 고민을 했던건 page에 container가 있으니까, 그것을 onPressed: container(color)로 생성하고 widget tree에서 붙여주면 되지 않을까?라고 생각했었다. 이 생각을 그대로 구현한게 stateful 방식이다. [4:50 PM] stateless로 만든 page는 widget tree에 붙일수 없다. 그래서 전체 widget tree를 재생성 해야만 상태가 변화된다. 그런데 stateful widget에선 sub widget tree만 갱신한다고 보면된다. 물론 sub widget tree에서 stateful한것은 갱신되고, stateless한것은 새로 만들어진다. [4:50 PM] stateful로 만들어보자.

hoyoul — 11/04/2023 5:05 PM stateful로 만드는 것은 간단하다. convert라는 기능을 써서 바꾸면된다. context actions라는 menu에 있다. wrap widget with…이거 있는데 있다. [5:07 PM] stateful로 바꾼 후 해줘야 할께, State클래스에서 onPressed: (){setState()}를 호출 하는것이다. setState에서는 stateful class의 color값을 바꿔주면 된다. [5:09 PM] State class에 있는 mycolor와 같은 멤버변수를 stateful class에서 접근하는 방식은 widget.mycolor처럼 접근할 수 있다. 이렇게 하면 stateless에서 root widget을 생성한것과 비교도 할수 없을 정도로 간단해 진다.

hoyoul — 11/04/2023 5:15 PM 하지만 이렇게 stateless를 editor의 도움으로 stateful로 만드는것에 앞서서 직접 만들어봐야 한다. 그리고 stateful은 dirty, clean상태 말고도 life cycle에 따른 호출되는 함수들이 있다. 이것을 하나하나 print문으로 직접 찍어봐야 한다.

hoyoul — 11/04/2023 7:52 PM stateful에 대한 좋은 그림이 있어서 가져왔다. 출처는:https://anmol-gupta.medium.com/stateless-vs-stateful-widget-in-flutter-b0a25ccd0707

Figure 5: stateless lifecycle1

Figure 5: stateless lifecycle1

stateless-life2.png 위의 life cycle에 따라서 함수호출을 찍어보자.

11.5

————————–[ 11 . 5 ] —————————– [10:16 AM] 오늘 해야할일 (1) emacs settings: 미뤄뒀던 emacs작업, tex 작업, flutter와 rails를 위한 setting작업. (2) 비동기와 stateful을 이해했기때문에 google_signed_in example 코드분석과,이해. (3) google_signed_in example 코드를 이해했다면, 인증도 할수 있고, profile정보도 꺼내오는 예제 만들기. (4) (3)이 됐다면, k-holdem flutter 코드 분인증및 정보 꺼내는 코드를 작성할 수 있다. 어쩌면 rest api를 위한 json 1.0 api공부를 별도로 해야할지도 모른다. 이건 아마도 내일해야 할꺼 같다. [10:16 AM] (1)-(3)까지만 오늘 했으면 좋겠다.

hoyoul — 11/05/2023 10:18 AM => 내일부터는 헤드라인을 사용해야 겠다. 예를 들면…. (edited) [10:20 AM] discord나 slack은 irc계열인데, emacs와 연결할려면 erc로 연결하면 될꺼 같다. [12:54 PM] 그런데 erc는 irc 프로토콜이라서 discord나 slack과 직접 통신을 할수 없다. 방법은 discord protocol을 알면, client 프로그램을 만들어서 사용해야 한다. [12:55 PM] 구글링하니까, discord protocol에 맞는 emacs client package는 없는거 같다. [12:55 PM] 대신 bitlbee라는 중간 서버를 사용한다. [12:56 PM] 그럼 일종의 proxy server로 보면 된다는 건데… [12:57 PM] https://www.bitlbee.org/main.php/news.r.html

hoyoul — 11/05/2023 12:59 PM bitlbee는 범용이라서 slack protocol wechat skype..등등 다양한 protocol을 지원하는데, discord를 위한 bitlbee가 plugin으로 제공된다. [12:59 PM] https://github.com/sm00th/bitlbee-discord 그럼, emacs에서 erc로 bitlbee를 통해서 discord와 통신이 가능하다는 건데… [1:00 PM] 우선 해보자.

hoyoul — 11/05/2023 1:17 PM brew install로 bitlbee를 설치한다. [1:17 PM] discord plugin을 설치해보자. [1:19 PM] github에서 repo를 clone하고 github에 나온대로 make작업을 해보자.

hoyoul — 11/05/2023 1:24 PM autogen.sh를 실행하니까 configure.ac:35: warning: AC_PROG_CC_C99 is obsolete; use AC_PROG_CC가 발생했다. [1:24 PM] configure.ac를 수정해보자.

hoyoul — 11/05/2023 1:43 PM ./confiugre로 makefile을 만들고 make를 했다. 엄청난 수의 warning과 에러가 나온다. [1:48 PM] 대부분 glib관련이다. 소스설치는 linux를 기반으로 하다보니, mac에서는 이런 에러나 warning이 많다. [1:49 PM] libtool을 다시 설치해야 하나? mac에서 package를 받아와서 build해서 사용할때 brew를 사용하지, gnu autotools를 사용하지 않는다. 그런데 gnu autotools를 사용하는 경우는 충돌나는 경우가 많다. linux를 사용하면 이점은 편하다. 자유롭게 gnu aototools 사용하니까..ㅠ (edited)

hoyoul — 11/05/2023 2:04 PM warning이나 info는 어떤 build system에서도 나오는것이고, 일종의 guide line이라서…compiler의 제안?정도..신경 안써도 되는데, 에러가 나오면…신경이 많이 쓰인다. 못 고쳐서가 아니라, 에러부분을 고치는건 문제가 아닌데…그게 공유라이브러리일 경우…다른 application도 사용하는거라서 손이 안간다. 지금 g_memdup2로 바꿔야 하는데, 바꾸면 다른 application이 g_memdup를 사용한다면 실행이 안되거나 에러가 날거 같다. [2:05 PM] 최악의 경우는 system 재설치라서…그냥 여기서 포기하기로한다. [2:06 PM] 백업만들고 하면되는데…업무용 컴퓨터라서 모험은 하지 않기로 한다.

hoyoul — 11/05/2023 4:15 PM brew 경로 문제 [4:16 PM] intel chip에선 /usr/local에서 brew package들이 설치 되었는데, m1에선 /opt/homebrew 에 설치된다.

hoyoul — 11/05/2023 4:36 PM homebrew로 설치된 bitlbee는 /opt/homebrew에 lib, sbin, bin과 같은 폴더를 만들었다. 하지만, plugin은 configure와 makefile이 모두 usr/local에 설정되고 설치된다. 즉 homebrew에서 설치된 package와 homebrew를 사용하지 않는 package의 충돌이다. homebrew를 사용하지 않는 package를 usr/local에 설치했는데…이 위치는 옛날 인텔칩에서 homebrew의 package설치경로다. 물론 인텔칩의 homebrew만 사용하는 폴더는 아니다. 원래 linux에서도 사용되는 login user들을 위한 범용적인 package설치용 폴더기 때문이다. 이것도 제대로 bitlbee가 설치문제가 되는 이유기도 하다. homebrew에서 bitlbee를 m1을 위해서 만들던지, 아니면 bitlbee도 소스 설치를 해야 한다. brew 설치를 하면 안된다. (edited) [4:38 PM] bitlbee를 brew에서 uninstall하자.

hoyoul — 11/05/2023 5:05 PM github에서 release tag를 보면, bilbee는 2019년도, bitlbee-discord 는 2021년도다. m1 chip에 대한 처리가 없는듯핟.

11.6

(1) Google_Signed_In example 분석

hoyoul — 11/06/2023 9:10 AM https://pub.dev/packages/google_sign_in/example 여기 있는 예제를 바로 실행하면 되지 않는다.

hoyoul — 11/06/2023 9:18 AM code by code로 보고 이해가 안가는 거 써보자. [9:18 AM] import문 1). import ‘dart:convert’ show json; show json? 처음 보는 import 표현이다. (edited) [9:21 AM] as라는 keyword는 본적이 있다. namespace를 자신이 지정한 이름으로 바꾸니까, python에도 있다. 그런데 show는 flutter에서 처음 본다.

hoyoul — 11/06/2023 9:31 AM python의 from import와 비슷한거 같다. import문에 쓰인대로, dart-sdk 폴더에 가면 convert라는 모듈이 있고, 거기에 json.dart가 있다. 이 파일만 load해라. 그런 뜻 같다.

Figure 7: exmaple1

Figure 7: exmaple1

import ‘dart:convert.json.dart’; 와 동일한 뜻 아닌가? [9:35 AM] dart.dev에는 너무 간단하게 설명되어 있다. https://dart.dev/language/libraries#importing-only-part-of-a-library json.dart를 열어보니, 일반적인 dart와는 다르다.

example2.png 다른 import문은 해석의 어려움은 없다.

Figure 9: exmaple3

Figure 9: exmaple3

dart는 dart-sdk에서 가져오니까, 별다른 처리가 없어도 된다. [9:51 AM] package는 flutter, google_sign_in, http가 pubspec.yml에 설치 되어 있어야 하고, flutter pub get으로 install 해주면 설치하고 위와 같이 import로 load하면된다. [9:51 AM] src는 소스에 있는것이고… [9:54 AM] 2.signInDemo stateful widget 분석 (edited) [9:56 AM] stateful widget은 왜 써야하고, 쓸수 밖에 없고 어떻게 쓰는지는 이미 공부했다.

example4.png initState를 보자.

hoyoul — 11/06/2023 10:19 AM 코드는 두개의 부분으로 나눠진다. 첫번째 부분: listener _googleSignIn.onCurrentUserChanged는 stream 객체다. 왜냐면 listen()를 사용하기 때문이다. stream에서 공부했듯이, 이것은 listener다. listener를 정의하는 code다. 두번째 부분: _googleSignIn.signInSilently() 호출 (edited)

hoyoul — 11/06/2023 10:27 AM 대충 뭐하는 건지는 알겠다. 우선 착각하지 말아야 할것은 initState는 state관점에서 초기화하는 것이지, 객체 초기화하곤 다르다. 변하는 상태에 focus를 맞추기 때문이다. 여기서는 google에 login한 사용자가 변할때 처리하는 코드다. [10:28 AM] 그래서 이부분을 먼저 이해하는 것보다. 우리가 원했던, consent 화면을 보여주는 code를 해석하고 그에 따른 상태변화의 코드로 initState코드를 이해하면 된다. [10:30 AM] 그렇게 하기위해서 button을 눌렀을때, login하는 부분을 먼저 분석하자. 왜냐? 우리가 그렇게 사용할것이기 때문이다. build를 보자. build()

Figure 11: example5

Figure 11: example5

별거 없다. 물론 나는 constrainedBox나 appBar의 속성이나 세부 사항은 잘 모른다. 그런데 몰라도 된다. flutter code는 stateful과 비동기를 알면 뼈대를 아는거라서 나머지는 찾아가면서 그때 그때 공부하면 된다. [10:33 AM] _buildBoy를 보자.

Figure 12: example6

Figure 12: example6

코드는 두 부분으로 구성되어 있다. google login인증이 성공했을때, 인증을 하지 않았거나 실패했을때 [10:37 AM] 우리는 버튼을 눌렀을 때 consent화면이 보이는 부분을 보고 싶은것이다. 인증하지 않았을때를 보면된다. [10:37 AM] return Column( mainAxisAlignment: MainAxisAlignment.spaceAround, children: <Widget>[ const Text(‘You are not currently signed in.’), / This method is used to separate mobile from web code with conditional exports. / See: src/sign_in_button.dart buildSignInButton( onPressed: _handleSignIn, ), ], ); [10:38 AM] _handleSignIn()를 보면 된다. [10:38 AM] _handleSignIn() (edited) [10:39 AM]

example7.png 찾았다! 이 부분이다. 그냥 단순하다. \_googleSignIn.signIn()를 호출할 뿐이다. [10:48 AM] Future, aync, await같은 코드는 이제 쉽게 이해되고 너무 당연하다. await는 값을 가져올때 runtime때 녹아버릴것이다. [10:48 AM] 여튼 consent화면을 보여주는건 직접 예제를 만들어서 봐보자. 진짜 consent화면이 떠지는지... [10:50 AM] 버튼 누를때 consent screen Demo [10:50 AM] flutter create consent_screen (edited) [10:51 AM] project를 만든다.

hoyoul — 11/06/2023 10:54 AM 우선 그 전에 빠르게 개발할 수 있도록 snippet을 만들어 놔야겠다. [10:54 AM] main부분과 page부분을 yasnippet에서 사용하게 등록하자.

hoyoul — 11/06/2023 11:24 AM main,page이름으로 snippet을 만들었다. 테스트해보자.

hoyoul — 11/06/2023 11:36 AM snippet 정상동작 확인! google_signed_in package를 설치하고 install하자. [11:38 AM] flutter pub add google_sign_in 하면 설치까지 된다.

hoyoul — 11/06/2023 11:44 AM 구현 [11:45 AM] 가져올 scope를 미리 정의: scope라는것은 GCP에서 설정한건데, google login했을때, 사용자의 개인정보들(resources)의 꺼낼수 있게 permission을 미리 설정했다. 여기서는 email정보와 contact정보를 가져오겠다고 선언한것이다. 즉 login이 되면 해당 정보를 가져온다. / The scopes required by this application. const List<String> scopes = <String>[ ’email’, ‘https://www.googleapis.com/auth/contacts.readonly’, ];

GoogleSignIn _googleSignIn = GoogleSignIn( / Optional clientId / clientId: ‘your-client_id.apps.googleusercontent.com’, scopes: scopes, ); (edited) [11:46 AM] SignIn()를 구현 Future<void> _handleSignIn() async { try { await _googleSignIn.signIn();} catch (error) { print(error);}} [11:47 AM] button에서 onPress: _handleSignIn,으로 호출하게 한다. [11:47 AM] -에러 (edited) [11:47 AM] flutter: MissingPluginException(No implementation found for method init on channel plugins.flutter.io/google_sign_in)

hoyoul — 11/06/2023 11:53 AM ios에서 테스트 할려면 pod install해줘야 하기 때문에 ios폴더에서 pod install을 했다. [11:53 AM] 에러가 발생했다. 우선 podfile을 열어보았다. 주석 처리된 부분인 버전을 12로 바꿨다.왜냐면 이게 xcode에서 보면 warning이 나오기 때문이다. => platform :ios, ‘12.0’, 나머지는 잘 몰라서 pass하고 다시 pod install을 해봤다. pod install Analyzing dependencies Downloading dependencies Generating Pods project Integrating client project Pod installation complete! There are 2 dependencies from the Podfile and 6 total pods installed.

[!] CocoaPods did not set the base configuration of your project because your project already has a custom config set. In order for CocoaPods integration to work at all, please either set the base configurations of the target Runner to Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig or include the Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig in your build configuration (Flutter/Release.xcconfig). (edited)

hoyoul — 11/06/2023 12:01 PM xcode를 열어서 실행해 보기로 한다. 8개정도의 warning은 생긴다. 그리고 error가 발생한다.

lutter: PlatformException(missing-config, GoogleService-Info.plist file not found and clientId was not provided programmatically., null, null) => 이것은 gcp에서 만든 plist등록이 필요하다는 에러다. 이걸 ios에 추가해야 한다. 이 부분을 처리해보자. 이 plist에는 아마도 google server에 접근할수 있는 인증관련과, 서버주소가 있을 거 같다. 그래야 app에서 server 의 api를 사용할 수 있기 때문이다. 아래는 에러처리의 일반적 과정이다. 에러 발생시 처리 (edited) [12:03 PM] flutter clean : build cache를 지운다. 설치된 package들도 모두 지운다. flutter pub get: package를 다시 설치한다. ios폴더로 가서 pod install : ios 관련 package 설치 pod update: 설치된 package update

hoyoul — 11/06/2023 12:26 PM GoogleService-Info.plist file missing 처리 [12:31 PM] googleService-info.plist는 gcp의 oauth2.0 credentials에서 만들어야 하는데, 그 부분은 처리하지 않고 consent화면만 만들었었다. 이것은 google에 접속하는 인증정보가 들어있다고 보면 된다. (edited) [12:32 PM] 밥먹고 이어서…

hoyoul — 11/06/2023 1:32 PM GCP에 들어가서 credentials를 만들어야 한다. (edited)

Figure 14: credentials1

Figure 14: credentials1

Figure 15: credentials2

Figure 15: credentials2

Figure 16: credentials3

Figure 16: credentials3

순서대로 따라 하면 된다. 여기서 bundle id만 설명하면, bundle id는 app의 identity를 나타낸다. flutter project의 ios를 xcode로 열어서 설정된 bundle id를 위의 bundle id에 넣고 plist를 만들면 된다.

Figure 17: client1

Figure 17: client1

이렇게 해서 만든 plist를 GoogleService-Info.plist로 이름을 바꾼다.

hoyoul — 11/06/2023 2:04 PM GoogleService-info.plist란? [2:05 PM] credential 정보를 가지고 있는 파일인데, 설정할 때 봤듯이 bundle id만 설정한다. bundle id를 가진 app이 GCP에 연결할 수 있는 client ID로 보면 된다. [2:06 PM] 그래서 이 파일에는 client ID가 있다고 표현한다.

client2.png 실제로 client ID가 있다.

hoyoul — 11/06/2023 2:18 PM client ID만 있으면 gcp에 연결 할 수 있다. 한번 해보자. [2:18 PM] plist를 사용하지 않고 하드 코딩해서 접근해 보자. [2:19 PM] GoogleSignIn _googleSignIn = GoogleSignIn( / Optional clientId / clientId: ‘your-client_id.apps.googleusercontent.com’, scopes: scopes, clientId: ‘123122320712-3hbsn6g38giv6tnp42sn7955nf0eu2ot.apps.googleusercontent.com’, ); [2:22 PM]

Figure 19: client3

Figure 19: client3

app이 깨지면서 에러가 발생된다. [2:23 PM] flutter: PlatformException(google_sign_in, Your app is missing support for the following URL schemes: com.googleusercontent.apps.123122320712-3hbsn6g38giv6tnp42sn7955nf0eu2ot, NSInvalidArgumentException, null) Lost connection to device. [2:24 PM] URL schemes 에러 [2:24 PM] url scheme은 uri가 가진 resource에는 resource를 나타내는 scheme이 있는데, 이 scheme에 따라 uri 표기도 달라지고 처리 방법도 달라진다.

hoyoul — 11/06/2023 2:25 PM app이 URL scheme을 인식하지 못한다는 것이다. 즉 url 형태이지만, gcp접근할때 app에서 바꿔서 접근할 수 있게 처리를 해줘야 한다. [2:25 PM] 좀 찾아보자. [2:28 PM] app의 info.plist에서 다음 키와 value를 추가한다. <key>CFBundleURLTypes</key> <array> <dict> <key>CFBundleTypeRole</key> <string>Editor</string> <key>CFBundleURLSchemes</key> <array> <string>com.googleusercontent.apps.123122320712-3hbsn6g38giv6tnp42sn7955nf0eu2ot</string> </array> </dict> </array>

성공했다.

</img/oauth/consent1.mp4>

원했던 consent 화면은 처리했다. [2:38 PM] login 성공시 redirect 처리, (edited) [2:39 PM] email주소와 contact를 scope로 요구했었는데, 가져왔는지도 확인해야 하고, consent화면이 끝난뒤 redirect는 어떻게 해야 하는지 잘 모른다. code를 해석해야할 듯하다. [2:39 PM] 10분 breaking

hoyoul — 11/06/2023 2:58 PM client_id를 하드코딩하지 않고 gcp에서 받은 plist만 ios/Runner에 넣으면 작동하게 하는게 더 좋지 않나? 란 생각을 한다.

hoyoul — 11/06/2023 3:06 PM 우선 동작하기 때문에 넘어간다. client_id는 http 스킴으로 보면 http://google/client_id=123123123 … 뭐 이런 의미지 않을까한다. [3:11 PM] login을 해봤다. 다음과 같은 에러가 발생했다.

Figure 21: error1

Figure 21: error1

scope와 관련이 있는거 같다. 내가 gcp에서 설정할때, scope에 대해서 아무것도 지정하지 않았다. 하지만, app에서 요구하는 scope는 email주소와 contact정보였다. [3:13 PM] 음…그러면 email주소와 profile을 code에서 요청하게 하고, gcp에서는 해당 resource에 대한 permission을 주고 다시 테스트 해보자. (edited)

hoyoul — 11/06/2023 3:15 PM code const List<String> scopes = <String>[ ’email’, ‘profile’, // ‘https://www.googleapis.com/auth/contacts.readonly’, ];

  • gcp

Figure 22: retest1

Figure 22: retest1

에러없이 진행되었다. 다만 redirect page가 없기 때문에, 제자리로 온다. 이전에 만든 k-holdem 피그마 보고 만든 화면으로 다시 테스트해서 redirect하는것까지 해보자. redirect가 되고 정보를 가져오는거까지 하면, kholdem flutter 소스를 분석하고 수정하고 테스트하는 작업으로 진행될듯하다.

</img/oauth/aaa.mp4>

아…근데 좀 이상한게 있다. consent화면이 맞는지 모르겠다. 나는 consent에 이미지도 넣었는데, 이미지가 보이지도 않는다. [3:30 PM] 그리고 사용자에게 어떤 정보를 꺼내오겠다는 notice같은것도 보여져야 하는거 아닌가?

hoyoul — 11/06/2023 3:36 PM -sign out 처리 [3:37 PM] 좀 이상한것들은 내가 아직 몰라서 그런거니, 차차 하기로 하고 쉬운것부터 하자. login했으니 log out도 하자. 간단하다. [3:43 PM] disconnect만 해주면 된다. sign in후 sign out하면 다시 google login창이 보인다. log out도 처리했다.

Figure 24: logout

Figure 24: logout

redirect page를 어떻게 만들 것인가? [3:51 PM] 위에 보이는 page는 build()에서 만들어주는 page다. 나는 page를 stateful하게 만들었다. 그래서 상태에 따른 화면 변환이 자유롭다. 처음에는 sign in버튼만 만들고, sign이 성공하면 signout버튼만 보이게 하면 된다. 새로운 redirect page를 만들 필요가 없다. 왜냐? stateful 로 page를 만들었기 때문이다.

hoyoul — 11/06/2023 4:06 PM 음…요기서 좀 생각이 필요하다. 우선 상태에 따라 다른화면이 보여줘야 하니깐..stateful이 관리하는 상태는 login 성공여부로 하면 될듯하다. [4:07 PM] 나는 login 처리를 다음과 같이 했다. Future<void> _handleSignIn() async { try { await _googleSignIn.signIn(); } catch (error) { print(error); } } [4:08 PM] signIn()가 return하는게 무엇인지 확인하고, 그것을 stateful의 property로 처리해야 한다. 그것이 상태니까…

hoyoul — 11/06/2023 4:14 PM google_sign_in example이 내 생각과 비슷하다. 물론 약간의 차이는 있지만, 거의 비슷하다. 참고해서 구현해보자. 우선, 예제에서는 두개의 상태를 관리하는거 같다. login성공과 authorized 성공. signin()가 성공해서 return하는 객체가 GoogleSignInAccount라는 객체다. [4:15 PM] 10분만 쉬자.

hoyoul — 11/06/2023 4:26 PM 아..authorize는 별개다. 내가 좀 이상하다고 한게 google login후에 consent화면 없이 login만 성공해서 이전 page로 돌아오는 과정이였다. consent page에 내가 이미지도 넣고, 어떤 resource를 사용하는지 user가 알수 있는 notice라도 보여줘야 하는거 아니냐고 했는데…별도로 요청하는 함수가 제공된다. 그러니까, sigin()와 requestScop()가 있는데, 이것을 실행해야 user가 consent screen에서 ok가 되는거 같다. [4:32 PM] 이 말이 맞는지 확인하기 위해 button을 하나 추가해 보자.

hoyoul — 11/06/2023 4:33 PM requestScope()를 호출하는 함수를 만들자.

hoyoul — 11/06/2023 5:03 PM 버튼을 눌러서 함수 호출하면 signIn()처럼 구글 관련 page가 잠깐 보이고 없어진다. 좀더 봐야 할듯하다. 내가 생각한것은 authentication을 하는 signIn()와 authorization하는 requestScope()가 별도로 있다고 생각했는데, Stream을 사용하는 예제를 보면 다음과 같이 되어 있다. _googleSignIn.onCurrentUserChanged .listen((GoogleSignInAccount? account) async { // In mobile, being authenticated means being authorized… bool isAuthorized = account != null; [5:22 PM] Stream에서 만일 login이 되면 listener함수인 listen()이 호출되는데, 여기서 account객체를 받는다. 이것은 login정보와 scope정보를 가지고 있는 객체다. 주석에 보면 이런말이 있다. mobile에서는 authentication하는것이 authorization하는 것과 같다는걸 의미한다고한다. 따라서 login이 되면 isAuthorize는 그냥 true가된다. 별도로 requestScope로 resource를 사용하겠다는 요청을 안해도 되는것이다. 그러면 내가 gcp에서 작성한 consent screen은 안보이는게 정상인가?

hoyoul — 11/06/2023 5:29 PM 내가 요청한 scope는 email과 profile인데, 이것은 기본 scope라서 별도의 consent screen이 필요없다는 거 같은데…난 api를 사용하기 위해서 consent screen을 보고 싶긴 하다.

hoyoul — 11/06/2023 7:46 PM oauth2.0 flow에 따른 rails와 key [7:47 PM] oauth 2.0 flow를 따르기 위해선 google 인증이후에 token을 받아와서 다시 rails에게 전달하는 인증 flow가 있어야 할듯하다. 서버가 없으면 모르는데, 서버가 있기 때문에 인증 처리과정이 필요하다. (edited) [7:48 PM] 좋은 그림이 있는데, 출처는 https://developers.google.com/identity/sign-in/web/server-side-flow#step_7_exchange_the_authorization_code_for_an_access_token

Figure 25: oaut flow

Figure 25: oaut flow

즉 server를 사용할려면 app에서 key를 받아서 다시 server쪽에도 전달하는 인증 프로세스가 필요하다. google_signed_in package만 사용하면 server없이 app만 사용할때인거 같다. google_signed_in package에선 token을 제공받는거 같진 않다. [7:50 PM] https://pub.dev/packages/oauth2/example

oauth2.0 package를 사용해서 key를 다시 server에 전달하는 과정이 있는거 같다. [7:53 PM] k-holdem에서 어떤 식으로 처리하는지 궁금해졌다. k-holdem flutter 인증 처리 (edited)

hoyoul — 11/06/2023 8:00 PM 우선 pubspec.yaml에 보면 oauth2라는 package는 사용하지 않는다. [8:00 PM] shared_preference가 있는걸로 봐선 이걸로 token관리를 하는거 같다.

hoyoul — 11/06/2023 8:18 PM main화면을 띄운후 인증과정을 SettingController.dart에서 진행한다. [8:18 PM] scope는 email만 받는 기본 scope다. [8:19 PM]

final GoogleSignIn _googleSignIn = GoogleSignIn(
    scopes: ['email'],
  );

인증과정에서 Rxn type을 사용하는데, 이게 getX와 관련된것이다. getX를 보긴했지만, 그땐, stateful을 왜 사용하냐? 안사용하면 안돼? state를 왜 관리해..등등 state에 대한 개념이 없었기 때문에 공부해도 몰랐다. 근데 지금은 stateful 당연히 사용해야 하고 없으면 안되고, state가 뭔지도 알았기 때문에…getX를 다시 볼 필요가 있다. [8:42 PM] About GetX

readme에 보면, flutter project를 처음 만들면 보이는 counter app을 getX로 바꾸는 내용이 나온다. 이걸 따라해보자.

hoyoul — 11/06/2023 8:56 PM counter app은 materialApp으로 app은 android style로 만들어졌고, page는 MyHomePage라는 stateful page로 만들어져 있다. (edited) [9:00 PM] 코드는 simple하다. counter라는 int변수가 상태고, 이 상태를 변화시키는것은 floating button이고, 상태를 반영하는건 text widget이다. [9:00 PM] stateful로 작성된 counter app은 별다른게 없다. 내가 지금 짜도 동일하게 짤것이기 때문이다. [9:01 PM] 그럼 getX를 적용해보자. [9:02 PM] (1) getX package를 install하고 load하자. [9:03 PM] install: flutter pub add get [9:03 PM] load: import ‘package:get/get.dart’;

hoyoul — 11/06/2023 9:05 PM (2) Step 1: Add “Get” before your MaterialApp, turning it into GetMaterialApp (edited) [9:06 PM] void main() => runApp(GetMaterialApp(home: Home())); [9:07 PM] MaterialApp을 GetMaterialApp으로 바꾼다. [9:08 PM] 여기서 드는 생각은 당연히 왜?이다. [9:09 PM] 우선 MaterialApp에서 GetMaterialApp()으로 바꾸었는데, GetMaterialApp도 type이다. 왜냐면 required named parameter로 home:이 있기 때문에 MaterialApp과 같은 type으로 보인다. [9:09 PM] definition을 보자. class GetMaterialApp extends StatelessWidget {} stateless widget을 상속 받는다. (edited) [9:11 PM] 아…그리고 여기서 중요한 차이가 있는데, 기존 counter는 mainApp(MyApp())으로 되어 있다. MyApp은 custom stateful widget이다. 즉 getX로 변경하려는게 여러 코드를 수정해야 한다. MyApp을 사용하지 않고 runApp에서 materialApp을 생성했다면 위와 같이 바로 수정하면 되지만, 그렇지 않다. [9:12 PM] 즉, MyApp이란 stateful widget을 사용하지 말라는 의미로 보일 수 있다. 왜냐면 자식인 MaterialApp도 stateful을 상속받는 widget이고 MyApp도 stateful이다. (edited)

hoyoul — 11/06/2023 9:15 PM MyApp(MaterialApp:stateful) -> GetMaterialApp(stateless) (edited) [9:18 PM] definition을 보면 builder란걸 사용하는데, builder에 따라서 MaterialApp을 child로 가질수도 있다.

hoyoul — 11/06/2023 9:22 PM 슈퍼좀 가자. 갔다와서 시간나면 다시 보던지 내일 보던지 하자..

hoyoul — 11/06/2023 9:52 PM 내가 아는 MaterialApp은 android look & feel에 맞춘 설정이 들어간거..그래서 theme이 있고, 이 app에 포함되는 page들이 android look & feel을 유지한다? 그정도인데, GetMaterialApp은 MaterialApp에 뭘 넣었는가? Material을 자식으로 가지고 있는거 봐선, android loo&feel은 유지하는거 같은데… 즉 routing기능, Material은 navigator로 하는데, 이것을 GetMaterialApp은 다른걸로 한다는것이고, getX하면 상태관리가 떠오르듯, 다른 상태관리를 하기때문에 root widget을 GetMaterialApp으로 바꾼듯 하다.

routing이 materialApp과 getMaterialApp의 주요 차이점이다. 그러면 MaterialApp의 navigater 방식에서 GetMaterialApp의 방식으로 왜 바꿨을까?

MaterialApp은 stateful하다. 이 말은 navigating해서 새로운 화면page가 화면에 보여진다는 것은 기존화면 page와 새로운 화면 page를 비교해서 dirty한지 안 dirty한지 판단하고 다시 그려진다는 것이다. stateful은 재사용이다. stateless처럼 객체를 그냥 dispose하고 새로 생성하지 않는다. 비용이 적게 들어간다. 그런데 화면 page를 바꾸는것은 stateful로 할 필요가 없다. 비용만 많이 들어간다. 그래서 GetMaterialApp은 새로운 화면 page로 바꾸는 navigating, routing을 stateless로 한다. 비용이 덜 들어가기 때문이다. 그래서 MaterialApp이 아닌 GetMaterialApp을 사용한다. (edited) [9:53 PM] 뭐 여러 추가 기능을 넣은 root widget을 사용한다? 그정도만 이해하자. [9:55 PM] Note²: This step is only necessary if you gonna use route management (Get.to(), Get.back() and so on). If you not gonna use it then it is not necessary to do step 1 [9:56 PM] GetMaterialApp을 root widget으로 넣으면 route가 가능하기 때문에 root widget으로 넣은거라고 한다. 만일 route를 안 사용한다면 step1처럼 root widget으로 넣지 않아도 된다고 한다. [9:56 PM] step1은 이해했다. [9:57 PM] (3) step2: Create your business logic class and place all variables, methods and controllers inside it. You can make any variable observable using a simple “.obs”. (edited)

hoyoul — 11/06/2023 9:59 PM business logic이란 말은 일종의 용어인데, db나 data에 대한 처리를 sw공학에선 business logic이라고 부르는데, 나는 좀 별루다. 너무 industrial한 용어라서… [9:59 PM] 여튼 상태 data를 class로 만든다는 거 같다. [10:00 PM] 상태 data를 변수로 하고 그 data의 set,get method같은 것들을 class에 넣는다고 한다. methods와 controller 가 뭔 차이가 있는지는 모르겠다. 여기서는 MVC모델에서 사용되는 용어와 oop용어를 혼합해서 사용하고 있어서, 하는 말이다. method와 controller를 얘기하는데, controller는 MVC에서 그 Controller다. 즉 요점은 getX는 MVC모델을 사용한다는 얘기다. M과 C는 하나의 class로 표현하고 V는 별도의 class로 작성한다. 3개를 따로 작성하지 않는다. 이것은 java를 떠올리게 한다. small talk은 MVC를 서로 독립적인 class로 나타낸다. 반면에 java의 swing에서 widget을 만들때 M과 C를 묶었었다. swing에서 사용하는 위젯처럼 getX도 흉내내는 것이다. getX를 사용하지 않으면 stateless widget이나, stateful widget을 만들때 Modle View Controller가 한몸이다. 분리 되지 않았다. 이것을 분리하겠다는게 getX의 의도다. 그런데 MVC를 smalltalk widget처럼 만드는게 아니라, swing처럼 만든다. (edited) [10:01 PM] 여기서 observable 이란 단어가 쓰이는데, observer pattern을 생각하면 된다. [10:02 PM] business logic을 나타내는 class가 뭘 상속 받는지는 모르겠지만, .obs()를 사용해서 변수를 observable하게 할수 있다고 한다. [10:04 PM] 기억은 잘 안나지만, 원래 pattern이란것은 gof가 책에서도 말했듯이 객체간의 관계에 대한 pattern이다. [10:04 PM] 즉 전제는 여러객체가 있다는게 전제다. [10:05 PM] observer pattern은 one to many 관계에서 어떤객체가 변경되면 다른 객체도 그걸 알게 해주는 방식인데….subscriber 패턴하고 똑같았는지는 기억이 안난다. [10:06 PM] 거의 뭐 비슷한거같다.

hoyoul — 11/06/2023 10:07 PM 그래서 뭐 등록을 한 객체들에게 notifier 쏴주고 뭐 그랬던거 같은데…필요하면 구글링 해야겠다. [10:08 PM] 여튼 class안의 변수를 observable하게 한다는것은 그 변수가 변경이되면 notifier를 날려서 observer들에게 알린다? 뭐 대충 이런 말 같다. [10:09 PM] class Controller extends GetxController{ var count = 0.obs; increment() => count++; } [10:09 PM] count.obs가 아니라, literal한테 .obs를 붙이는게 좀 특이하다. [10:10 PM] 물론 dart는 literal도 객체 instance라서 문법적으로 문제가 있는건 아닌데… [10:10 PM] 뭐 더 직관적이긴 하다. [10:11 PM] 즉 counter값이 바뀌면 등록된 다른 객체들에게 알려줘야 할텐데, 등록된 객체는 어디 있는지 모르겠다. [10:11 PM] 물론, 이 값이 바뀌면 UI가 바뀌는 widget이 아마도 등록된 observer들일꺼라고 예상은 되지만, [10:12 PM] 위 code에는 아무것도 나와 있지 않다. [10:13 PM] (4) step3:Create your View, use StatelessWidget and save some RAM, with Get you may no longer need to use StatefulWidget. [10:13 PM] 내일 이어서 하자.

11.7

view를 만든다고 한다. 이것은 observer를 만드는것이다. 위에서 GetX는 MVC로 page를 만든다고 했다. 기존에 page를 나타낼때는 state에 해당하는 data를 stateless나 stateful custom widget에 포함시켰다. data나 view나 가져오는 controller나 하나의 class로 묶었다. 예를들어 버튼을 누르면 container의 색이 변하는 page에서도, model에 해당하는 color data, button을 누를때 color를 변화시키는 onPressed: lambda같은 controller 그리고 color값을 기반으로 ui를 반영하는 container(View)가 모두 하나의 class에 있었다. 즉 page에 MVC가 포함되어 있는 형태다. 그래서 getX는 page를 MVC에서 MC를 하나의 class, V를 또다른 class로 분리를 했다. MVC 모델을 채용한 것이다. 여튼 getX에선 page를 표현하기 위해, Model를 만들어야한다. 그리고 Model에 포함시키는 data들이 있게 되는데, 그중 상태에 해당하는 literal data는 .obs를 붙여줬다. 왜냐면 View에서 관찰하는 데이터라는것을 표현하기 위해…요시찰 데이터? 라는 꼬리표를 붙여줬다. (edited)

hoyoul — 11/07/2023 9:16 AM 요약은 여기까지 하고 다시 step3를 보자. view를 만드는 과정을 얘기한다. view를 stateless로 만든다. stateless로 만들면 비용이 덜나가고, stateful을 안써도 된다…뭐 이런 얘길 한다. 근데 이것은 getX의 routing과도 관련이 있다. routing, navigating은 page를 이동하는 것이다. MaterialApp의 입장에서 보면 UI변화는 상태변화고, stateful widget으로 만들어진 MaterialApp은 State객체에서 dirty여부를 판단해서 다시 widget tree를 비교를해서 만드는데, 이게 계산과정이 많고 비용이 많이 나가는 작업이다. 그래서 MaterialApp은 문제점을 이미 가지고 있는 widget이였다. 더군다나 Material App은 root widget으로 사용되기 때문이다. page를 이동한다는건, page를 완전 싹 바꾸기때문에 stateful로 이전 page와 새로운 page를 비교할 필요 자체가 없는데, 쓸데 없는 비교를 하는것이기 때문이다. 그것은 MaterialApp이 stateful이기 때문이다. GetX는 그점을 지적한 것이다. [9:23 AM] GetX가 다루는 것은 routing과 MVC모델 사용이다. 그리고 다루는 widget의 단위는 page다. page를 교체하는 routing을 효율적으로 하기 위해선 기존의 page들이 stateful이여서 매번 비교를 하기 때문에 getX에서 사용하는 page는 stateless로 한다. 그리고 page는 MVC로 만든다. 그중 V에 해당하는 widget을 stateless로 만든다는 얘길 하는 것이다. 아래는 그 예를 보여준다. (edited)

 class Home extends StatelessWidget {

  @override
  Widget build(context) {

    // Instantiate your class using Get.put() to make it available for all "child" routes there.
    final Controller c = Get.put(Controller());

    return Scaffold(
      // Use Obx(()=> to update Text() whenever count is changed.
      appBar: AppBar(title: Obx(() => Text("Clicks: ${c.count}"))),

      // Replace the 8 lines Navigator.push by a simple Get.to(). You don't need context
      body: Center(child: ElevatedButton(
              child: Text("Go to Other"), onPressed: () => Get.to(Other()))),
      floatingActionButton:
          FloatingActionButton(child: Icon(Icons.add), onPressed: c.increment));
  }
}

class Other extends StatelessWidget {
  // You can ask Get to find a Controller that is being used by another page and redirect you to it.
  final Controller c = Get.find();

  @override
  Widget build(context){
     // Access the updated count variable
     return Scaffold(body: Center(child: Text("${c.count}")));
  }
}

class Home extends StatelessWidget 홈이라는 page를 stateless로 만든다. 위에서도 말했듯이 이것은 view page다. [9:32 AM] final Controller c = Get.put(Controller()); 이 부분이 생뚱맞다. 갑자기 Controller, Get.put()가 사용된다.

hoyoul — 11/07/2023 9:53 AM GetX에선 page를 MVC를 사용하기 때문에 view와 business logic이 decoupling된다고 했다. 그래서 Controller라는 model class를 step2에서 만들었다. 여기는 stateless로 구현된 View다. View에 Model에 해당하는 Control을 생성해서 View에 inject시킨다? 그러면 이전 widget처럼 MVC가 widget 에 다 있는거 아닌가? 분리한거 아니였냐? 근데 다시 inject하면 분리가 아니잖아? 라고 말할 수 있는것이다. 왜 View에 다시 Model(Control)을 inject했냐?가 나의 의문이다. [9:55 AM] Get.put이라는 코드가 View에 Controller를 inject한게 아닐수도 있다. Get.put을 정확히 알 필요가 있다.

hoyoul — 11/07/2023 10:04 AM Get.put(Controller)의 의미 [10:05 AM] MVC에서 view만 따로 떼어내서 만든 page에는 Controller가 없다. GetXController로 상속한 model을 별도 관리하기 때문이다. decoupling하게 되면 Controller와 View는 자유도가 높아진다. [10:06 AM] Controller는 A라는 view page뿐 아니라, B,C,D라는 page에도 붙여서 사용될 수 있기 때문이다. [10:09 AM] 즉 Get.put(Controller)는 model을 view와 연결(inject:주입)하는 과정이다. 위에서 내가 왜? 다시 Model을 view에 주입하는가? 즉 coupling할바엔 decoupling을 애초에 안하면 되지 않냐?라고 질문했었다. 그런데, decoupling의 자유도는 내가 미쳐 생각지 못했다. 여튼 Get.put을 사용해서 MC와 View를 coupling한다. (edited)

hoyoul — 11/07/2023 10:11 AM 이렇게 하면 예전 widget처럼 mvc모델이 하나의 class에 모두 있던것과 비슷한 상황이 된다. 내가 예전에 test로 버튼이 눌려지면, container색이 변하는 page를 예제로 만든적이 있다. [10:14 AM] 이 page를 stateless widget으로 만들면, runApp()을 다시 수행해야만 했었다. 그리고 stateful로 만들면 button에서 onPress: changeColor(), 를 통해서 Color data를 변경하고, 변경후 setState()를 호출해서 container에 변경된 색상이 적용되게 했다. Get.put(controller)로 View에 controller를 주입하면 stateful에서 하듯이 할 수 있게 된다. 방법은 좀 다르다. (edited)

hoyoul — 11/07/2023 10:22 AM 즉, 버튼이 눌려지면, color값을 변경하는 함수를 호출하는데, 이 부분이 getX를 사용하지 않은 stateful widget에서는 changeColor()를 썼는데, Controller를 사용하는 getX에서는 controller에 정의된 changeColor()를 호출한다. controller.changeColor() 이렇게 사용한다. 별반 차이가 없다. 그런데, GetX package readme의 예제에서는 단순히 controller의 함수를 호출하는게 아니라, 버튼이 눌려지면 page전환도 가능함을 보여준다. 위 code를 보자. (edited)

body: Center(child: ElevatedButton(
              child: Text("Go to Other"), onPressed: () => Get.to(Other()))),
      floatingActionButton:
          FloatingActionButton(child: Icon(Icons.add), onPressed: c.increment));

두개의 button을 눌렀을때, FloatingActionButton은 내가 구현한 changeColor처럼 controller에 정의된 c.increment 함수를 호출한다. 그런데, ElevatedButton은 Get.to함수를 사용해서 other라는 page를 호출한다. GetX를 사용하면 놀랄정도로 편한 page이동이 되는 것이다. GetX에서 button을 눌렀을 때 화면이동 하는 것을, stateless에서는 runApp()로 widget tree를 다시 만들어야 했었고, stateful에서는 setState()을 호출했다. setState는 page의 변경된 부분을 다시 비교해서 화면을 재구성했다. 그런데 버튼을 눌러서 새롭게 보여질 page가 이전 page와 완전히 다르다면, 비교해서 재구성하는건 시간 낭비다. Get.to의 인자로 page를 줘서 이동하는게 너무나도 편리해보인다.

hoyoul — 11/07/2023 11:38 AM 그런데 이렇게 GetX로 page를 이동할때는 주의할 점이 있다. 이전 page의 버튼에서 사용하고 있는 Controller를 새로운 page에도 inject해줘야 한다. 그런역할을 하는게 아래의 코드의 Get.find()다.

class Other extends StatelessWidget {
  // You can ask Get to find a Controller that is being used by another page and redirect you to it.
  final Controller c = Get.find();

  @override
  Widget build(context){
     // Access the updated count variable
     return Scaffold(body: Center(child: Text("${c.count}")));
  }
}

GetX의 view에서 상태값을 반영하는 방법은 stateful, stateless widget과는 다르다. stateless는 상태값 반영이 아예 안되기 때문에, pass하고, stateful widget에서는 setState()를 호출해주면 다시 build함수를 사용해서 ui를 재구성할때, 변경된 상태에 맞게 ui가 반영된다.

hoyoul — 11/07/2023 12:04 PM 그런데 GetX에선, obx란 함수를 사용한다. setState가 lambda를 인수로 취하듯, 비슷하게 obx란 함수도 lambda를 인수로 취한다.

return Scaffold(
      // Use Obx(()=> to update Text() whenever count is changed.
      appBar: AppBar(title: Obx(() => Text("Clicks: ${c.count}"))),

이렇게 처리하는게 낯설긴 하다. [12:08 PM] 즉 상태를 반영할 widget이라면, 그 상태값을 controller에서 관리하니까, 상태를 반영하는 widget은 obx()에서 lambda로 생성해서 사용해라…이런 의미인데…좀 낯선 표기라서…자주 봐야 할듯 하다. [12:10 PM] obx는 상태 data를 반영하는 widget을 생성하는 함수다. 그런데 그 상태 데이터는 controller에서 obs로 표시했다. 뭐 이정도…

hoyoul — 11/07/2023 12:29 PM 근데, 내가 알고싶었던건, kholdem에서 본 Rnx란 generic type이였는데…

hoyoul — 11/07/2023 1:35 PM Reactive State Manager reactive variable을 사용하는데, getX패키지에서 제공하지만 readme에도 설명은 없다. 구글링해서 찾아봐야 한다. (edited) [1:36 PM] https://chornthorn.github.io/getx-docs/docs/state/reactive-state/

getX로 할수 없기때문에 Reactive state manager를 도입해서사용한 건지…Reactive가 익숙해서 섞어서 사용했는지는 모르겠다. 우선 내가 reactive 도 모르고 reactive State Manager도 모르기 때문에 한번 읽어봐야 할 듯하다.

hoyoul — 11/07/2023 1:42 PM 좀 찾아보고 이해가 안가면 가볍게 물어봐야겠다. [1:48 PM] reactive programming은 사람들이 이상하게 생각하게 만든다고 한다. 그런데 getX로 도입할때 setState()를 사용하는것만큼 쉽게 만들었다고 한다. You won’t need to create StreamControllers. You won’t need to create a StreamBuilder for each variable You will not need to create a class for each state. You will not need to create a get for an initial value. [1:49 PM] 위와 같이 해서 쉽게 만들었다고 하는데, reactive는 상태관리를 위해서 저렇게 해준 모양이다.

Figure 26: getx

Figure 26: getx

getX에서 본 모양이다. 상태로 관리될 literal에 .obs를 붙여주는것… [1:55 PM] From now on, we might refer to this reactive-".obs”(ervables) variables as Rx. [1:56 PM] .obs값을 갖는 변수들을 Rx변수라고 한다.

hoyoul — 11/07/2023 2:01 PM 내부적으로 Stream<string> name = ‘jonatas Borges’로 값이 초기화된다고 한다. stream을 만든다는 뜻인데..여기까지는 이해됐다. [2:01 PM] 즉 stream이다. [2:01 PM] 그러면 view에는 stream listener가 있어야겠네..이런 생각이 든다. [2:02 PM] 그게 obx(lambda)라고 설명한다. [2:04 PM] name의 값이 변한다는것은 stream에 새로운 data가 sink한것이기 때문에, stream의 listen()가 호출되듯이, 마찬가지로 .obs변수인 Rxn이 변경되면, obx(lambda)가 호출되면서 widget의 ui가 변경된다는 것이다. 그런데 듣고 보면, 아니 이거 그냥 getX의 readme에서 다 설명한 내용이잖아..하는 생각이 든다. (edited) [2:05 PM] 내가 알고 싶었던건 Rxn이란 type이다. 물론 추측하면 .obs를 붙여서 stream을 만들지 않고 Rxn으로 명시적으로 Rx변수를 사용했네…라고 이해할 수 있다. [2:06 PM] 중요한것은 googling했을때, Rxn<String> googletoken과 같이 사용하는 예제가 안보이기 때문에 좀 낯선것이다. [2:06 PM] 초기값이 없다면 Rxn<String> googletoken처럼 사용되는건 문제될께 없어 보이긴 하다. 즉 Rxn은 녹으면, stream이 된다. obx()는 녹으면 listen()가 된다. 이정도로 정리하면 된다. (edited)

class SettingController extends GetxController {
  static SettingController get to => Get.find();

  var googleUser = Rxn<GoogleSignInAccount?>(null);
  var userToken = Rxn<String?>(null);
  var userEmail = Rxn<String?>(null);

  final GoogleSignIn _googleSignIn = GoogleSignIn(
    scopes: ['email'],
  );

  @override
  void onInit() {
    super.onInit();
    loadData();
  }

  Future<void> loadData() async {
    final prefs = await SharedPreferences.getInstance();
    userToken(prefs.getString(appUserToken));
    userEmail(prefs.getString(appUserEmail));
  }

kholdem의 인증처리부분에서 sharedPreference를 사용하는데, 잘모른다. 찾아보자. [2:19 PM] SharedPreference [2:20 PM] https://pub.dev/packages/shared_preferences

Wraps platform-specific persistent storage for simple data (NSUserDefaults on iOS and macOS, SharedPreferences on Android, etc.). Data may be persisted to disk asynchronously, and there is no guarantee that writes will be persisted to disk after returning, so this plugin must not be used for storing critical data.

내용을 읽어보면 android에서 data를 저장하는 공간인 sharedpreference와 ios의 NSUserDefaults에 데이터를 저장하는데 사용하는 package라고 한다. [2:28 PM] critical한 정보를 저장하면 안된다고 한다. [2:29 PM] k-holdem에는 appuser token이나 email을 저장하는데, 이것은 critical하지 않은가? [2:29 PM] 여튼, shared preference는 뭔지 알겠다. ios나 android에서 storage중에 key:value로 저장하는 hash table이 있는데, 거기에 data를 저장하는 package라는 것이다. [2:33 PM] 사용법은 어렵지 않다. write data

// Obtain shared preferences.
final SharedPreferences prefs = await SharedPreferences.getInstance();

// 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', <String>['Earth', 'Moon', 'Sun']);

read data

// 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<String>? items = prefs.getStringList('items');

지금까지 정리 [2:46 PM] kholdem 인증 코드를 해석하기 위한 기반이 되는것들을 공부했다. [2:48 PM] kholdem에서 google signed in을 사용해서 access token, id, token, server auth token을 가져오는것까지 확인했다. 나는 oauth2.0 package를 사용할거라고 생각했는데, authentication이란 함수로 가져왔다. [2:48 PM] 가져온 token을 rais server와 주고받는 과정을 봐야 한다. [2:49 PM] DIO와 json을 사용한다. [2:49 PM] 이 부분은 좀 공부해야한다. [2:52 PM] dio json을 알게 되면, 어떻게 rails서버와 통신해서 인증이 돌아가고 하는것을 알게 되면, jira에 적힌 해야하는것을 할수 있을꺼 같다. ui를 바꾸는건 내가 stateful,과 stateless공부를 했기 때문에 그냥 바꾸면 된다. 별도의 공부는 필요없다. 오늘까지 kholdem 인증관련 소스분석을 다 끝내자. 아니면 내일 오전까지 해서, 문서를 만들자. (edited)

hoyoul — 11/07/2023 2:53 PM DIO 사용법 [2:54 PM] https://pub.dev/packages/dio

dio는 큰 package다. 대략적으로 http protocol을 사용하는 모든것을 할수 있다고 보면 된다. get,post, download, etc.. [3:14 PM] Dio가 무엇의 약자인지 궁금해졌다. [3:14 PM] 그런데 별다른 설명을 찾을수 없었다. gpt chat에선 Dio is object의 약자라고 한다. [3:14 PM] dio는 필요할때 찾아서 보면 될듯 하다. [3:15 PM] JsonApiSerializer [3:17 PM] Serialization은 객체 직렬화를 말하는거 같다. java나 python에 보면, 객체를 byte stream으로 만들고 다시 stream을 객체로 만든다. 이것은 옛날에 RMI기술에 많이 사용된것이다. 즉 program에선 data를 객체형태로 유지하고 사용한다. 하지만 네트웍으로 보낼때는 string이나 byte로 보내야 한다. 받은 byte를 다시 객체화시키거나 보낼때 객체를 string으로 바꾸는것을 직렬화라고 부른다. (edited)

hoyoul — 11/07/2023 3:19 PM json은…보통 xml부터 설명하는데…http protocol에서 data를 보낼때 xml을 사용했다. xml에선 tag를 자유롭게 사용하기 때문에 html과 달리 data를 보내기 유용하다. 그래서 xml을 사용해서 데이터를 주고받았는데…이게 어차피 oop프로그램과 연결지어 사용될때는 parsing해서 객체 property로 assign하는 과정을 거친다. [3:20 PM] 이런 데이터를 바로 object로 변환시키면 얼마나 좋을까? 해서 나온게 json이다. [3:20 PM] 그래서 javascript object다. [3:21 PM] object를 file로도 file을 object로도 변환한다고 보면 된다. [3:21 PM] json 파일이 주어지면 object로 만들고, object를 js파일로도 만든다. 이때 들어가는 기술이 serialize다. [3:23 PM] 아! 그런데 JsonApiSerizer는 외부 package가 아니다. dart sdk에 포함된 library다. [3:23 PM] https://pub.dev/documentation/rest_data/latest/rest_data/JsonApiSerializer-class.html

https://docs.flutter.dev/data-and-backend/serialization/json 이 문서에 좀 자세히 나왔다. 살펴보자.

hoyoul — 11/07/2023 4:27 PM 잠깐 휴식 10분

hoyoul — 11/07/2023 5:00 PM serialize를 2개로 나눌수 있다. encode와 decode다. encode는 객체 -> string, decode는 string -> 객체로 보면된다. code화 한다는 encode를 string화한다고 생각하면 편할 듯하다. [5:00 PM] 수동으로 serialize하는 방법과 자동으로 하는 방법이 있다고 한다. [5:01 PM] 수동 serialize (작은 project에 적합) (edited) [5:03 PM] dart:convert 라이브러리를 사용, jsonDecode()를 사용하면, json파일(string)을 읽어서 Map<string, dynamic>으로 만들어 낸다. map객체로 만드는 것이다. [5:04 PM] 사용법 예시 [5:04 PM] { “name”: “John Smith”, “email”: “john@example.com ” } (edited) [5:04 PM] 위와 같은 json파일이 있다고 하자. [5:05 PM] dart:convert library의 jsonDecode()를 사용해서 parsing하는 과정은 다음과 같다. [5:05 PM] final user = jsonDecode(jsonString) as Map<String, dynamic>;

print(‘Howdy, ${user[’name’]}!’); print(‘We sent the verification link to ${user[’email’]}.’); [5:06 PM] jsonDecode의 생성자 인자로 json 파일의 string을 입력해서 Map객체로 만들고, 여기서 json field를 수동으로 뽑아낸다. [5:07 PM] 예를 들면 name이란 항목은, user[’name’]으로 접근하고, email 항목은 user[’email’]로 뽑아낸다.

hoyoul — 11/07/2023 5:08 PM 이것의 문제점은 만일 json의 항목이름이 first_name인데, 실수로 name으로 뽑아낼수도 있다. 필드가 적으면 그냥 이렇게 필드명을 명시해서 사용하면 된다. [5:09 PM] json을 만들어서 보내는 쪽이나, 그걸 받아서 parsing하는 쪽이나 수동으로 한다면 조심해서 작성해야 한다. [5:10 PM] 만일 json이 100개의 항목을 가지고 있다면, 이것을 하나하나 다 조심해서parsing해야 한다. 예를 들어 user[‘fname’]으로 뽑아냈는데, 알고보니 pname이였다면 에러가 난다. [5:11 PM] 즉 작은 project에서는 이렇게 사용해도 문제없다. 에러나도 고쳐서 빌드하면 되니까… [5:11 PM] 그런데 json항목이 100개만 넘어가도 typo같은 실수가 날 확률이 높아진다. [5:11 PM] 그래서 자동화된 방식을 사용한다. [5:12 PM] 자동 serialize (edited) [5:13 PM] dart의 convert library를 사용하지 않고, json_serializable and built_value 같은 library를 사용한다. [5:13 PM] 대용량의 json파일을 처리해야 한다면 convert를 사용하는 방식은 에러가 많이 날수 있기 때문이다. [5:14 PM] 그런데, json_serializable, build_value는 다 외부 package다. [5:15 PM] kholdem에서 사용하는 jsonApiSerializable은 dart library다. convert library와 비슷한 종류로 small project에 사용되는 방식인거 같다.

hoyoul — 11/07/2023 5:17 PM 자동 serialze방식은 외부 package라서 설명이 외부 package에 있기 때문에 pass한다. kholdem에는 JsonApiSerialize library가 있기 때문에 그것에 대한 사용법을 살펴보자. [5:18 PM] khodem serialize (edited) [5:19 PM] 위에서도 말했듯이 kholdem에서 사용하는 방식은 manual방식이다. convert를 사용하는 방식과 비슷하다. 작은 project에서 사용하는 방식이다. code를 보자.

Future<bool> login(
      String? accessToken, String? idToken, String? authCode) async {
    try {
      var response = await kholdemAuthApi.post('/google_oauth2_with_id_token',
          data: jsonEncode({
            "omniauth.auth": {
              "credentials": {"token": accessToken},
              "extra": {"id_token": idToken},
              "server_auth_code": authCode
            }
          }));

      var userToken =
          UserToken(serializer.deserialize(jsonEncode(response.data)));

      SettingController.to
          .setUserToken(userToken.authToken, userToken.authEmail);

kholdemAuthApi객체가 아마도 rails server와 연결될 것이고, post로 보내는 data는 수동으로 작성한 json이다. 보낼때는 네트웍을 타기 때문에 당연히 객체를 직렬화 해야한다. 그래서 jsone encode로 byte나 string으로 만들어서 보내는데, 보내는 형식이 수동으로 key: value쌍으로 보내다.

hoyoul — 11/07/2023 7:44 PM Test (edited) [7:45 PM] 처음에 인증받은 token을 rails서버로 전달하는데, 내가 test해보고 싶은것은 local에 rails server를 띄운 상태에서 flutter에서 debugger를 사용해서 값을 주고 받은것을 check하고 싶다. [7:46 PM] flutter에서 debugger를 어떻게 사용하는지 모르기 때문이다. [7:46 PM] 우선 local에서 rails server를 띄우고 flutter로 local server에 접속하는걸 먼저 해보자. [7:47 PM] 왜냐면 내가 해석한게 맞는지 여러값들을 바꿔보고 테스트해야 하기 때문이다. [7:49 PM] 우선 소스를 git pull로 다 땡겨오자.

잠깐 10분 breaking

hoyoul — 11/07/2023 8:08 PM 우선은 simulator로 테스트하고, 그다음 실기에서 해보자. [8:11 PM] 음..좀 이상하다. [8:12 PM] server에서 pending migration error가 난다. [8:13 PM] 내가 db를 안띄웠다. [8:13 PM] 아…지난번에 컴퓨터를 다시 깔때 rails 세팅을 하지 않았기 때문이다. [8:14 PM] 그러면 오늘은 rails와 flutter 연결만 확인하자.

hoyoul — 11/07/2023 8:27 PM 아니다. tableplus도 되어 있어서 설치는 되어 있는데, pending migration이면 예전 설치된것과 지금 db에 변화가 있어서 migration으로 update를 해줘야 되는거 같다. [8:27 PM] 10월 26일날 migration한게 있다. [8:29 PM] rails에 post할때 문제가 있다. 좀더 봐야 할듯하다.

hoyoul — 11/07/2023 8:43 PM 우선 token을 google에서 가져오기는 하는건가? 그거부터 찍어보자 [8:43 PM] 아..여기서 debugger사용하면 딱인데… [8:43 PM] debugger부터 공부하자. [8:43 PM] flutter debugger [8:44 PM] https://docs.flutter.dev/testing/debugging

이거 읽고 emacs에 적용해보자. [8:47 PM] 우선 devtools라는게 있다. perfomance를 측정하고 debugging도 가능한거 같다. [8:48 PM] 나는 bp만 걸면 되기 때문에, 차라리 lsp에서 찾는게 나을 수 있겠다.

hoyoul — 11/07/2023 8:50 PM lsp-dap에 보니, breakpoint 걸만한 함수들이 꽤보인다. [8:51 PM] https://github.com/emacs-lsp/dap-mode#dap-mode 잠깐만 읽어보자. [8:53 PM] dap-hydra를 해야, 명령어 보기가 수월해진다. 설치하자. [8:55 PM] lsp도 docker를 많이 사용한다. 주말에 docker를 좀 봐야할 듯 하다. [8:57 PM] dap-ui-breaktoggle이 안걸린다. binding이 안되니 불편하다.

hoyoul — 11/07/2023 9:08 PM 우선 bp는 거는 건 쉽다. 근데 ide ui처럼 보여서, 좀 그렇다. gdb 스타일이 편한데…

Figure 27: oauth debug

Figure 27: oauth debug

우선 해야할게 dap-breakpoint-toggle을 hook걸어서 binding걸고, 원하는곳에 bp를 건다. 그다음 dap-debug로 실행하고, dap-continue, dap-next도 binding걸자.

hoyoul — 11/07/2023 9:31 PM rails도 byebug를 설치하자. [9:34 PM] pry를 많이 쓰지만, byebug도 비슷하다. [9:35 PM] 근데 이미 debug가 develop에 설치 되어 있다.

hoyoul — 11/07/2023 9:42 PM debug gem이 뭔지 몰라서 검색을 했는데, https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem

나는 byebug를 쓸 것이다. [9:43 PM] 하나만 확인하자. token을 가져오는지만, 그리고 내일하자. [9:46 PM] local이라 그런가? 좀 이상하다. 내일 더 보자.

11.8

어제 확인한건 google에서 토큰을 가져올때, 에러가 났었다. 이게 좀 이상한게 예전엔 simulator로 인증받고 게임화면으로 잘 넘어갔었다. 그런데 구글인증에서 에러가 났다면, 게임화면 진행이 안되야한다. [9:29 AM] 차이는 내가 local로 진행하면서 수정한거 밖에 없다. [9:29 AM] 뭔가 local로 할때 빠진게 있거나 잘못한것이다.

hoyoul — 11/08/2023 9:33 AM 우선 재부팅하고 슈퍼가서 커피 좀 사오자. breaking 10분

hoyoul — 11/08/2023 10:12 AM getX에 대해서 다시 생각해보자. 우리 app은 getX로 만들었다. 왜 getX로 만들었을까? getX를 안사용하면 안돼? 라는 질문에…이전에 공부했듯이, 해야 한다. [10:13 AM] 우리는 크게 보면 2개의 시작화면과, 1개의 게임판 화면이 있다. [10:14 AM] 단지 이렇게만 본다면, 굳이 getX를 사용할 필요가 없다. [10:15 AM] 게임판 화면은 stateful을 사용하는게 더 효율적이다. 게임판에 변경되는 widget만 dirty여부로 다시 그려주는게 낫기 때문이다. 완전히 다른 page라면 getX를 사용하는게 필수라고 말할수 있다. [10:16 AM] 근데도 나는 getX를 사용해야 한다고 말한다. 왜냐면 3개의 page만 사용하는게 아니라, 계속 추가가 될 것이기 때문이다. 그래서 getX를 사용하지만, game판화면은 stateful로 만들면 되는 것이다. [10:16 AM] getX로 만드는데는 순서가 있었다. [10:17 AM] GetMaterialApp을 사용 (edited) [10:18 AM] GetMaterialApp을 사용한다. kholdem도 GetMaterialApp이 적용되었다. page를 routing이 쉽고, page들은 stateless로 제작되어야 한다. 그렇게 했을까?

void main() {
  WidgetsFlutterBinding.ensureInitialized();

  runApp(const MyApp());
  Get.put(GameController());
  Get.put(SettingController());
}

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

  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      title: 'kholdem',
      theme: ThemeData(

build에 보면 GetMaterialApp을 사용하고 있다. [10:21 AM] ok. 그다음엔 뭘해야 했을까? 보여줄 page를 만들어야 한다. 보여줄 page는 stateless로 만들고 MVC를 적용해야 한다. [10:22 AM] kholdem은 첫번째 page를 뭘로 했을까? 일명 home page말이다. 그리고 stateless와 MVC를 적용했을까? [10:23 AM] MainScreen home page

),
      home: const MainScreen(),
    );

MainScreen이란 page가 첫번째 page, 즉 home page다.

hoyoul — 11/08/2023 10:28 AM 음…근데 예상과는 다르다. stateful로 했다.

class MainScreen extends StatefulWidget {
  const MainScreen({Key? key}) : super(key: key);

  @override
  State<MainScreen> createState() => _MainScreenState();
}

class _MainScreenState extends State<MainScreen> {
  @override
  void initState() {
    super.initState();
  }

이건 getX readme에서 step으로 나눠서 설명한 방식과 어긋난다. (edited) [10:31 AM] 그럼 이 page에 MVC를 구현했을까?

@override
  Widget build(BuildContext context) {
    return SafeArea(
        child: Obx(() => Scaffold(
              body: SettingController.to.isValidToken()
                  ? const IntroPage()
                  : const LoginPage(),
            )));
  }

obx()가 있다. [10:32 AM] obx는 stream의 listen()와 거의 같다고 했다. [10:33 AM] model에서 obs로 꼬리표를 달아준 state변수가 내부적으로 stream이기 때문이다. [10:34 AM] model에서 정의해준 상태변수가 변하면 obx()가 호출되서 view가 변하는건데…

hoyoul — 11/08/2023 10:36 AM 우선! 자연님의 코드는 정확하게 getX를 쓴건 아니다. getX는 page에 MVC모델을 적용해서 사용한다. [10:36 AM] 그런데, 자연님의 코드에서 모델은 page와 연동되지 않았다. [10:38 AM] 자연님의 코드에서 entry point를 보자.

void main() {
  WidgetsFlutterBinding.ensureInitialized();

  runApp(const MyApp());
  Get.put(GameController());
  Get.put(SettingController());
}

entry point에 model과 연동했다. [10:39 AM] 이것이 무슨 의미냐? page에 MVC를 적용한게 아니라, program자체에 model을 연동시킨거다. [10:40 AM] program에 속한 GetMaterialApp, MainScreen은 당연히 model에 반응을 할것이다. [10:40 AM] 이게 문제가 있느냐? 문제는 없다. 하지만 이상한 코드다. [10:42 AM] 의도를 가지고 했는지, 문제없이 동작하니까, 이렇게 작성했는지는 모른다. 그런데 동작이 되고 이 동작을 내가 이해만 하면 상관없다.

hoyoul — 11/08/2023 10:45 AM 문제가 되는건 의도를 가지고 했는데, 내가 그 의도를 몰랐을때가 문제가 되는데…. [10:45 AM] 내가 봤을땐 특별한 의도가 있어보이진 않는다. [10:47 AM] page에 model을 inject시키는 것보다, 그냥 program에 inject를해도 되니까, 그리고 더 편해보이기도 하다. performance에 문제는 없을까? 라는 질문을 한다면, 이건 모르겠다. [10:49 AM] 여튼 model을 page에 inject하는게 MVC모델로 page를 구성하는 GetX의 원리인데, page에 inject하지 않고 program에 inject하면서….page의 mvc모델은 깨졌다. 그렇기 때문에 page를 stateless를 하지 않고 stateful로 짠것이다. [10:50 AM] model을 page에 inject하는 이유는, model의 controller를 page에서 수행해야 하기 때문이다. [10:52 AM] 이전 getX에서 제공하는 sample예제 counter에서도 봤듯이, counter버튼을 누르면 increase()가 호출되서 count상태변수를 증가시킨다. 여기서 increase()를 controller라고 부르고, 이 controller()는 Model에 정의되어 있다. 그래서 Model을 page에 연동 시키는 것이다.

hoyoul — 11/08/2023 10:54 AM 2개의 model, GameController와 SettingController를 main()에 연동(inject)해서, controller를 사용하는 모든 page는 controller를 어떻게 사용할까? MVC 가 깨진 상황에서 어떻게 처리를 했을까? 궁금하다.

  @override
  Widget build(BuildContext context) {
    return SafeArea(
        child: Obx(() => Scaffold(
              body: SettingController.to.isValidToken()
                  ? const IntroPage()
                  : const LoginPage(),
            )));
  }
}

home page인 MainScreen에서 SettingController.to.isValidToken()가 controller다. [11:00 AM] 직접 사용한다. Model을 View에 inject하지 않고 MainScreen에서 SettingController.to.isValidToken()이라는 controller를 사용하고, obx()를 사용한다. view에서 model을 사용하는건 문제가 없어 보인다.

hoyoul — 11/08/2023 11:01 AM 단지 model을 page에 적용시켜 각각의 page를 MVC로 decoupling하는 GetX의 방식과는 다르지만, 이렇게 사용할 수도 있다. [11:04 AM] 근데 여기서 약간의 꼼수가 있다.. controller를 사용할 때, counter예제에선 Model이 page에 연동되어 있기 때문에 Model.increase()로 호출했다. 근데 여기선 SettingController.isValidToken()를 사용하지 못하니까. SettingController.to.isValidToken()를 사용한다. 이게 model을 view에 연동하지 않고 main에 연동하기 때문에 발생하는 side effect다.

hoyoul — 11/08/2023 11:09 AM to라는게 뭐냐면? [11:11 AM] page에 model을 inject했다면 해당 model에 포함된 controller가 a()라고 한다면 page의 view에서 model.a()를 직접 호출해서 사용한다. 그런데 page의 view가 자신과 연동되지 않은 model에 있는 controller를 호출 할때는 to를 사용한다. [11:12 AM] 즉 이렇게 사용할 수 밖에 없는것이다. 왜냐면 Get.put(모델)을 main에다 해주었기 때문이다. [11:12 AM] 그럼 이거 고쳐?라고 말할수있다. [11:13 AM] 고치기 전에 더 분석을 해야한다. 내가 확신이 들어야 고치는것이지, 지금은 분석을 더해야한다. 아하…이제 flutter소스 알겠네..하면..이거 내식대로 하자. 라고 바꿔야 하는데, 지금은 분석중이라서… [11:14 AM] 왜냐면 섣부르게 고치면 꼬인다. 물론 github에 있는 소스는 안전하니까, 맘대로 고칠수 있다. [11:15 AM] 근데 전체를 공부한후 고치는것이 더 안전하다. 고치면서 공부하는건 더 부담이 있다.

그런데 obx()는 여기서 사용할 수는 있는거야? 라고 질문할 수 있다.

hoyoul — 11/08/2023 11:26 AM 음..내가 아직 getX를 모르는거 같다. 내가 지금껏 한 말이 맞는것일까?

hoyoul — 11/08/2023 11:35 AM 자연님한테…물어봐야겠다.

hoyoul — 11/08/2023 12:13 PM GetX 정리 (important!) (edited) [12:14 PM] getX는 rootwidget을 GetMaterialApp을 사용해야 한다. getX는 page를 MVC로 적용하기 위해서, page를 decoupling한다. page를 두개의 class로 만든다. view와 controller. (edited) [12:15 PM] [controller] controller는 MVC에서 M,C에 해당하는 class인데 GetxController를 상속한 class다. class MyController extends GetxController { var a = 0.obs;

void incrementA() { a++; } (edited) [12:18 PM] [view] view는 GetX의 routing을 위해서 stateless widget으로 만든다. Get.put(controller) Getx는 controller를 pool에 넣어서 관리한다. controller의 삭제및 등록을 할 수 있다. put은 Getx에 controller를 등록한다.

hoyoul — 11/08/2023 12:21 PM [view에서 사용하는 함수] Get.find() Getx에 등록된 controller를 찾기 위해선 Get.find()로 controller를 찾아서 사용한다. 예를 들어, ControllerA control = Get.find<ControllerA>(); (edited) [12:22 PM] view에서 controller의 data를 변경할때, 사용한다. [12:26 PM] [view에서 사용하는 함수] obx() view에서는 controller에 obs로 정의된 data가 변경될때 반영한다. 어떻게 반영하냐? controller에 obs로 선언된 data가 obs내에 있는경우 obx()가 호출된다. [12:26 PM] ps: 착각하지 말아야 하는게 있다. obx는 obs로 선언된게 변하면 무조건 호출되지 않고, 관련된 obs가 있을때 호출된다는 것이다. 관련되었다는건 obx()내에 obs로 선언된 상태가 있는걸 말한다. (edited) [12:27 PM] 예를 들어보자. A controller에서 a.obs, B controller에서 b.obs가 선언되어 있다. 그리고 Get.put(ControllerA), Get.put(ControllerB)로 등록되어 있다. (edited)

hoyoul — 11/08/2023 12:29 PM view에는 obx((){ Text({’ value is $a’})} 도 있고, obx((){Text{‘value is $b’}))도 있다. [12:29 PM] 여기서 a의 값이 변하면, 모든 obx가 호출되는게 아니다. a 와 관련된 obx만 호출된다. [12:30 PM] 또, 알아야 할께, 일종의 용어인데, Getx를 사용하는 경우, obx()는 view에서 사용한다. 그리고 view에서 obx()는 상태값을 기반으로 widget을 만들기 때문에, obx()를 obx위젯이라고 부른다. GetX in Kholdem (important!) [1:39 PM] 내가 getX를 정리하고 이해한 범위에서 다시 kholdem의 GetX를 분석해보겠다. 그리고 수정하겠다. 왜냐면, 자연님의 코드는 내가 공부한 GetX와 다르게 사용한다. 그렇기 때문에 자연님한테 물어봐서 이 코드를 이해하고 이대로 진행하는건 아닌거 같다. 자연님이 계속 같이 작업한다면 자연님에 맞춰서 작업하는게 맞다. 근데 다음주부터 내가 이 코드를 운영할거다. 그러면 내식대로? 내가 이해한대로 해야 수정도 가능하고 기능추가도 가능하기 때문이다. 그래서 자연님한테 묻지않고, 내가 이해한대로 게임이 동작하도록 고치자. (edited) [1:43 PM] 코드를 분석하면서 이해 안되는 이유와 그것을 고칠 것이다. (edited)

hoyoul — 11/08/2023 1:52 PM main.dart

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:kholdem/config/app_color.dart';
import 'package:kholdem/controller/game_controller.dart';
import 'package:kholdem/controller/setting_controller.dart';
import 'package:kholdem/view/main_screen.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();

  runApp(const MyApp());
  Get.put(GameController());
  Get.put(SettingController());
}

WidgetsFlutterBinding.ensureInitialized();

hoyoul — 11/08/2023 2:09 PM 이 함수는 왜 넣었을까? 없으면 안돼? 이 함수의 역할은 framework가 초기화가 될때까지 기다린다. [2:09 PM] 는게, 역할이다. [2:10 PM] entry point인 main이 호출되는 시점이 framework가 완전히 load되고 안정화 되었을때 main()를 호출한다고 생각하기 쉽다. 그런데 framework는 runtime때 완료가 되는 비동기 작업도 많이 한다. 즉 main()가 호출될때도 동시에 framework의 비동기 함수는 호출되고 있을수 있다. [2:11 PM] 그래서 main()의 비동기함수가 처리된다면 framework와 꼬일수 있다. main의 비동기 함수가 실행되서 framework가 사용하는 resource에 lock을 건다면, framework은 초기화 되지 않는다. [2:14 PM] 이런 문제를 없애기 위해서 framework가 완전히 초기화 되어 안정화 되었을때 main에 있는 코드를 처리할려고 한다. 그런데 대부분의 코드에선, 이 함수를 사용하지 않는다. 왜냐면 지금 이 함수를 사용한것은 Get.put에 비동기처리하는 코드가 있기 때문에 한것이다. 비동기처리(google login)하는 코드가 있으면 문제가 생기기 때문에 쓴거다. 그러면 Get.put()가 main에 있어야할 필요가 있는가?

hoyoul — 11/08/2023 2:19 PM 나는 gameController와 settingController는 main에 있어야 한다고 생각한다. 왜냐면 app이 시작할때, gamecontroller를 통해서 rails와 연결하고, settingcontroller를 통해서 google과 연결에 필요한 초기화 작업을 해야하기때문에 main에서 초기화작업하는게 나쁘다고 생각지 않는다. (edited) [2:23 PM] 그런데 보다시피 Get.put으로 넣는다는것은 인자가 page의 model이란 뜻이다. 일반 class가 아니다. 단순히 연결을 위한 작업을 위한 class라면 생성하는게 맞다. [2:24 PM] 하지만, Getx로 관리하는 controller는 page의 controller이기 때문에 이렇게 처리하는게 맞는가?하는 의문이 있다. (edited)

hoyoul — 11/08/2023 2:26 PM 자연님은 Getx를 나하고 다르게 보고 있다. 지연님이 보는 getX는 model이 되는 data는 controller에 넣고, view에서는 그냥 data를 가져와서 사용만 한다고 보기때문이다. 그래서 view는 stateful로 만들었다. [2:28 PM] 그런데 getX는 page관점에서도 본다. page관점에서 상태데이터를 model로 빼냈기 때문에 page에는 상태가 없다. 그래서 stateless로 만드는 것이다. [2:28 PM] 만일 지연님 생각대로 하면 코드는 꼬일수 밖에 없다.

hoyoul — 11/08/2023 2:35 PM 그러면 어떻게 고칠 것인가? [2:36 PM] 고치지 않아도 되는가?

자연님 생각대로 초기화에서 get.put으로 controller를 등록하는거…그럴수 있다. 다만 view에서 controller를 사용하는 부분은 달라져야 한다. controller가 원래 view가 가진 model을 따로 떼낸것이라서 stateful을 stateless로 고치고 model의 접근방식을 바꿔야 한다. 그래야 최소한의 코드변환으로 바꿀수 있다.

hoyoul — 11/08/2023 2:43 PM => 결론: code분석도 완료되었고, 고칠 부분은 없다. (edited) [2:44 PM] MyApp() 생성

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

  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      title: 'kholdem',
      theme: ThemeData(
        useMaterial3: true,
        primaryColor: colorMain,
        colorScheme: ColorScheme.fromSeed(seedColor: colorMain),
        scaffoldBackgroundColor: const Color(0xff000000),
        appBarTheme: const AppBarTheme(
            backgroundColor: Color(0xff000000),
            elevation: 0,
            centerTitle: true,
            titleTextStyle: TextStyle(
              color: Colors.white,
              fontWeight: FontWeight.w600,
              fontSize: 18,
            )),
        switchTheme: SwitchThemeData(
          trackColor: MaterialStateProperty.resolveWith((states) {
            if (states.contains(MaterialState.selected)) {
              return colorMain;
            } else {
              return Colors.white10;
            }
          }),
        ),
        snackBarTheme: SnackBarThemeData(
            backgroundColor: Colors.white,
            contentTextStyle: const TextStyle(color: Color(0xff0D0C0F)),
            actionTextColor: colorMain,
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(10.0),
            ),
            behavior: SnackBarBehavior.floating,
            insetPadding:
                const EdgeInsets.symmetric(vertical: 15, horizontal: 15)),
        elevatedButtonTheme: ElevatedButtonThemeData(
          style: ElevatedButton.styleFrom(
            foregroundColor: Colors.black,
            backgroundColor: Colors.white,
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(30.0),
            ),
            textStyle: const TextStyle(fontWeight: FontWeight.w700),
          ),
        ),
      ),
      home: const MainScreen(),
    );
  }
}

main()에서 myApp()을 생성한다. build() (edited) [2:47 PM] myApp widget은 생성되면 build()가 호출된다. [2:48 PM] build()는 framework에서 control하는 함수다. framework에서 호출하지만, stateless widget에서는 widget을 생성후에 framework가 build()를 호출하기 때문에, 객체가 생성되고 build()가 호출된다고 이해하면 된다.

hoyoul — 11/08/2023 3:25 PM GetMaterialApp을 생성한다. app이 Getx를 사용하기 때문에 ok [3:26 PM] theme이란게 나온다. [3:31 PM] MaterialApp은 android look& feel을 따른다고 한다. material design이라고 해서 android app만이 가진 색상이나 외모를 정의해 놓았다. 어떻게 생겼고(look) 어떤 상황에서 어떤 동작(feel)이 취해질지를 미리 정해놓았는데, 나는 이것이 theme이라고 생각했다. [3:31 PM] 즉 모른다는 얘기다. 그냥 생각만 했었다. 이왕나온김에 theme을 살펴보자. theme (edited)

hoyoul — 11/08/2023 3:40 PM https://api.flutter.dev/flutter/material/Theme-class.html theme이 무엇인가? app에서 사용하는 color와 typographic의 선택들?이라고 한다. (edited) [3:41 PM] A theme describes the colors and typographic choices of an application. [3:43 PM] theme을 app에서 사용하는 color와 font라고 말하면 반만 맞는것이다. Applies a theme to descendant widgets. [3:43 PM] descentdant widget들에게 적용된다. 적용 범위도 포함되어야 한다.

hoyoul — 11/08/2023 3:48 PM 이건 theme의 일반적인 얘기 아닌가? flutter에서 theme이 무엇이고 어떻게 사용하는가? 일반적인 theme개념과 연관지어 말할 수 있는가? [3:54 PM] theme은 widget이다. Theme을 만드는 가장 쉬운 방법은 ThemeData()로 theme 을 가져오는 것이다. 가져온 theme을 MaterialApp이나 GetMaterialApp에 보면 theme: 이라는 requried named parameter에 적용시키는 것이다. Theme의 본질을 얘기하기전 적용을 얘기하는게 삼천포이긴 하지만, 공식강좌를 따르다 보니 그렇다. (edited)

hoyoul — 11/08/2023 4:04 PM 여튼 theme이란 widget이 있고, ThemeData()를 사용해서 theme을 만든다고 하니 예를 들어보자. light theme과 dark theme이 적용된 app을 만들어보자. [4:04 PM] light theme

Widget build(BuildContext context) {
  return MaterialApp(
    theme: ThemeData(
      brightness: Brightness.light,
    ),
    darkTheme: ThemeData(
      brightness: Brightness.dark,
    ),
    themeMode: ThemeMode.light,

    home: Scaffold(
      appBar: AppBar(
        title: const Text("appbar title"),
      ),
     body: const Center(
        child:  Text("light theme"),
      ),
    ),
  );

Figure 28: theme

Figure 28: theme

ThemeData를 생성하면 color와 typo에 관련된 어떤 설정이 다 세팅된 theme객체가 만들어지는데, 그 default설정이 light theme라고 한다. [4:15 PM] ThemeData에서 brightness를 light로 하면 light theme가 된다. 그리고 themeMode로 어떤 theme을 선택할지 정해줘야 한다. (edited)

hoyoul — 11/08/2023 4:35 PM ventura에서도 xcode먹통되는 현상이 발견되었다. xcode에서 simulator띄어서 했더니…먹통..

</img/oauth/theme.mp4>

dart theme [4:50 PM] dark theme을 만드는것도 비슷하다. theme을 직접 생성하는게 아니라, 이미 모든 color와 typo가 설정된 ThemeData에서 일부 속성만 변경해서 theme을 생성한다.

Widget build(BuildContext context) {
  return MaterialApp(
    theme: ThemeData(
      brightness: Brightness.light,
    ),
    darkTheme: ThemeData(
      brightness: Brightness.dark,
    ),
    themeMode: ThemeMode.dark,
    home: Scaffold(

dark.png brightness:라는 속성을 dark로 했더니, appbar나 body의 color가 변했다. 그리고 text도 white로 변했다. 나는 text나 배경색 color를 설정한건 없다. 단지 ThemeData의 brightness를 dark로 했을뿐이다. 하나를 설정했는데, 여러속성들이 적용된건, 이 기능이 일종의 black box가 되어 버린다. (edited) [4:55 PM] 즉 어떻게 설정했을때 어떤 모습으로 보일지 확실히 알지 못하면 사용할 수 없게 된다. [4:58 PM]

ThemeData의 사용

hoyoul — 11/08/2023 5:14 PM themeData를 설명할때 builder가 나온다. 여기서 좀 어렵다.

hoyoul — 11/08/2023 5:26 PM 음…우선 밥먹고 와서 보자.

hoyoul — 11/08/2023 7:41 PM 다시 한번 정리하자. Theme은 widget이라고 했고, Theme을 만들때 ThemeData를 사용하는데, ThemeData는 말그대로 Theme에 관련된 Data를 가지고 있는 widget이다. ThemeData에 미리 color와 text에 관한 configuration이 설정 되어 있기 때문에 일부 속성만 변경해서 생성해서 쓰면 된다고 했다. Theme은 보통 app에 적용되는데, app에서 여러 page들을 관리하기 때문이다. 즉, 10page, 20page를 만든다고 할때, 매번 color와 typo를 적용하면 힘들기때문에 theme을 app에 한번 설정하면 설정된 color와 typo가 10page,20page에 다 적용된다고 했다. (edited) [7:42 PM] 그런데 ThemeData의 어떤속성을 변경하면 page의 어떤부분이 바뀌는지 blackbox라고 했다. 내 입장에서 blackbox지, 실제는 설명이 되어 있을것이다. [7:42 PM] 그 설명을 찾고 있다.

hoyoul — 11/08/2023 7:49 PM theme을 이루는 2가지 구성요소가 있다고 한다. colorScheme과 textTheme

hoyoul — 11/08/2023 8:27 PM https://m3.material.io/ 이 사이트를 좀 보고 있는데…

Theme이 어렵다. 왜냐면, widget level에서 widget의 color를 설정하는건 익숙하지만, widget의 color 속성을 변경하면되니까…그런데..app level에서 widget의 color를 설정한다는게 좀 어렵다. (edited) [8:33 PM] app단위에서 설정해야 하기 때문에, MaterialApp에서 themeData로 설정한다는 거 까지는 이해했다. [8:33 PM] 어떻게 구체적으로 설정하느냐? app level에서…

hoyoul — 11/08/2023 10:22 PM Theme에 관한 정리 again. (edited) [10:23 PM] widget단위의 color, text 속성은 widget의 property에서 설정할 수 있다. [10:24 PM] (ex: AppBar(color:Colors.blue), (edited) [10:25 PM] 100page의 app에는 100개의 appbar가 있다. 모든 appbar의 color를 blue로 정하는 코드를 짤수 없다. (theme을 사용하는 이유)

hoyoul — 11/08/2023 10:35 PM root widget에서 Theme을 정의하면, 모든 page, 모든 widget에 해당 theme이 적용된다고 보면된다. (edited)

hoyoul — 11/08/2023 10:44 PM 각각의 page에선 Theme.of(context)를 통해서 root widget에 정의된 혹은 widget tree에서 가장 가까운 theme를 사용할 수 있다고 한다. 이것에 대해선 설명이 필요하다.

theme을 만드는 이유는 app에서 사용되는 look&feel을 유지하기 위함이다. 뭔소리냐면, 우리 app에선 Color를 독단적으로 사용하는것을 허용하지 않을꺼야..왜냐면 아무 색이나 사용한다면 일관성을 해치게 되니까..그래서 theme에서 colorscheme이란 걸 만들어 두었다. 이게 뭐냐면 우리 app에서 자주 사용되는 color를 3개 지정한것이다. primary color, secondary color 그리고 error를 나타내는 t로 시작하는 color를 만들어놓았다. 즉 여기서 꺼내 쓰라는 얘기다. 그래서 widget에서 color를 설정할때, Colors.red나, Color(‘0xfffffff’)라고 쓰면 안된다. [10:49 PM] 그러면 여기서 의문이 들수 있다. 아니 우리 app에서 사용되는 color가 3개만 사용되냐? 못해도 10개의 색상이 넘게 쓰이는데 3개만 정하는건 너무 적은거 아니냐?

hoyoul — 11/08/2023 11:01 PM app에서 사용되는 모든 widget들에는 이미 default theme을 기준으로 color나 text가 설정되어 있다는걸 잊어선 안된다. [11:02 PM] 즉 대부분 widget에서 색은 color scheme의 색을 이용해서 background, fore ground를 primary, secondary로 세팅해 놓았고, 고유의 색을 지정한것이 있을수 있다. 이것은 theme과 무관한 고유의 색일수 있다.

hoyoul — 11/08/2023 11:16 PM 다시 질문에 답을 하자면, 우리가 만드는 app에 있는 widget들의 색상은 일반적으로 color scheme의 색을 이용하도록 이미 세팅되어 있기에,3개보다 많은 색상을 이용할때, theme에서 재정의를 해야한다. 예를 들면, snackbar의 각각의 요소별 색상 text를 지정해 놓을수도 있고. appbar같은것도 appbartheme을 사용해서 theme에 설정을 할 수 있는 것이다. (edited)

hoyoul — 11/08/2023 11:31 PM 그래서 ThemeData를 가지고 우리가 theme을 만드는데, 이때 보면 colorScheme, textTheme 으로 app에서 공통적인? 보편적? 획일적으로 사용할 color와 text를 설정한다. 이렇게 설정하면 대부분의 widget의 색깔이나 text는 primary color로 설정되어 있거나 bodytext로 설정이 되어 있어서 그대로 theme을 따르게된다. 아니면 appBartheme, snackbarTheme처럼 widget별로 색상이나 color를 themeData에서 정해줘도 된다. [11:33 PM] 그런데, ThemeData를 설정하는데 있어서 이해하기 힘든 요소들이 있기는 하다.

hoyoul — 11/08/2023 11:41 PM ThemeData에서 설정할 수 있는 요소들을 보자.

Figure 31: style

Figure 31: style

widget별로 가질수 있는 style을 다 정의할 수 있다. [11:43 PM] 그 중에 보면 Brightness, VisualDensity, PrimarySwatch…등등…이게 어떤 의미인지 잘 모르겠다.

hoyoul — 11/08/2023 11:51 PM 내가 착각한거 [11:53 PM] 나는 theme에서는 color를 마음대로 정할수 있다고 생각했다. 그런데 불가능하다고 한다. Material Design을 사용하는 경우, materialColor라고 정해진 color만을 사용할 수 있다고 한다. widget에서 color property를 설정할때, rgb, hexa를 사용해서 color를 정해줬다면, theme에선 불가능하다고 한다. [11:54 PM] 즉, theme이라는게 우리 app에선 이런 color와 text밖에 못써! 하는 제약을 건다는건데…material design을 따른다는것은 theme에 제약을 거는거다. material design을 사용한다고? 그러면 너가 theme으로 사용할 색은 materialColor에 지정된 색밖에 못써! 라고 제약을 건다. [11:56 PM] 그것을 회피하기 위해서, swatch를 사용해서 우리가 원하는 색상 palette를 만들어서 materialcolor를 안 쓸수도 있다고 한다. November 9, 2023

hoyoul — 11/09/2023 12:02 AM kholdem ThemeData분석 (edited) [12:03 AM]

theme: ThemeData(
  useMaterial3: true,
  primaryColor: colorMain,
  colorScheme: ColorScheme.fromSeed(seedColor: colorMain),
  scaffoldBackgroundColor: const Color(0xff000000),
  appBarTheme: const AppBarTheme(
      backgroundColor: Color(0xff000000),
      elevation: 0,
      centerTitle: true,
      titleTextStyle: TextStyle(
        color: Colors.white,
        fontWeight: FontWeight.w600,
        fontSize: 18,
      )),
  switchTheme: SwitchThemeData(
    trackColor: MaterialStateProperty.resolveWith((states) {
      if (states.contains(MaterialState.selected)) {
        return colorMain;
      } else {
        return Colors.white10;
      }
    }),
  ),
  snackBarTheme: SnackBarThemeData(
      backgroundColor: Colors.white,
      contentTextStyle: const TextStyle(color: Color(0xff0D0C0F)),
      actionTextColor: colorMain,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(10.0),
      ),
      behavior: SnackBarBehavior.floating,
      insetPadding:
          const EdgeInsets.symmetric(vertical: 15, horizontal: 15)),
  elevatedButtonTheme: ElevatedButtonThemeData(
    style: ElevatedButton.styleFrom(
      foregroundColor: Colors.black,
      backgroundColor: Colors.white,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(30.0),
      ),
      textStyle: const TextStyle(fontWeight: FontWeight.w700),
    ),
  ),
),

ThemeData는 theme을 설정해서 사용한다.

hoyoul — 11/09/2023 12:12 AM useMaterial3: true, => material3를 사용하는것은 material3에서 제공하는 widget들의 style을 사용하겠다고 생각하면된다. 없으면 안돼? 있어야 할듯 하다. 왜냐면, 우리가 Material.dart를 import해서 material3에서 제공하는 widget을 사용하는데, style을 지정할수 없다면 안되지 않나? [12:16 AM] primaryColor: colorMain, => colorMain은 자연님이 지정한 color다. 원래 theme에서 사용되는 color는 직접지정하지 못한다고 했다. material design에서 지정한 MaterialColor에 설정된 색이 아니면 안된다. 그래서 primary Swatch로 색상을 customize해서 사용한다고 했다. 그런데 자연님은 config/app_color.dart란 파일에 다음과 같이 색상을 지정해 놓았다.

Figure 32: color

Figure 32: color

colorScheme: ColorScheme.fromSeed(seedColor: colorMain), => 내가 color scheme을 3개의 color만 지정할 수 있다고 위에 썼는데, 잘못된 설명이다. primary,secondary, tertiary는 group이였다. 즉, colorScheme은 palette로 봐야 하고, app에 적용될 색상도 3개가 아니라 여러개다.

Figure 33: color scheme

Figure 33: color scheme

seedColor를 정해주면 그 색을 primary color로 만들고, 나머지 색상을 조합해서 만들어준다. scheme은 보통 구조나 계획을 뜻한다. db 스키마 처럼 생각해도 된다.즉, color scheme은 뼈대가 되는 색깔들? app을 구성하는 기둥이 되는 색깔들을 의미한다.즉 colorMain을 seedColor로 던져주면, material design에 맞게 적절히 bright, hue등을 조합해서 surface color나 error color, secondary color등을 알아서 만들어주는듯하다. (edited)

hoyoul — 11/09/2023 12:38 AM scaffoldBackgroundColor: const Color(0xff000000), => scaffold는 page를 나타낸다. page의 배경색을 흰색으로 한다. default값인데, 여튼 설정했다. [12:39 AM] app bar setting

appBarTheme: const AppBarTheme(
 backgroundColor: Color(0xff000000),
 elevation: 0,
 centerTitle: true,
 titleTextStyle: TextStyle(
   color: Colors.white,
   fontWeight: FontWeight.w600,
   fontSize: 18,
 )),

appbar와 같은 widget을 아예 theme으로 지정했다. app의 모든 page에 사용되는 appbar는 위와같은 style을 갖게 될 것이다. [12:43 AM] switch widget theme 설정 => appbar widget의 theme를 설정한것과 마찬가지로, app에서 사용되는 모든 switch는 아래와 같은 style을 갖게 된다. [12:43 AM]

switchTheme: SwitchThemeData(
  trackColor: MaterialStateProperty.resolveWith((states) {
    if (states.contains(MaterialState.selected)) {
      return colorMain;
    } else {
      return Colors.white10;
    }
  }),
),

switch 위젯은 우리가 아는 switch button을 뜻한다. app에서 사용할 switch버튼도 설정해준다. [12:57 AM] snackbar widget도 theme를 설정한다.

snackBarTheme: SnackBarThemeData(
    backgroundColor: Colors.white,
    contentTextStyle: const TextStyle(color: Color(0xff0D0C0F)),
    actionTextColor: colorMain,
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(10.0),
    ),
    behavior: SnackBarBehavior.floating,
    insetPadding:
        const EdgeInsets.symmetric(vertical: 15, horizontal: 15)),

elevateButton widget도 theme을 설정한다.

elevatedButtonTheme: ElevatedButtonThemeData(
  style: ElevatedButton.styleFrom(
    foregroundColor: Colors.black,
    backgroundColor: Colors.white,
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(30.0),
    ),
    textStyle: const TextStyle(fontWeight: FontWeight.w700),
  ),
),

kholdem themeData 요약 (edited) [1:02 AM] 자주 사용하는 widget은 scaffold, appbar, switch, snackbar, elevate button인거 같다. 이것들은 app의 모든 page에서 사용될때 동일한 모양과 색상을 갖게 하기위해서 themeData에 설정했다.

hoyoul — 11/09/2023 1:03 AM ps: themeData 부분도 특별히 고칠부분은 없는데, 내가 읽은 문서에서 Theme에선 materialcolor이외의 색으로 color를 설정할 수 없다고 했는데, primary color를 maincolor라는 이름으로 직접 지정한 색을 사용하는데, 이게 가능한건지, 해서는 안되는건지, 잘 모르겠다. [1:06 AM] => MaterialColor를 사용하는게 추천 사항일뿐 강제성은 없다. 즉 자연님이 MainColor를 지정해서 사용하는건 문제가 되지 않는다. 따라서 그냥 그대로 사용해도 무방하다. [1:09 AM] K-holdem GameController 분석 [1:10 AM] MainScreen으로 넘어가기 전에 main.dart에서 GameController와 SettingController를 생성한다. 그리고 GetX에도 등록한다. 그래서 이 부분을 살펴 봐야 한다. [1:10 AM] 이부분 부터는 내일 하자.

11.9

main.dart에는 entry point가 있다. entry point인 main에서 제일 처음 하는 것은 MyApp이란 widget을 runApp()로 실행하는 것이다. 그 다음 GameController를 생성하고 등록하고, SettingController를 생성하고 등록한다. 이 부분이 맞는 것일까?

 void main() {
  WidgetsFlutterBinding.ensureInitialized();

  runApp(const MyApp());
  Get.put(GameController());
  Get.put(SettingController());
}

runApp이 하는 것은 widget tree를 만들고, root widget으로 MyApp widget을 생성하고 끼워 넣는다. 그러면 MyApp의 build()가 호출된다. [9:13 AM] MyApp은 GetMaterialApp widget을 생성하는데, 이것이 GetX를 사용하는 app widget이다. [9:15 AM] 즉 app에서 GetX를 사용하겠다는건데, widget tree를 벗어나서 GameController와 SettingController를 생성후에 GetX에 등록한다. 이상하다. 잘못된 코드는 아닌데, 이상하다.

hoyoul — 11/09/2023 9:19 AM 즉 root widget인 MyApp에서 Get.put으로 controller를 등록하는게 자연스럽지 않나? GetX는 app widget내에서 사용되는거지, app widget 밖에서 사용되는게 아닌데…. 그래서 부자연스럽게 WidgetsFlutterBinding.ensureInitialiazed()를 호출할 수 밖에 없다. Game Controller나 SettingController가 급박하게 처리되어야 하기 때문에 main에 넣었다면, runApp()가 호출되기 이전에 해야 이해가 된다. 그런데, runApp()이 호출된 이후에 Get.put으로 controller를 생성후 등록한다. [9:20 AM] 그러면, runApp()으로 UI를 보여준 후에 controller를 등록한다는 것은 매우 급박하게 하는것도 아니라는 얘기다. [9:21 AM] 그럼 어디를 고쳐야 한다고 생각하나? [9:22 AM] Get.put을 Myapp의 생성자에서 수행하게 하겠다. 왜냐? Get.Put이 하는 일은 app에서 사용되는, 특히 app이 가진 page의 model을 등록하는것이다. app안에 있는 page의 model을 왜 app밖에서 실행하냐? 초기화 작업을 MyApp()의 생성자에서 처리할 것이다. (edited) [9:24 AM] GameController 코드 분석

class GameController extends GetxController {
  static GameController get to => Get.find();

  var user = User(JsonApiDocument('', '', {}, {})).obs;
  var users = <User>[].obs;

  var rooms = <GameRoom>[].obs;
  var currentRoom = GameRoom(JsonApiDocument('', '', {}, {})).obs;
  var currentGame = Game(JsonApiDocument('', '', {}, {})).obs;
  var currentPlayerId = "".obs;
  var winner = User(JsonApiDocument('', '', {}, {})).obs;
  var pot = "".obs;

  var isShowBet = false.obs;

  @override
  void onInit() async {
    // await initGame();
    await KholdemSocket().initSocket();
    super.onInit();
  }

static GameController get to => Get.find(); => 이게 바로 문제의 코드다. (edited) [9:28 AM] getx는 page를 mvc 모델로 만들고, observer pattern을 사용했다. 이게 무슨 말이냐면? 기존의 page코드에서 model과 controller에 해당하는 부분을 떼서 controller class로 만들고, view는 stateless로 만들었다.

hoyoul — 11/09/2023 9:38 AM stateful로 만든 page에선 model과 view가 한 코드에 들어있다. 종속적이다. 그런데 GetX는 이 종속성을 깨고 Model(controller) , View로 separate해서 독립적으로 만들었다. 독립적으로 만들면, view는 여러개의 model에서 정의된 상태 data를 처리할 수 있다. 예를 들어서, 부부가 이혼을 하면, 남편이던 부인이던 다른 남자, 다른 여자를 마음대로 만날 수 있는것처럼, view는 다른 모델들을 처리할수 있게 되는것이다. [9:40 AM] 그런데 이렇게 stateful을 controller class와 stateless class로 쪼개면 예전에 stateful이였을때 가졌던 종속적인 특징을 잃어버린다. stateless가 되어버린 view는 자신이 반영하고자 하는 상태 data를 가진 controller를 찾아야 하는데, 이것을 GetX가 해주는 것이다. [9:43 AM] GetX는 controller들을 Get.Put()로 등록해서 유지 관리를 하기 때문에 stateless widget이 자신이 필요한 상태data를 가진 controller를 Get.find()를 통해 찾는다. 즉 Get.find()는 view에서 controller를 찾는 함수다. 그런데, 여기서는 controller에서 static 메소드로 연결했다. 이렇게 할 필요가 없는 것이다. view에서 사용하라고 만든 Get.find()이기 때문에 view에서 필요한 controller는 view에서 찾아서 쓰면 되는것이다. GetX가 그렇게 할려고 만들어진것이기 때문이다. [9:43 AM] 어떻게 고칠 것인가?

hoyoul — 11/09/2023 9:45 AM static GameController get to => Get.find(); 를 controller에서 삭제하고 GameController의 상태 data가 필요한 view에서 다음과 같이 작성해서 사용하면 된다.

GameController gc = Get.find<GameController>();

var user = User(JsonApiDocument(’’, ‘’, {}, {})).obs; (edited) [9:47 AM] User class와 JsonApiDocument가 뭔지 모르겠다.

class User extends JsonApiModel {
  User(JsonApiDocument doc) : super(doc);

  // Attributes
  String get name => getAttribute<String>('name');

  set name(String value) => setAttribute<String>('name', value);

  String get balance => getAttribute<String>('balance');

  set balance(String value) => setAttribute<String>('balance', value);

User는 game에서 사용하는 game player, user를 class로 만든 후에 server에서 받은 json을 파싱해서 객체로 만드는 거 같다. 그리고 JsonApiModel을 상속받는다. (edited) [9:52 AM] JsonApiDocument도 살펴보자.

hoyoul — 11/09/2023 9:54 AM 이걸 살펴볼려면 package를 먼저 보는게 나을 듯하다. 왜냐면 User도 JsonApiModel을 사용하고 인자로 JsonApiDocument를 사용하기 때문이다. 아무래도 느낌상, 이게 json api 1.0을 사용하는 코드인거 같다. [9:56 AM] json_api package https://pub.dev/packages/json_api

근데 이 package의 score를 보면 형편없다. 쓰는 사람이 없는거 아닐까? 만든 사람은 alexis?라고 실리콘 벨리에서 일하는 사람이고, version 6.0이 넘었는데, 평가는 박하다.

Figure 34: json1

Figure 34: json1

json api를 쓰는 dart package는 4개 정도 되나보다.. [10:14 AM] 근데 pub.dev의 score는 원래 모두 다 낮아서 의미는 없겠다.

hoyoul — 11/09/2023 10:20 AM standard Recommended URL structure란게 있나보다. uri를 구조화한 design을 만들어서 사용하는 듯하다. 다음 page에서 참조해서 만들었다니까. 한번 보자. https://jsonapi.org/ [10:22 AM] 첫문장에 다음과 같은 글이 있다.

If you’ve ever argued with your team about the way your JSON responses should be formatted, JSON:API can help you stop the bikeshedding and focus on what matters: your application.

한번이라도, json response format을 어떻게 할지를 가지고 싸워본 적이 있나? 그러면 이문제를 이 json:api 가 해결해줄거다. 뭐 이런말인데…json format문제는 bikeshedding이다. 사소한건데, 왜 이걸로 싸우냐. json:api사용해라! 이건데… [10:24 AM] 음… [10:26 AM] 다음 문장도 보자.

By following shared conventions, you can increase productivity, take advantage of generalized tooling and best practices. Clients built around JSON:API are able to take advantage of its features around efficiently caching responses, sometimes eliminating network requests entirely.

Here’s an example response from a blog that implements JSON:API:

json response format을 shared conventions 따르게 하면 된다는 것이다. 그런데 json의 response format은 보내고 받는 사람 사이에 원래 conventions이 있는 거 아닌가? 여튼 convention을 만들어서 해결한다는 건데, 그냥 둘이서 약속하는 convention이 아니라 shared convention인데…이것이 모든 사람에게 공유될수 있는 convention을 말하는거 같다. 그러면 tool을 사용할 수 있다.? [10:32 AM] dart에서 manual json처리가 있고 auto json을 처리할 수 있었다. 후자가 tooling을 사용하는 것이다. [10:33 AM] 그리고 예제를 보여주는데, json:api를 구현한 blog server에서 response한 json format인거 같다.

예제를 folding해서 봐야 편할듯 하다. emacs json mode에 folding이 안보인다. yafolding? 설치해봐야겠다. [10:40 AM] 10분간 휴식!

json 예제를 보면 크게 3부분으로 되어 있다.

Figure 35: json2

Figure 35: json2

blog가 보내는 resource는 articles라는 collection의 첫번째 글을 보내는데, data에 기술되어 있고, 그 article에 댓글단사람같은 정보는 include에 있고, link들은 fetch나 update하기위해서 사용된다고 한다. link가 include에도 있다. 여튼 link는 그렇다고 한다. (edited) [11:18 AM] Json:Api를 사용하면 단순히 json파일을 response만 하는게 아니라, resource들을 만들고 update도 해준다고 한다. 즉, data, include로 표현된 resource들 말고도 새로운 resource를 만들수도 있고, 이미 있는 data, include를 update도 할 수 있다고 한다. 즉 api를 제공한다는 말. [11:19 AM]

MIME Types
JSON:API has been properly registered with the IANA. Its media type designation is application/vnd.api+json.

[11:19 AM] mime 은 internet의 resource를 표현하는 방법 아닌가? [11:20 AM] http나, mail이나 resource의 접근방법, resource의 종류같은걸 정의해서 사용하는데..json:api가 IANA에 등록되어 있다고 한다. 뭔소린지 잘 모르겠다. [11:21 AM] 여튼 IANA에는 등록되어 있다. https://www.iana.org/assignments/media-types/application/vnd.api+json [11:22 AM] 확장자가 .json이면 media type이 json이다. 뭐 이런거라서… mp3, mov도 다 iana에 등록되서 사용하듯이 비슷한 말인듯하다.

Format documentation
To get started with JSON:API, check out documentation for the base specification.
Extensions
The JSON:API community has created a collection of extensions that APIs can use to provide clients with information or functionality beyond that described in the base JSON:API specification. These extensions are called profiles.

You can browse existing profiles or create a new one.

json:API가 제공하는 거 이상의 기능을 가진 확장판도 있는데, 이게 profiles라고 한다. extension으로 보면 될듯하다. [11:24 AM] community가 활발한가?

궁금한거 json파일에 data, include같은 key는 필수인가? example보면 죄다 data,include key를 갖는다. [11:31 AM] 아마도 필수같다. 그냥 느낌상, 왜냐면, 이게 shared convention으로 productivity를 높이는데, tool을 사용한다고 써있기 때문에… [11:33 AM] 개발자 둘이서 어떻게 주고 받을지를 항목들을 가지고 자체적으로 정의해서 사용하면 개발자들이 parsing tool을 만들어야한다. 물론 여기서는 parsing tool만을 얘기하지 않고 json을 쉽게 만드는 것도 포함한다고 써있긴하다. json:api를 구현한 프로그램을 써라..그러면 그함수들을 사용하면 된다. 뭐 이런거니까…

Figure 36: json3

Figure 36: json3

data,errors, meta는 must고 include는 may라고 적혀있네..근데 어차피 tool 쓰면 자동으로 만들어주기때문에 신경쓰지 않아도 된다.

hoyoul — 11/09/2023 11:43 AM 그러면 예를 들어, 가장 간단하면서 legit 한 response json은 어떤게 있을까?

Figure 37: json4

Figure 37: json4

두개는 동일한 내용인데 위에서 type은 class다. 즉 articles라는 객체를 의미한다. [11:46 AM]

a single resource object, a single resource identifier object, or null, for requests that target single resources

이게 must인데, single resource object로 나타내거나, single resource identifier object로 나타내야 legit하다. 위의 두가지 예 모두 legit하다. [11:49 AM] 위에 보면 attributes와 relationships가 있는데, 이것을 single resource object라고 부르고, 아래처럼 없는 경우는 빼고 보내는데 이것을 resource identifer object라고 부른다. 모두 맞는 response라고 한다. [11:50 AM] 즉, 결론은 json response는 data: type, id가 포함된 형식만 사용하면 legit이다. 그리고 json 1.0은 이렇게 문서보고 사용하면 될듯하다. json1.0을 구현한 flutter package 사용법을 알아보자. [11:53 AM] json_api 6.01의 readme에는 코드만 나왔다. 말이 필요없단 뜻인데.ㅋ

Figure 38: json5

Figure 38: json5

그냥 파싱해서 사용하는거다. 특별해보이진 않는다. [11:55 AM] collection이란 용어가 나오는데, 이게 json api 1.0에서 must로 지정한 data의 type을 의미하는것다. [11:56 AM] json 1.0에서 보여준 예는 data: type:articles이였다. 즉, articles collection이였다. [11:57 AM] 여기선 colors collection이다. 즉, response json은 아마도, data: types: colors, attribute도 있는 single resource object로 보내는거 같다. attributes:에는 name, red,green,blue란 속성들이 있는것이고… [11:57 AM] 그냥 우리가 parsing하는거랑 똑같다. [11:58 AM] 다만 규제하는 must는 data: id, type을 반드시 명시하라..이정도.

hoyoul — 11/09/2023 11:59 AM error도 있다. error같은 경우는 어떻게 처리하는지 문서를 봐야 할듯하다.

Figure 39: json6

Figure 39: json6

error response를 보낼 때 must가 있다. errors라는 array를 갖는 json파일로 보내야 한다는 것이다. (edited) [12:05 PM] 그리고 errors란 array에 들어갈 값들을 나열하며 설명하는데, 중요한건 나열된것중에 무조건 하나는 포함되어야 한다는 것이다. [12:05 PM] 다시 package 설명으로 돌아가자. 예제에 보인 error.title은 error json파일에 title항목이 있다는것을 말한다.

try {
   /// Fetch the collection.
   /// See other methods to query and manipulate resources.
   final response = await client.fetchCollection('colors');

   final resources = response.collection;
   resources.map((resource) => resource.attributes).forEach((attr) {
     final name = attr['name'];
     final red = attr['red'];
     final green = attr['green'];
     final blue = attr['blue'];
     print('$name - $red:$green:$blue');
   });
 } on RequestFailure catch (e) {
   /// Catch error response
   for (var error in e.errors) {
     print(error.title);
   }
 }

json api 1.0을 사용할때 주의사항은 최소한 서버에서 response할 때, data:id,type이 포함된 legit한 json파일을 보내면 되고, error의 경우는 errors라는 array가 반드시 포함되어야 하는 정도다. 그냥 일반 json파일 주고받듯 사용하면 될듯 하다.

hoyoul — 11/09/2023 12:14 PM 다시 kholdem 소스로 돌아가자. [12:14 PM] User class Kholdem

hoyoul — 11/09/2023 12:22 PM GameController에서 User class를 사용해서 이것을 분석하다 말았다. [12:28 PM] 여튼 우리가 json 1.0을 사용할려면, 1.0 specification을 구현한 package를 사용하면 되는데, 그 package로 사용하는게 json_api 6.02라는 flutter package였다. 이런 tool들을 사요하면 json document도 만들어주는 함수도 제공하고 parsing하는 함수도 다 제공되니까, 쓰면된다고 했다. [12:28 PM] 그 부분을 살펴보면 된다. [12:28 PM] 밥먹고 와서 하자.

class User extends JsonApiModel {
  User(JsonApiDocument doc) : super(doc);

  // Attributes
  String get name => getAttribute<String>('name');

  set name(String value) => setAttribute<String>('name', value);

  String get balance => getAttribute<String>('balance');

  set balance(String value) => setAttribute<String>('balance', value);

User는 JsonApiModel을 상속 받는다. JsonApiModel은 json_api package에서 제공하는 class다. [1:34 PM] JsonApiModel이 어떻게 정의되었는지 소스를 보자.

class JsonApiModel with EquatableMixin implements Model {
  JsonApiDocument jsonApiDoc;

  JsonApiModel(this.jsonApiDoc);

  JsonApiModel.create(
    String type, {
    Map<String, dynamic>? attributes,
    Map<String, dynamic>? relationships,
  }) : jsonApiDoc = JsonApiDocument.create(
          type,
          attributes ?? Map<String, dynamic>(),
          relationships ?? Map<String, dynamic>(),
        );

눈여겨 볼것은 create()가 있다. 즉 json을 만들어준다. type은 requirement고, attributes와 relationships는 optional이다. [1:39 PM] json_api package가 json 1.0규약에 맞춰서 json파일을 만들어주는 함수라고 보면된다. jsonApiDoc이 json파일이다. 그리고 serializable도 해준다. (edited) [1:40 PM] 그럼 jsonApiModel에 parsing하는 함수도 제공되는 지 보자.

hoyoul — 11/09/2023 1:43 PM 제공되지 않는다. [1:44 PM] 요약하면 jsonAPIModel을 상속하면 create()에 type, attributes,relationships를 넣어서 jsonApiDoc라는 json파일 객체를 만든다. [1:47 PM] 소스를 code by code로 보자.

import 'package:kholdem/api/json/game.dart';
import 'package:rest_data/rest_data.dart';

class User extends JsonApiModel {
  User(JsonApiDocument doc) : super(doc);

  // Attributes
  String get name => getAttribute<String>('name');

  set name(String value) => setAttribute<String>('name', value);

  String get balance => getAttribute<String>('balance');

  set balance(String value) => setAttribute<String>('balance', value);

  List get cards => getAttribute<List>('cards');

  set cards(List value) => setAttribute<List>('cards', value);

  String get currentAction => getAttribute<String>('current_action');

  set currentAction(String value) =>
      setAttribute<String>('current_action', value);

  String get currentBet => getAttribute<String>('current_bet');

  set currentBet(String value) => setAttribute<String>('current_bet', value);

  Map get availableActions => getAttribute<Map>('available_actions');

  set availableActions(Map value) => setAttribute<Map>('available_actions', value);

  // Relationship
  String? get currentGameRoomId => idFor('game_room');

  String? get currentGameId => idFor('game');

  String? get currentRoundId => idFor('current_round');

  // Included
  Game? game;

}

import ‘package:kholdem/api/json/game.dart’; => Game이라는 JsonModel을 상속한 Game class가 있다. [2:30 PM] import ‘package:rest_data/rest_data.dart’ => rest_data라는 package를 사용한다. 이걸 왜 사용하는거지? [2:32 PM] 왜냐면 json api 1.0을 지원하는 package인 json_api를 사용한다. 이걸로 json document도 만들고, parsing도 하고 다한다. [2:33 PM] rest_api package도 비슷한 일을 한다. 둘중에 하나만 사용해도 다 할수 있는걸 두개의 package를 섞어서 사용하는건 아닐지 의심이 간다. [2:34 PM] 소스는 크게 3부분으로 나눠진다. attributes, relationship, include. [2:35 PM] 어떤 기준으로 이렇게 나눴는지 잘 모르겠다. collection이 뭔지 알아야겠다.

hoyoul — 11/09/2023 2:44 PM 우선 내가 읽은 문서를 기반으로 이해한것은…. [2:45 PM] json은 객체다. json file은 객체를 file로 만든거다. byte로 나타내면 serialzable된것이 json file이다. [2:46 PM] attribute는 객체의 속성값을 나타내기 때문에 class 멤버변수값이 들어간다. [2:50 PM] relationships는 무엇이냐? 객체가 다른 객체와 relationship을 가질때, 다른객체의 instance를 json으로 표현한것이다. 무슨말이냐면, blog의 article을 나타내는 객체를 json으로 나타내면 article이 가지고있는 title, contents, author…이런것들이 data object로 표현된다. 그중에서 author라는 멤버는 또다른 객체이기도 하다. 그래서 author의 name, type도 professor, customer냐…age도 attributes로 가질수 있다. 그런데 이런 relationship을 갖는 객체는 또 다른 json파일에 구현되어 있기 때문에 link로 그 위치를 기술할 수도 있다. (edited)

hoyoul — 11/09/2023 2:53 PM 그러면 included는 무엇인가? (edited) [2:57 PM]

Figure 40: json7

Figure 40: json7

relationship과 비슷한가?

Figure 41: json8

Figure 41: json8

아..여기서 햇갈리지 말아야 하는게, relationships는 relationship object다. 즉 author의 구체적인 정보, 그러니까 이름 나이같은것들은 relationships object에 포함이 되지 않는다. 단지 include에서 그런 부가정보를 제공한다. (edited)

hoyoul — 11/09/2023 3:11 PM relationship은 1번 article을 쓴 author라는 객체와의 관계만 알려줄 뿐이다. 예를 들어서, 내가 article이라는 data객체를 json으로 나타내는데, attributes는 날짜,시간,작성자, article종류,…등등의 정보를 표현할 수 있다. 그런데 만일 작성자를 객체로 가지고 있다면 attribute에 넣는게 아니다. [3:12 PM] relationships에 객체를 넣어야 한다. 그래서 그 관계를 나타내는 객체는 위에서는 people 9번이다. attribute로 들어갔다면 value가 있었을 것이다. 예를들어 author: hoyoul, 일텐데, program상에서 author라는 class를 사용하고 있기 때문에 relationships라는 항목에 people 9번이라고 기술해주는 것이다. 즉 또다른 관련된 객체가 있다는 건만 알려준다. (edited) [3:15 PM] 딱 이정도만 알려줘도 된다. 근데, 이런 json을 받는 사람은 author에 대한 정보가 더 필요할수 있다. 예를 들어서 author의 name이나 나이, 닉네임. 그렇다면, 이런 부가정보를 같이 보내줄수 있다. 2가지가 있는데, 첫번째는 위와 같이 link로 author의 정보를 알려주는 방식, 두번째로 include로 나이나 name과 같은것을 포함시켜서 전달하는 방식이 있다. [3:17 PM] 어차피 json은 oop에서 instance를 파일로 표현한거에 불과하다. 이것이 client나 server에서는 object로 존재하는데 object는 단독으로 존재하지 않는다. 연관관계를 갖기때문에 이런것을 표현한것이다. [3:18 PM] json에서 relationship이나, include를 사용한다는것은, 동일하게 객체들이 그런관계를 가지고 있어야 한다. server나 client에서 instance가 그렇게 만들어져 있어야 한다. 그걸 그대로 파일로 만든것이기 때문이다.

hoyoul — 11/09/2023 3:18 PM 만일 program은 그렇게 안짰는데, json만 그렇게 짠다면 코미디가 되버린다. [3:23 PM] 우리 프로그램이 객체지향 프로그램으로 짜지는 않았을것이다. java처럼 oop로 design해서 짰다기 보다, fluuter에서 제공하는 library를 이용하는 식이기 때문에 oop처럼 짜진 않았을꺼 같다. 여튼 program에 구조에 맞춰서 relationships와 include는 사용하면 되는 것이다. [3:24 PM] 다시 kholderm의 user로 돌아가보자.

kholdem은 객체지향적으로 짜지 않았다. 설계가 안되어 있다. [3:27 PM] 비슷하겐 했는데, 여튼 game player를 나타내는게 User class다. 그리고 User의 property를 보자. [3:30 PM] 음…우선 내가 생각하는 oop와 좀 다르다. 여기서는 2개의 class를 만들었다. User와 Game이다. 섯다게임에 사용되는 object가 2개밖에 안될까? [3:31 PM] 이건 좀 생각해봐야 한다. 왜냐면, oop로 짠다는건, db에 그대로 적용되고, 마찬가지로 json에 그대로 적용된다. [3:31 PM] oop의 설계가 가장 중요한건데…내가 게임을 잘 몰라서..ㅠㅠ

hoyoul — 11/09/2023 3:35 PM 2개의 class만 사용된다면 db도 2개의 table을 가지고 있을것이다. user와 game [3:35 PM] 근데 그렇지 않다. [3:37 PM] db에는 game player, game room users, game rooms, player round처럼, 여러 class에 해당하는 table들이 있다. 그런데 client에는 user와 game이라는 class로 처리한다. 즉 따로 노는 구조다. [3:38 PM] 이건 생각해볼 문제다.

hoyoul — 11/09/2023 3:49 PM 왜냐면 card도 class고, card를 모아놓은 그걸 모라고 하지….deck도 class고 player, game room…엄청 많을꺼 같은데… [3:50 PM] 마치 이런 느낌이 든다. db를 설계할때 normalize를 해야하는데, norm을 적용하지 않고 하나의 table에 다 밀어넣고 처리하는 느낌. [3:51 PM] 동작하는데는 문제가 없는데, 나중에 문제가 되는…. [3:51 PM] 그럼 어떻게 해야하는가? 이게 젤 중요한데… [3:52 PM] 이 부분은 좀 책도 보고 좀 생각을 해야할듯하다. [3:53 PM] 내가 혼자서 처음부터 짠다고 생각해야한다. [3:53 PM] 그래야 고칠수 있다. 어떻게 짜야할까? [3:55 PM] 그대로 사용하는 방법은 안되나? 어차피 client는 server에서 어떻게 하던, 그냥 두개의 class를 사용해도 동작은 되지 않냐? 효율성? 그건 알수 없지 않냐? [3:55 PM] 다만 변환처리가 많아지고 손이가는데 어차피 한번만 코드로 만들어지면 되지않나?

Figure 42: oop1

Figure 42: oop1

wateroo대학에서 숙제로 내준 blackjack이란 게임의 class diagram이다. [4:02 PM] 여기엔 game room이 하나고, round도 1개인 경우다. 그리고 user와 player도 분리해야 하고, 손에 든 카드들도 class로 만들어야 한다. oop로 한다면…

hoyoul — 11/09/2023 4:28 PM 고칠만한건 표시만하고 분석을 계속해야겠다. 지금 그걸로 시간끌면 안된다. 어차피 게임 돌아가니까…나중에 보고 계속 분석 진행하자. [4:29 PM] 슈퍼좀 갔다오자 10분 브레이킹

hoyoul — 11/09/2023 4:44 PM GameController 다시

class GameController extends GetxController {
  static GameController get to => Get.find();

  var user = User(JsonApiDocument('', '', {}, {})).obs;
  var users = <User>[].obs;

  var rooms = <GameRoom>[].obs;
  var currentRoom = GameRoom(JsonApiDocument('', '', {}, {})).obs;
  var currentGame = Game(JsonApiDocument('', '', {}, {})).obs;
  var currentPlayerId = "".obs;
  var winner = User(JsonApiDocument('', '', {}, {})).obs;
  var pot = "".obs;

  var isShowBet = false.obs;

User가 뭔지 궁금해서 User class를 살펴 봤었다. User는 jsonApiModel이란 class를 상속받고 여기서 JsonApiDocument를 생성할 수 있는 create함수도 있는것을 확인했다. 그리고 User라는 class는 Player를 나타내는 객체로 보면 된다. [4:46 PM] User를 만들때 .obs를 걸었다. [4:50 PM] 음..여기 보니까..oop대로 만들었다. 고 볼수도 있다. 내가 이상하게 생각했던건 controller를 user와 Game으로 만들었기 때문에 그런데 class는 따로 있는듯하다. (edited) [4:50 PM] 그러면 server하고 따로 노는건 아닌거 같다.

hoyoul — 11/09/2023 4:58 PM 맞다. api폴더에 json에 class를 정의해 놓고 json을 받으면 객체화 한다. deserialize한다. [5:01 PM] 근데 uml로 봐야 보기 편한데, uml작업을 좀 해야할듯하다.

Figure 43: oop2

Figure 43: oop2

emacs에서 돌렸는데, 좀 이상하긴 하지만… [5:10 PM] class의 멤버변수는 원래 생성자에서 초기화를 한다. 근데 User에서는 그냥 explicit하게 초기화를 했다. [5:11 PM] 그래서 멤버변수가 안나오고 함수가 출력되었다. [5:12 PM] 이것도 나중에 시간되면 고쳐야한다. [5:14 PM] 내일 부터 5시에 밥먹으러 나가야겠다. 너무 어둡다.

hoyoul — 11/09/2023 5:24 PM 그런데 생각해보니까, getter하고 setter는 원래 바로 explicit하게 초기화 했었나? 하는 생각도 든다. [5:28 PM] 나도 잘 모르겠다. 그냥 생각하면, flutter에서 보면, 멤버변수 쫘악 나열하고, 생성자에서 필요한것만 required named parameter를 사용하던, 그냥 assign하던 하고 나머지는 생성자안에서 하던데…setter와 getter에대해서 java는 예전에 좀 다르게 처리했던거 같기도 하고… [5:28 PM] 이건 뭐 그냥 넘어가자. [5:28 PM] 밥먹고 와서 하자.

// Attributes
String get name => getAttribute<String>('name');

set name(String value) => setAttribute<String>('name', value);

User는 getAttribute()를 사용해서 name이란 멤버변수를 초기화하는것 같다. 그런데 String get name이란 형태는 뭔지 모르겠다. 형태가 낯설다. [7:39 PM] getAttribute와 setAttribute는 JsonApiModel이란 class에 정의되어 있다.

@override
T getAttribute<T>(String key) => jsonApiDoc.getAttribute<T>(key);

generic으로 정의 되어 있다. 그런데 이상한점이 있다. getAttribute와 setAttribute의 정의는 JsonApiModel이란 class에 있고, 이 class는 json_api.dart package에서 제공한다. jsonApiDoc도 마찬가지로 json_api라는 package에서 제공한다. 그런데 getAttribute와 setAttribute를 호출 해서 사용하는 user.dart파일에는 rest_data.dart를 import한다. 즉 rest_data package에 있는 getAttribute와 setAttribute를 사용하는것이다. 그리고 app에는 rest_data와 json_api를 둘다 install해서 사용한다.

hoyoul — 11/09/2023 7:58 PM app에서 설치한 json관련 api는 4개가 있다. json_api package rest_data json_serializable json_annotation [7:59 PM] 여기서 의문이 드는게, json_api에도 serializable할수 있고, rest_data에서도 할수 있다. 그리고 rest_data와 json_api는 기능이 겹친다. json annotation은 아직 모르겠다. 우선 json_api package는 install만 하고 사용하지 않는다. 대신 rest_data package를 사용해서 json처리를 한다. [8:28 PM] 위에서 JsonApiDoc나 JsonApiModel이 json_api package로 말한것은 착각이였다. 왜 착각했냐면, 정의된 file 이름이 json_api.dart파일에 정의가 되어 있어서 json_api package인줄 알았다. [8:29 PM] 그런데, rest_data package에 json_api.dart라는 파일이 포함되 있는것이였다. [8:31 PM] https://pub.dev/packages/rest_data rest_data package분석 (edited) [8:33 PM] Json:api를 지원한다. 따라서 json_api package와 중복된다.

hoyoul — 11/09/2023 8:47 PM model, serializer, adapter의 3개의 abstractions가 있다고 한다. 3개의 기능이 있다고 생각해도 된다. model은 rest api resource를 나타내는데, jsonApiModel을 생각하면 된다. serializer도 object를 json파일로 만들거나 json파일을 object로 만드는 기능이라고 보면 되고, adapter는 정확히 모르겠다. api, 즉 uri를 통해서 json을 주고 받는 통신을 의미하는건가? [8:49 PM] 사용법이 나오긴했다. 좀 봐야 할듯… November 10, 2023