본 문서는 Fluter Example의 내용을 원저작자의 동의하에 번역한것 입니다.
원 저작자 Eric Windmill에게 감사를 전합니다.
이해하는데 불필요한 문장은 과감하게 버렸습니다. 오 번역에 대해서 의견 주시면 적극 반영 하겠습니다.

Dissecting the Counter App (카운터앱 해부)

‘Hello World’ 플러터 앱은 다음과 같은 간단한 카운터 이다.
counter app1 counter app2

1. 터미널에서 다음 명령을 실행.

1
2
flutter create my_app
cd my_app

에디터로 project을 open.

2. IDE 또는 터미널을 사용하여 앱을 실행.

1
flutter run

IntelliJ: IDE상단에는 다음과 같은 모양의 도구 모음이 있다.(나의 경우 테마를 설치해서 아마도 여러분의 아이콘과는 다를 것이다.)
ide toolbar

  1. toolbar 이미지의 iPhoneX 라고 씌여진 부분이 비어 있는 경우 iOS 시뮬레이터 열기(open iOS Simulator)를 선택.
  2. 앱이 실행되면 'play’버튼을 클릭해서 앱을 실행. 주의 여러분은 'debug’버튼을 debug mode로 실행할 수 있다. 나는 일반적으로 개발할땐 항상 debug mode로 실행한다. debug mode 는 play버튼 실행시와 동일하지만 'breakpint’와 'inspector’를 허용한다.
    앱이 실행되면:

3. 앱의 오른쪽 하단의 ‘+’ 버튼을 클릭.

그것이 유일한 기능이고 카운터가 변경되는것을 확인해 보자.

무슨일이 일어났나? 버튼에는 카운터를 나타내는 앱 상태 변수를 증가 시시는 이벤트 리스너가 있다.
플러터는 해당 상태를 변경할 때마다 해당 상태에 의존하는 위젯(이 경우 number)를 다시 출력해야 함을 알고 있다.

4. 코드 이해하기

첫번째로 디렉토리 구조(directory structure)상에서 무슨 작업을 하는지 알아보자.

1
2
3
4
5
6
7
8
9
10
11
12
my_app
|- android
| ... a bunch of junk
|- ios
| ... a bunch of junk
|- lib
| main.dart
|- test
pubspec.lock
pubspec.yaml
README.md
...

개발시간의 99%는 lib폴더와 pubspec.yaml 파일에만 관심이 있으며, 여기서 프로젝트 의존성을 나열 할 수 있다.
lib폴더는 앱을 빌드하는 곳이며, my_app프로젝트에는 main.dart만 존재한다.
main.dart는 반드시 존재해야 하며, 반드시 lib폴더 root에 존재해야 한다. 이 파일은 다트와 플러터가 진입점으로 인식하는 파일 이다.

파일의 코드를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// import the Flutter sdk
import 'package:flutter/material.dart';

// 모든 다트 프로그램은 무조건 main()함수가 있어야 한다.
// 이 함수는 본질적으로 자바스크립트 Document.ready()이며, 반드시 있어야 한다.
// 이것은 다트 코드의 진입점이다.
// runApp은 앱을 실행하는 플러터 함수 이다.
// 이것은(runApp함수) 위젯을 인수로 취한다.
void main() => runApp(new MyApp());

// 모든것이 위젯이다.
// 전체앱의 root를 포함:
class MyApp extends Stateless Widget {
...

// 주의: MyApp은 임의의 이름 이다.

Stateless and StatefulWidgets

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
// Stateless 위젯은 어떠한 상태변경도 하지 않는다.
// 여러분 어플리케이션의 Root
class MyApp extends StatelessWidget {
// Build method
@override
Widget build(BuildContext context) {
// MaterialApp은 플러터의 기본내장된 위젯으로 다양한 스타일을 제공한다.
// MaterialApp의 가장 중요한 아규먼트는 title과 home이다.
return new MaterialApp(
// Arguments that Material App is looking for.
title: 'Flutter Demo',
theme: new ThemeData(
// 이것은 앱의 기본색상을 Blue로 설정한다.
primarySwatch: Colors.blue,
),
// MyHomePage은 개발자인 여러분이 이름붙인 또 다른 임의의 위젯이다.
home: new MyHomePage(title: 'Flutter Home Demo Page'),
);
}
}


// 이 위젯은 카운터의 상태를 관리하고 있으므로 Statefull임.
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
// 제목으로 전달되고 있어 위에서 볼수 있다.
final String title; // => Flutter Home Demo Page

// Stateful 위젯은 build메서드를 가지고 있지 않다.
// createState() 메서드를 가지고 있고 State를 생성하면 플러터 State클래스를 상속한 클래스가 리턴된다.
@override
_MyHomePageState createState() => new _MyHomePageState();

// Stateful Widgets are rarely more complicated than this.
}


// 이것은 MyHomePage가 생성한 state이다.
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;

// flutter counterApp sample의 comment가 내가 설명하는것보다 더 나아서 남겨둠.
void _incrementCounter() {
// 플러터 메서드에 내장
setState(() {
// setState의 호출이 발생하면 플러터 프레임워크에 뭔가 상태 변경이 있음을 알려주며, 이로 인해 아래의
// build 메서드가 재실행되어 디스플레이에 업데이트된 값이 반영 될 수 있다.
// setState()를 호출하지 않고 _counter의 값을 변경하면 build메서드가 다시 호출되지 않으므로 아무 일도 일어나지 않을 것이다.
_counter++;
});
}

@override
Widget build(BuildContext context) {
// 이 메서드는 setState가 호출되면 매번 재 실행된다. (예를 들어 위의 _incrementCounter메서드가 완료되면)

// Scaffold는 우리에게 표준 모바일 앱 레이아웃을 제공하는 앱의 또 다른 빌드다.
return new Scaffold(
//앱의 상단에 있는 bar
appBar: new AppBar(
// State클래는 widget.someProperty 형식으로 부모 클래스의 프로퍼티를 액세스 한다.
// StatefulWidgets와 해당 StateClasses를 단일 위젯으로 생각하는 것이 더 쉽다.
title: new Text(widget.title),
),
body: new Center(
// Center는 layout위젯이다. 이것은 단일 자식을 취하며 부모의 중간(한가운데)에 위치 시킨다.
child: new Column(
// Column 또한 layout위젯이다. 이것은 다수의 자식을 수직으로 정렬한다.
// 기본적으로 자식들을 수평으로 맞추기 위해 크기를 정하고 부모의 사이즈만큼 커진다.(역자주: 부모의 사이즈를 자식수로 나눠서 사이즈가 정해진다.)
//
// Column은 크기를 어떻게 해야 하고 자식들을 어떻게 위치시키는지를 제어하는 다양한 속성을 가지고 있다.
// 여기서 우리는 mainAxisAlignment를 사용하여 자식들을 세로 중앙에 위치 시킨다.
// 여기서 주축은 세로축이다, Columns이 수직이기 때문이다.(교차축은 수평이 된다)
//
// mainAxisAlignment와 crossAxisAlignment는 CSS의 Flexbox또는 Grid를 사용해봤다면 매우 익숙할 것이다.
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(
'You have pushed the button this many times:',
),
new Text(
// Text는 첫번째 아규먼트 String을 이용한다.
// 우리는 counter의 값을 삽입 String으로 전달한다.
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
// Floating actino버튼은 특수 버튼임.
floatingActionButton: new FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: new Icon(Icons.add),
),
);
}
}

The Widget Tree

이 모든 클래스의 중첩이 혼란 스러울 경우 위젯 시각화된 트리가 도움이 될 것이다. 다음 그림은 여러분의 현재 Counter 어플리케이션의 위젯 트리이다. 중첩된 위젯은 단순히 모든 JS기능이 내장된 중첩된 React컴포넌트 또는 HTML웹 구성요소로 생각할 수 있다.

widget tree

Comment and share

크리에이티브 커먼즈 라이선스
이 저작물은 크리에이티브 커먼즈 저작자표시-비영리-변경금지 2.0 대한민국 라이선스에 따라 이용할 수 있습니다.

본 문서는 Fluter Example의 내용을 원저작자의 동의하에 번역한것 입니다.
원 저작자 Eric Windmill에게 감사를 전합니다.
이해하는데 불필요한 문장은 과감하게 버렸습니다. 오 번역에 대해서 의견 주시면 적극 반영 하겠습니다.

BuildContext Class

모든 플러터 위젯은 BuildContext 인수(매개변수)가 포함된 @override build() 메소드를 가지고 있다.

1
2
3
4
5
class CartItemWidget extends StatelessWidget {

@override
Widget build(BuildContext context) {
// ...

why BuildContext

BuildContext를 간단히 설명하자면 다음과 같다:

  • 위젯tree에서 위젯의 위치.
  • 중첩되어 감싸진 위젯의 위젯. <div <div> .html>와 같은…
  • qt와 비슷한 부모 객체들(parent objects in qt and alike) (역자주: 여기서 말하는 qt가 뭔지 모르겠음.)
  • 플러터에선 최종 build.call() 까지 모든게 위젯임.
  • 마지막으로 위젯이 "stuff"를 리턴 할때 까지의 행(row) 차원(dimentions).

이해 해야할 중요한 개념은:

  1. 모든 위젯은 고유의 build() 메소드와 context를 가지고 있다.
  2. BuildContextbuild() 메소드에 의해 리턴된 위젯의 부모다.

즉, 위젯의 build() 메소드를 호출하는 위젯의BuildContextbuild()를 통해 리턴되는 위젯의 BuildContext는 동일하지 않다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class _MyHomePageState extends State<MyHomePage> {
_MyHomePageState() {
print(context.hashCode);
// prints 2011
}

@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Container(),
floatingActionButton:
new FloatingActionButton(onPressed: () => print(context.hashCode)),
// prints 63
);
}
}

(역자주 : 위 코드에서 두개의 print 메소드 실행시 전달된 BuildContext의 hashCode값이 다르다는것은 서로 다른 객체라는걸 알수 있다.)

그래서 뭘 얘기 하려는 거요?
이거 큰거 하나 잡았다!(큰거 하나 알게되었다는 뜻인듯…) ;-)

  • 잘못된 build()를 참조하기 쉽다.
  • 그리고 그것은 context. (역자주: 잘못된 build()의 리턴값인 BuildContext를 참조하여 문제를 일으 킬수 있다정도로 이해됨 )
  • 이것은 특히 of() 메소드를 사용할때 예기치 않은 상황이 발생 할 수 있다.

The ‘of()’ Method

플러터에선 모든게 위젯이므로,(역자주: 이제 집겨다!!) 어떤 경우 다른 위젯을 참조하기 위해 위젯 트리를 위/아래로 훑기도 한다. 이것은 일부 기능에선 필수이다.

특히 상속(inherited)된 위젯의 상태를 사용하려는 위젯은 상속한 위젯을 참조 할 수 있어야 한다. 이것은 일반적으로 of() 메소드의 형식으로 제공된다.

예 :

1
2
3
4
5
6
@override
Widget build(context) {
return new Text('Hello, World',
style: new TextStyle(color: Theme.of(context).primaryColor),
);
}

of() 메소드는 내부적으로 Theme 유형의 다음 상위 위젯에 대한 트리를 찾고 기본 색상 속성을 가져온다. 프레임워크는 이 build context와 관련한 트리를 알고 있으므로 올바른 Theme개체를 찾을 수 있다.

The Gotcha

플러터는 scaffold(비계)를 통해 우리에게 다음과 같은 문제를 해결 할 수 있는 좋은 방법을 제공한다.

snackbar와 같은 일부 위젯을 만들때, 가장 근접한 Scafold 컨텍스트를 얻어서 플러터가 snackbar를 그리는 방법을 알게 해야 한다. Scafold는 실제 우리가 snackbar를 디스플레이 할 수 있는 위젲이기 때문이다.

아래 코드는 작동하지 않는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Container(),
/// 이 컨텍스트는 Scaffold가 없음
/// 왜냐면 빌드로 전달된 컨텍스트는 현재 트리의 상위 위젯이고
/// 상위 위젯에에는 Scaffold가 없기 때문임.
///
/// 이것은 에러를 던질것이다:
/// 'Scaffold.of() 호출시 전달된 context에는 Scaffold가 포함되어 있지 않기 때문에'
floatingActionButton: new FloatingActionButton(onPressed: () {
Scaffold.of(context).showSnackBar(
new SnackBar(
content: new Text('SnackBar'),
),
);
}));
}

Builder Methods

Builderclosure를 사용하여 하위 위젯을 작성하는 위젯이다. laymans(역자주: In laymans - 평신도 라는 단어인데 어떻게 해석할지 모르겠음)에서는 build 메소드로 리턴되는 자식들에게 직접 컨텍스트를 전달하는데 사용 할 수 있다.

위의 예제를 사용하면(역자주: 위에 오류난 코드를 사용):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Container(),
/// Builders를 사용하면 현재 build메소드에서 컨텍스트를 전달할 수 있다.
/// 이 build 메소드에서 리턴되는 하위 위젯에 직접 전달.
// 'builder' 속성은 모든 위젯에서 build메소드로 정확하게 처리 할 수 있는 callback를 허용.
floatingActionButton: new Builder(builder: (context) {
return new FloatingActionButton(onPressed: () {
Scaffold.of(context).showSnackBar(
new SnackBar(
backgroundColor: Colors.blue,
content: new Text('SnackBar'),
),
);
});
}),
);
}

tip: 여러분은 단순히 build메소드를 작게 만들고 더 상위 위젯에서 Scaffold를 리턴하여 이 문제를 해결 할 수도 있다. 의심 스러운 경우 더 작은 리턴 method를 고수하라!

Comment and share

크리에이티브 커먼즈 라이선스
이 저작물은 크리에이티브 커먼즈 저작자표시-비영리-변경금지 2.0 대한민국 라이선스에 따라 이용할 수 있습니다.

Flutter Widgets

in flutter

본 문서는 Fluter Example의 내용을 원저작자의 동의하에 번역한것 입니다.
원 저작자 Eric Windmill에게 감사를 전합니다.
이해하는데 불필요한 문장은 과감하게 버렸습니다. 오 번역에 대해서 의견 주시면 적극 반영 하겠습니다.

Flutter Widgets

어떤걸 다루나?

What’s a Widget

플러터에선 모든게 위젯이다.

만약 여러분이 이전에 React나 Vue로 작업을 해봤다면 이것은 매우 쉬울 것이다. 플러터에서 모든것들이 Widget 이다. 재사용 가능한 작은 컴포넌트로 작업하는 것은 JS프레임워크와 매우 유사하다. 그리고 위젯은 플러터 클래스를 확장하는 다트 클래스에 지나지 않는다.

모든 플러터 위젯은 다음과 같다:

1
2
3
class ImageWidget extends StatelessWidget {
// class stuff
}

위젯 클래스들은 오직 하나의 요구사항만 있다: 그것은 반드시 다른 위젯을 리턴하는 build 메소드를 가지고 있어야 한다는 것이다. 이 규칙의 유일한 예외는 기본타입(primitive types) (대개 Strings 또는 numbers)을 반환하는 Text와 같은 낮은 수준의 위젯이다.

1
2
3
4
5
class BigText extends StatelessWidget {
Widget build(context) {
return new Text('text');
}
}

그외 위젯은 일반적인 다트 클래스 일뿐이다. 여러분은 위젯에 메소드와 속성 등을 추가 할 수있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class BigText extends StatelessWidget {
// a property on this class
final String text;

// a constructor for this class
BigText(this.text);

Widget build(context) {
// Pass the text down to another widget
return new Text(
text,
// Even changing font-style is done through a Dart class.
textStyle: new TextStyle(fontSize: 20.0),
);
}
}

그런 다음 앱의 다른 곳에서 위젯을 다음과 같이 사용하면 된다:

1
2
3
4
// ...
// This is how we'd use the BigText within another widget.
child: new BigText('This string would render and be big'),
// ...

Stateless and StatefulWidgets

플러터 위젯은 플러터 라이브러리에서 몇 가지 클래스를 확장 해야 한다. 여러분이 사용할 둘은 거의 항상 StatelessWidgetStatefulWidget이다.

둘간 차이점은 위젯 내에 상태(state)개념이 있고 그 중 일부가 변경되면 플러터에 다시 렌더링(re-rendering)을 지시하는 메소드가 내장 되어 있다는 것이다. 이것은 플러터의 핵심 개념이다.
Stateful 위젯은 조금 달라 보인다. 사실 두 가지 클래스인 state객체와 위젯 이다. (역자주: Stateless위젯은 StatelessWidget 클래스만 상속 받으면 되지만 Stateful위젯은 StatefulWidget를 상속한 위젯 클래스와 위젯 생성시 내부적으로 사용되는 state객체를 생성하기 위한 State클래스를 상속받은 클래스 이렇게 두개가 필요함을 말한다.)

다음과 같이 작성하면 된다:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Counter extends StatefulWidget {
Counter({Key key, this.title}) : super(key: key);

// Stateful Widgets don't have build methods.
// They have createState() methods.
// Create State returns a class that extends Flutters State class.
@override
_MyHomePageState createState() => new _MyHomePageState();

// Stateful Widgets are rarely more complicated than this.
}

class _MyHomePageState extends State<MyHomePage> {
int counter = 0;

void increaseCount() {
// setState is a special method that tells Flutter to repaint
// the view because state has been updated!
setState(() {
this.counter++;
}
}

// gotta have that build method!
Widget build(context) {
return new RaisedButton(
onPressed: increaseCount,
child: new Text('Tap to Increase'),
);
}
}

Material and Cupertino Widgets

플러터 SDK가 특별히 좋은점은 AndroidiOS 스타일의 위젯을 내장하고 있다는 것이다.
감정을 분출해서 미안하지만(역자주: 글쓴이가 플러터에서 기본 제공하는 머터리얼과 쿠퍼티노 위젯 제공에 대해 너무 좋아함을 말하는듯함) 플러터가 상자에서 꺼내준것은 꽤 놀랍다. 여러분이 별도의 디자인 능력 없이도 꽤 보기 좋고 접근하기 쉬운 모바일 앱을 만들 수 있다.

만약 여러분이 새로운 React, Vue, React Native 등의 프로젝트를 시작했다고 상상해 보자. 그러면 디자인 표준을 염두에 둔 수백가지(수많은)의 구성 요소들이 필요할 것이다.
이것이 Material과 Cupertino 위젯의 역할이다. Android앱처럼 보여주게 디자인된 Material과 iOS처럼 보여주게 디자인된 Cupertino.(역자주: 새로운 프로젝트를 위해서 수 많은 UI컴포넌트들이 필요한데 이런 작업들을 편리하도록 플러터는 Android와 iOS UI스타일에 맞게 디자인된 Material과 Cupertino 위젯음 기본 제공하므로 굉장히 편리하다.)

위젯에 내장된 이러한 기능과 완전한 사용자 정의 위젯을 만드는 능력은 당신에게 많은 힘(도움)을 준다. low-level custom 위젯으로 완전한 custom앱을 만들거나 MVP(역자주 : Minimum Value Product - 최소 기능 제품)에 도달하기 위해 주어진 기능을 사용하여 만들 수도 있다.

Most Common Widgets

다음은 즉시 사용할 준비가 된 위젯이다. 다음과 같은 위젯에 매우 익숙해 져야 한다.

  • Text : 단순히 화면에 텍스트를 표시하는 위젯.
  • Image : 이미지를 표시.
  • Icon : 플러터의 내장된 Material과 Cupertino 아이콘을 표시.
  • Container : 플러터 UI에서 div 같은거라 보면 됌. 패딩, 정렬, 위젯의 크기 조절, 다른 것들의 boatloads(역자주: 앞서 설명한 요소들을 담을 수 있는것) 를 추가 할 수 있는 편리한 위젯. 비어 있을땐 0px 공간을 차지함.
  • TextInput : 사용자 의견을 처리.(역자주: 사용자 입력 폼)
  • Row, Column : 수평 또는 수직 방향으로 child(자식) 목록을 표시. 레이아웃을 위한 flex-box 규칙을 따른다.
  • Stack : 스택은 하나 이상의 다른 자식을 목록에 표시한다. 이 기능은 CSS의 position 속성과 매우 유사하다.
  • Scaffold : 앱의 기본 레이아웃 구조를 제공하는 앱내 모든 페이지의 root이다. bottom navigations(하단 메뉴), appBar(상단 타이틀 bar), back buttons (이전 버튼) 등을 쉽게 구현 할 수 있다.

플러터의 위젯 문서는 굉장히 훌륭하다. 목록을 확인해 봐라.

1
주의: 여러분이 React같은 컴포넌트 기반 프레임워크에 익숙하다면 내용을 읽지 않아도 된다. 위젯은 단지 컴포넌트 일 뿐이다.

Thinking in Widgets

플러터에서 모든것이 위젯이다(역자주: 몇번에 걸쳐 말하는거 보니 엄청 중요하겠죠? ㅋ)
위젯은 완벽한 앱을 만들기 위해 결합 할수 있는 아주 작은 UI덩어리 입니다. 플러터로 앱을 만드는 것은 레고 블록을 하나씩 조립해서 만드는것과 같다.
위젯은 앱을 만들기 위해 내부에 서로 중첩된다. 여러분의 앱의 root 또한 위젯이며 모든 위제들은 하위로 쭉 내려간다.

플러터는 UI의 모든 측면(aspect)이 위젯으로 처리된다는 점에서 독특합니다.

위젯은 무언가를 표시하거나, 디자인을 정의하거나, 레이아웃 처리를 하거나, 상호 작용을 처리 할 수 있다. 다시 한번 강조하지만 플러터의 모든것은 Widget 이다.

  • 텍스트를 표시하는 간단한 위젯 : const Text('Hello World')
  • 사용자 상호작용을 처리하는 간단한 위젯 : const Button(onTaps: ...callback...)
  • 배경색상을 추가 하는 간단한 위젯 : const BoxDecoration(background: Colors.blue)

기본적으로 여러분의 CSS, HTML 및 자바스크립트가 모두 위젯에서 처리된다고 상상해 보자. 마크업이 없고, CSS도 없다. 오직 위젯이다.

Widget Hierarchy

아래 그림에서 윤곽선으로 보이는 모든 것이 위젯이다.
outlined widget

이 그림은 FlutterByExample의 튜토리얼 앱 중 하나에서 가져온 것으로, 강아지(dog) 정보에 대한 세부 페이지 정보 이다.

녹색 윤곽선은 page를 나타낸다. 그리고 페이지는 플러터의 위젯이다. 파란색 윤곽선은 논리적으로 그룹화된 UI 조각을 나타낸다. 나머지는 흰색으로 윤곽이 그려져 있고, 이는 단순히 내용과는 상관없는 아둔한 컴포넌트(dump component)일 뿐이고, 이것들은 단지 그들이 전달받는 것을 보여줄 뿐이다.

다음은 이 페이지의 위젯 계층도 이다.

  • PageWidget
    • DogProfileWidget
    • CircleImage
    • Text
    • Text
    • Text
    • Container
      • Icon
      • Text
  • RateDogWidget
  • Slider
  • Text
  • SubmitButton

주의 위 계층은 아주 정확하진 않다. 여기엔 columnpadding과 같은 위젯도 있다.

Design for Re-usability

플러터 위젯을 효과적으로 사용하는 가장 중요한 부분은 재사용 할 수 있도록 가장 저수준의 위젯을(lowest level) 디자인 하는 것이다.

예를들어 위 위젯계층 구조에서 CircleImage 위젯은 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class CircleImage extends StatelessWidget {
final String renderUrl;

CircleImage(this.renderUrl);

@override
Widget build(BuildContext context) {
return new Container(
width: 100.0,
height: 100.0,
decoration: new BoxDecoration(
shape: BoxShape.circle,
image: new DecorationImage(
fit: BoxFit.cover,
image: new NetworkImage(renderUrl ?? ''),
),
),
);
}
}

그런 다음 앱의 어느 곳에서나 이 위젯을 다시 사용할 수 있다. 이렇게요 ( new CircleImage(https...) ) 이 컴포넌트는 재사용성을 고려 하여 설계 되었으므로 여러분은 특정 크기의 둥근이미지를 원하는 앱의 어느 곳에서든 url만 전달하면 사용할 수 있다. 그러므로, 굳이 이 위젯을 반복해서 다시 만들 필요는 없다.

추가적으로 이 원 이미지는(역자주: CircleImage 컴포넌트) 어떤 이미지를 나타내야 하는지 전혀 신경쓰지 않고 있다. 이것은 단지 컴포넌트의 스타일을 강제할 뿐이다.

다음 그림은 테스트앱에서 위 컴포넌트가 사용되어진 카드(card)중 하나이다.
dogs_of_cards

예제소스

Comment and share

크리에이티브 커먼즈 라이선스
이 저작물은 크리에이티브 커먼즈 저작자표시-비영리-변경금지 2.0 대한민국 라이선스에 따라 이용할 수 있습니다.

본 문서는 Fluter Example의 내용을 원저작자의 동의하에 번역한것 입니다.
원 저작자 Eric Windmill에게 감사를 전합니다.
이해하는데 불필요한 문장은 과감하게 버렸습니다. 오 번역에 대해서 의견 주시면 적극 반영 하겠습니다.

Setup and Tools(설정과 도구들)

플러터를 사용하기 위한 첫번째 단계는 Dart설치를 포함하는 플러터 SDK를 설치하는 것이다. 이 가이드 를 따라 시작하시오.

Choose an Editor with Dart Plugins (다트 플러그인을 포함한 에디터 선택)

여러분이 다트 코드를 작성하려고 할때 어떤 text에디터든 사용할 수 있지만 플러터가 지원되는 IDE를 선택하면 훨씬 더 생산적인 경험을 할 수 있다. 내가 제안할 두 가지는 다음과 같다.

  1. IntelliJ Idea
    나는 JetBranis의 IntelliJ IDEA를 적극 추천한다. community edition은 무료임.
    이 IDE는 코드완성, 스니펫(snippets), 오류강조, 린팅(linting) 등 어떤 언어든 원하는 모든 멋진 기능을 제공한다. 또한 죽여주는 디버깅 기능을 얻을게 될 것이고, 디버그 도구에는 브라우저 dev-tools 스타일 검사기 등이 있다.
    (역자주: 저의 경우 android studio에 flutter, dart플러그인 설치하는게 더 좋은것 같음.)

IntelliJ를 사용하려면 IntelliJ 내에 Dart 및 Flutter 플러그인을 설치해야한다.
IntelliJ IDEA
위젯 트리를 보여주는 플러터 검사기 - 브라우저의 dev tools의 HTML보기와 유사하다.

  1. VS Code
    다트 커뮤니티는 플러터 기능을 지원하는 VS코드용 다트 플러그인을 만들었다. 그것은 강력하지는 않지만 좋은 대안이 될 수 있다.
    (역자주: 기존에 IDEA을 사용해오셨다면 고생하지 마시고 그냥 Android Studio에 플러터, 다트 플러그인 설치해서 사용하세요.)

Pub and Libraries

pub는 다트 패키지 관리 시스템이다. 다트 및 플러터에서 사용 가능한 모든 패키지를 검색하고 정보를 얻을 수 있는 웹사이트가 있다.
다트 Pub 패키지

Gitter

문제가 발생하면 플러터 Gitter채널에서 실시간으로 도움을 요청 할 수 있다. 그곳에서 구글의 플러터 팀 구성원과 플러터를 지지하는 팬 커뮤니티를 찾을 수도 있을 것이다.

Flutter Docs

마지막으로 나는 플러터의 문서가 내가 만난 최고 중 하나라고 진심으로 생각한다. Flutter Widget Docs

Comment and share

크리에이티브 커먼즈 라이선스
이 저작물은 크리에이티브 커먼즈 저작자표시-비영리-변경금지 2.0 대한민국 라이선스에 따라 이용할 수 있습니다.

Flutter란?

in flutter

본 문서는 Fluter Example의 내용을 원저작자의 동의하에 번역한것 입니다.
원 저작자 Eric Windmill에게 감사를 전합니다.
이해하는데 불필요한 문장은 과감하게 버렸습니다. 오 번역에 대해서 의견 주시면 적극 반영 하겠습니다.

Flutter란?

Flutter는 모든 사람이 아름다운 모바일 앱을 개발 할 수 있도록 지원하는 모바일 SDK이다.
여러분이 웹개발자 이든 native 모바일 개발자 이든 관계없이 프레임워크 (역자주: android, ios, React native, Vue native…)을 사용 하지 않고 친숙하고 단순한 방식으로 모바일 앱을 보다 쉽게 만 들 수 있다.

이글을 쓰는 시점에서 Google AdWordsAlibaba 모두 플러터를 사용하고 있다.
플러터 웹사이트의 showcase 페이지에서 플러터의 사용예를 볼 수 있다.

요즘 플러터에 대해 말이 많다. 내가 종종 봤던 질문은 "Flutter랑 React Native 중에 어떤걸 사용해야 하냐?" 이다.
모든 프로그래밍이 다 그렇지만 여러분이 무언가 하고자 하면 그것에는 trade-off(이율 배반)가 따르기 마련이다.

나는 여러분에게 플러터가 모바일 앱 개발을 위한 최선의 선택이라는 것을 확신 시키기 위해 노력할 것이다.

나는 플러터가 다른 cross platform framework보다 뛰어나다고 믿으며 아마 native 개발보다 나을것이다. ( - 약간의 설명이 필요하긴 함. )

그전에 Dart 프로그래밍 언어 부터 시작해서 플러터가 무엇인지에 대해 빠르게 살펴 보겠다.

Dart란?

Dart는 구글에서 만든 프로그래밍 언어로 플러터를 작성하는데 사용됐다.
다트는 구글이 server sidefront-end코드를 작성하는데 Javascript 보다 더 나은 언어를 원했기에 만들어 졌다.

내가 알기로는, 자바스크립트의 가장 큰 문제는 승인을 위한 거대 위원회와 이를 구현하기 위한 여러 브라우저 공급업체에 의존하고 있기 때문에 새로운 기능 업데이트가 느리다는 것이다.

구글은 자바스크립트를 직접 사용할지 말지에 대한 일련의 결정 후에 의미적으로 자바스크립트와 맞는 언어를 만들기로 결정했다. 다시 말하자면 다트에서 작성되는 모든것은 자바스크립트로 컴파일 할 수 있으며 이것이 구글이 Java를 사용하지 않고 새로운 언어를 개발한 이유이다. - 이것은 큰 의미를 갖는다.

링크는 2010년 구글에서 유출된 이메일 체인 이다. 그들이 자바스크립트에 대해 뭔가 해야 한다는걸 결정한 것은 예수께 이르는 순간 이다.(역자주: 예수께 이르다는것은 무엇으로 부터 구원을 받은~ 으로 이해했음)

하지만 좋은 소식이 있다. 다트는 safe 언어를 배우는 데 탁월하다. 구글은 다트로 어떤 혁신적인것을 만들기 위해 시작하지 않았다. 그들은 단순하고 생산적이며 자바스크립트로 컴파일 될 수 있는 언어를 만들고자 했다.

구문에는 특별히 흥미로운것도 없으며 여러분을 혼란스럽게 할 특별한 연산자도 없다.
다트(자바스크립트와는 달리)에는 truefalse를 한 가지 방법으로만 표현한다.

자바스크립트에서 다음은 true로 인식된다.

1
if (3) { ... }

다트에선 위 처럼 하면 프로그램이 엉망이 될것이다. 다트의 핵심은 생산적이고 예측 가능하며 간단한 언어라는 것이다. 이것은 중요하다, 왜냐면 플러터에서 앱을 작성하는것은 단순히 다트를 작성하는 것이기 때문이다. 플러터는 하부에 다트 라이브러리 클래스들이 있다. markup 언어와 관련됐거나 JSX스타일의 하이브리드 언어는 없다. 모든 프론트 엔드 코드는 다트로 작성된다. No HTML, No CSS.

플러터가 다트를 사용하는 이유는?

만약 여러분이 다른 기술배경을 사용해봤다면 아마도 플러터가 자바스크립트가 아니라 다트를 사용한다는 사실에 대해 불평했을 것이다. (개발자들은 믿거나 말거나 독단적이다.)
그리고 이 선택에 회의적인 이유는 있다. 다트가 오늘날 가장 많이 사용되는 25개 언어에 포함되지 않는 인기 없는 언어라는 것이다. “뭐라구요? 구글이 자사 언어라서 사용했다구요?” 나는 그것도 맞는 소리라고 생각하지만 실용적인 이유도 있다.

  • 다트는 JIT(Just In Time)컴파일과 AOT(Ahead Of Time)컴파일을 모두 지원한다.
    • AOT컴파일러는 다트코드를 효율적인 native 코드로 바꾼다. 이것은 플러터를 빠르게 만들지만,(사용자와 개발자에게 승리) 거의 모든 프레임워크가 다트로 작성되었음을 의미 한다. 여러분 즉 개발자가 모든 것을 커스터마이징한다는 것이다.
    • 다트의 옵션인 JIT컴파일은 hot-reloading을 가능케 한다. 빠른 개발과 반복은 플러터 사용 즐거움의 핵심이다. 텍스트 편집기에 코드를 저장하면 시뮬레이터상의 앱이 1초 이내에 업데이트 된다.
  • 다트는 객체 지향이다. 이것은 다트만으로 시각적 사용자 경험(UX)을 쉽게 작성 할 수 있게 해주며, markup language는 필요치 않다.
  • 다트는 생산적이고 예측 가능한 언어이다. 배우기 쉽고 친숙하다. 여러분이 동적언어(dynamic language)를 사용해 왔든 정적언어(static language)를 사용해 왔든 쉽게 시작 할 수 있다.
  • 그리고 나는 같은 회사에서 만든 언어를 사용하는 것이 매우 매력적이라는 생각이 들기도 한다. 왜냐면 플러터 팀은 다트 팀과 긴밀히 협력하여 필요로 하는 새로운 기능을 구현할 수 있었기 때문이다.

Flutter vs. React Native ( 및 기타 선택 )

여러분의 다른 선택에 대한 나의 의견을 제시하기 전에 이것을 분명히 말하고 싶다. 플러터는 100% 정답이 아니다. 그것은 도구이고 우리는 당면한 일에 적합한 도구를 선택해야 한다는것이다. 즉 나는 이것이 미래에 여러분이 강력하게 고려해야 할 것이라고 주장 할뿐이다.

Native develoment(iOS and Android)

여러분의 첫번째 선택은 iOSAndroid native앱을 쓰는 것이다. 이것은 최대한의 제어와 디버깅 도구, 그리고 (잠재적으로) 성능 좋은 앱을 제공한다. 이말은 회사에서 모든것(개발대상)을 각 플랫폼마다 한번씩, 두번 작성(개발)해야 한다는 것을 의미한다. 여러분이 서로 쉽게 도울 수 없는 다른기술을 가진 다른팀의 다른 개발자가 필요할 것이다.

React Native, WebViews, and other cross-platform JavaScript options

여러분의 두번째 선택은 WebViewsReact Native와 같은 자바스크립트 기반의 cross-platform 이다. 이건 나쁜 선택이 아니다. 네이티브 개발로 겪는 문제가 사라진다. 팀의 모든 프론트엔드 웹 개발자는 최신 자바스크립트 기술만 알고 있으면 이를 지원(사용)할 수 있다.
AirBnb, Facebook, Twitter와 같은 대기업들이 React Native를 핵심 제품(서비스)에 사용한 것도 이런 이유에서이다. (AirBnb는 최근 아래 설명할 몇가지 이유로 인해 React Native 사용을 중지 한다고 발표했다.

첫번째 모바일 앱은(Airbnb의) WebKit(브라우저 렌더링 엔진)에서 실행되는 간단한 WebViews 였다. 문자 그대로 내장된 웹페이지다. 이것의 문제는 기본적으로 DOM조작이 매우 비싸고 훌륭한 모바일 경험(UX)을 만들 만큼 성능이 좋지 않다는 것이다.

일부 플래폼에선 Javascript Bridge를 구축하여 이런 문제를 해결 했다. 이 브릿지는 자바스크립트가 네이티브 위젯과 직접 대화(통신) 할 수 있게 한다.

이것은 WebViews보다 훨씬 더 뛰어난 성능을 발휘 하지만 여전히 이상적이진 않다.
앱이 렌더링 엔진과 직접 대화(통신)할때 마다 브릿지를 위해 네이티브 코드로 컴파일 되어야 한다.
단일 상호 작용에서 브릿지는 두 번 교차해야 한다. 한번은 플랫폼에서 앱으로 그리고 다시 앱에서 플랫폼으로.


Single interaction

플러터는 Chrome에서 사용하는것과 동일한 렌더링 엔진인 Skia를 자체 렌더링 엔진으로 사용하기 때문에 다르다. Skia는 플러터 앱과 통신 할 수 있다. 그 결과 로컬 이벤트를 자바스크립트로 먼저 컴파일 할 필요 없이 직접 받아 들인다.
플러터가 native ARM코드로 컴파일 되기 때문에 가능하고 이것이 성공의 비결이다. 여러분의 앱이 사용자의 장치에서 작동되면 이 앱은 전적으로 장치의 운영체제가 기대하는 언어로 실행된다.


Javascript bridge는 확실히 현대적 프로그래밍의 경이로운 부분이지만, 세 가지 큰 문제가 존재한다.

첫번째 문제는 디버깅이 어렵다는 것이다. 런타임 컴파일러에 오류가 있을 경우 해당 오류를 자바스크립트 브릿지를 통해 추적하여 자바스크립트 코드에서 찾아야 한다. 그것은 마크업 또는 CSS와 같은 구문 일 수도 있다. 디버거는 우리가 원하는 만큼 잘 작동하지 않을 수도 있다.

두번째로 큰 문제는 성능이다. 자바스크립트 브릿지는 매우 비싸다. 앱에서 무언가 탭(tapped)될 때마다 해당 이벤트가 브릿지를 통해 자바스크립트 앱으로 전송되어야 한다. “적절한 용어를 찾지 못하겄다! 걍 jank다.” (jank는 품질에 문제가 있는것들을 의미함. )

세번째로 큰 문제는 AirBnb에 따르면 그들은 자신들이 원하던 것보다 더 자주 native 코드를 까봐야 한다는 것을 발견했다고 한다. 그것은 대부분 자바스크립트 개발자로 구성된 팀들의 문제였다.

플러터의 즉각적인 이점

이 글을 읽고 있으니 플러터에 관심이 있을 수도 있지만… 회의적일 수도 있다. 나는 여러분의 철저한 기술조사에 존경(감탄) 한다.
여러분의 회의적인 이유는 타당하다. 그것은 새로운 기술이다. 이는 API의 변경사항을 깨뜨리는 것을 의미 한다. 이는 중요한 기능 (예: 구글지도)에 대한 지원이 누락 되었음을 의미한다. 구글이 언젠가 그것을 포기 할 수도 있다.

그리고 여러분이 다트가 훌륭한 언어라고 믿는 사실에도 불구하고, 다트가 널리 사용되지 않고 여러분이 원하는 third-party 라이브가 존재하지 않을 수도 있다는 사실은 변하지 않는다.

하지만 나는 그 모든 점들에 대해 논쟁을 벌일 것이다. 구글은 Google AdWords를 포함한 주요 수익 창출 앱에서 내부적으로 플러터를 사용하기 때문에 API는 바뀔것 같지 않다.

다트는 최근 버전2로 변경됐고, 이는 대 규모 변경이 있기까진 시간이 걸릴 것이라는걸 의미한다.
컴퓨터 세계에서 사실상 영구적인 변경이 소개 되기까지는 수년이 걸리지 않는가…

맞습니다. 실제로 누락된 기능들도 있지만 플러터는 여러분에게 여러분 고유의 native 플러그인을 추가 할 수 있는 완전한 제어권을 제공한다.
사실 map, camera, location service, device storage와 같은 가장 중요한 operationg system플러그인은 이미 존재한고 다트와 플러터의 생태계 커뮤니티도 이미 존재한다. 물론 자바스크립트 커뮤니티보다 훨씬 작지만 간결하다고 주장 하고 싶다. 나는 매일 새로운 패키지를 만드는 대신 기존 패키지에 기여 하는 사람들을 봤다.

이제 플러터의 구체적인 이점에 대해 얘기해 보자.

No JavaScript bridge

이것은 개발 및 응용프로그램 성능의 주요 병목이다. 다시 말하지만 이것은 jank로 이어진다. (위에도 jank에 대해 언급 했지만 한마디로 x같다.임)
스크롤이 매끄럽지 않고 항상 성능이 좋은것도 아니고 디버깅 하기도 어렵다.

플러터는 실제 native코드로 컴파일 되고 Skia를 사용해서 렌더링 된다. 앱 자체도 native로 실행되므로 다트를 native로 변환할 이유가 없다.
이는 사용자의 기기에서 실행될 때 어떠한 성능이나 생산성을 잃지 않는다는것을 의미한다.

Compile time

여러분이 만약 native개발자 라면, 여러분의 큰 고통 중 하나는 개발주기 였을겁니다. iOS의 미친 컴파일 시간은 악명이 높다. 플러터에서 전체 컴파일은 일반적으로 30초 미민이며, 핫 리로드 덕택에 증분 컴파일은 초 단위 이다. 플러터의 이런 개발주기 덕분에 우리는 모바일 클라이언트를 위한 기능을 빠르게 개발 할 수 있게 되었다. 구현히 확실할때만 웹 클라이언트에 이러한 기능을 개발한다.

Write once, test once, deploy everywhere (한번의 개발, 한번의 테스트, 어디에나 배포)

앱을 한 번만 작성하고 iOSAndroid에 배포 할뿐만 아니라 테스트를 한번만 작성하면 된다. 다트 unit테스트는 아주 빠르고 플러터는 테스트용 Widget들을 포함하고 있다.

Code sharing (코드 공유)

나는 여기서 공평해질꺼다. 나는 이것이(코드공유) 자바스크립트에서도 기술적으로 가능하다고 생각한다. 하지만 확실히 native개발에선 불가능하다. 플러터와 다트를 사용하면 웹 및 모바일 앱이 각 클라이언트들의 뷰를 제외한 모든 코드를 공유 할 수 있다.(물론, 다트를 통해서 웹 앱을 만든 경우에만 해당). 의존성 삽입을 사용하여 동일한 modelcontrollerAngularDar앱과 플러터 앱을 쉽게 실행 할 수 있다.

물론 웹앱과 모바일 앱간에 코드 공유를 하고 싶지 않더라도 모든 코드를 iOSAndroid앱간에 공유 할 수 있다.

실용적인면에서 이것은 여러분이 매우 생산적이라는것을 의미 한다. 앞서 말했지만 우리 회사는 먼저 모바일 기능을 개발한다. 왜냐면 웹과 모바일간에 비지니스 로직을 공유하기 때문에 일단 모바일 기능이 구현되면 동일한 컨트롤러 데이터를 필요로 하는 view만 작성하면 되기 때문이다.

Productivity and collaboration (생산성과 공동작업)

iOSAndroid용 팀이 분리 되는 시대는 끝났다. 사실 여러분이 여러분의 웹 앱에서 다트를 사용하든, 자바스크립트를 사용하든 간에, 플러터 개발은 여러분의 모든 팀이 통합될 정도로 충분히 친숙하다. 자바스크립트 웹 개발자가 플러터와 다트에서도 효과적으로 개발할 것으로 기대하는건 결코 쉬운 일이 아니다. 여러분이 나를 믿는다면, 여러분의 새로운 통합팀은 3배 더 생산적인 팀이 될 것 이다.

Code maintenance (코드 유지보수)

버그를 한번 고치는게 모든 client를 고친는것 보다 났다는것은 두말 하면 잔소리지만 매우 특정한 경우에 한해선 플러터로 제작된 iOS앱에 버그가 있다면 Android 버전에도 존재한다. (반대의 경우도 마찬가지) 이런한 경우 100% 이러한 버그는 버그가 아니라 내장된 위젲의 기기 OS설계 시스템을 따르기 때문에 외관상의 문제가 된다. 이러한 문제는 텍스트 크기 조정 또는 정렬과 같은 문제이기 때문에 엔지니어링 시간을 사용하여 수정하는 맥락에서 보면 사소한 것이다.

Flutter for JavaScript developers (자바스크립트 개발자들을 위한 플러터)

CSS-Tricks를 읽어 있으니 나는 여러분이 웹 개발자라 확신한다.(역자주: CSS-Tricks가 뭔지를 모르겠음) 만약 여러분이 오늘날 가장 인기 있는 framework을 사용한다면, 여러분이 플러터를 줍는게(공부하는게) 쉽다는 것을 알게 되어 기뻐할 것이다.

플러터는 완전히 반응형(reative)이기 때문에 React에서 익숙해진 동일한 사고방식과 패러다임이 플러터로 이어진다.
여러분은 기본적으로 React처럼 재사용할 수 있는 수 많은 컴포넌트(풀루터에선 Widget이라고 부름)를 만들고 있을 것이다. 이 위젯들은 라이프 싸이클(life cycle)메소드로 완성되며, 클래스(class)로 작성된다.

React에서 이 구문을 사용한 경우:

1
2
3
4
const MyComponent extends React.Component {
//...
render(){}
}

…그리고 아무 문제 없이 플러터를 잡을 것이다. 플러터에서 동일한 작업을 수행하는 방법은~

1
2
3
4
class MyWidget extends StatelessWidget {
//...
build(){}
}

그리고 React와 마찬가지로 플러터에서도 상속(inheritance)보다는 구성(composition)을 선호한다.
예를 들어 여러분이 React에서 특별한 AddToCartButton을 만들기 원한다면, JSX의 특별한 함수와 스타일을 사용하여 버튼을 만들 수 있다. 플러터에서도 정확히 그렇게 하는 거다. (JSX는 제외하고~)

마지막으로 플러터의 레이아웃(Layout) 시스템은 우리에게 익숙한 CSS규칙, 즉 flexbox와 절대위치 지정(absolute positioning)과 비슷하다.
이것은 또한 플러터의 view를 만드는데 있어 큰 차이점이 있다. 플러터에선 문자 그대로 모든것이 위젯(Widget)이다. Text, ButtonAppBar와 같은 명확하고 구체적인 위젯이 있고 Animations, Layout 또한 위젯이다. Text를 가운데로 정렬하기 위해 Center위젯으로 Text 위젯을 감싼다. 패딩(padding)을 추가하려면 Padding이 있다.

React 앱을 가능한 가장 작은 재사용 가능한 구성 요소로 분해 한다고 상상해 보자. 예를 들어 여러분이 단순히 Padding 속성을 다루는 고차원의 React 컴포넌트를 만든 경우 그 중첩된 패딩을 해당 패딩 양만큼 추가하면 된다. 이것이 CSS나 마크업 없이 플러터가 작동하는 방식이다.

이 샘플 그림에서는 사용할 수 있는 몇 가지 레이아웃 위젯을 설명하지만 사용자로서 '참조’할 수는 없다:

Examplelayout
엄청나게 단조로운 작업처럼 보일 수 도 있지만, 플러터에는 (PaddingCenter와 같은) 내장된 아주 많은 위젯들이 있기 때문에 여러분은 쓸때 없이 시간을 낭비할 필요가 없다.

다음은 가장 일반적인 위젯 중 하나이다.

Final note

플러터를 사용해 보겠는가?
만약 여러분이 버터같은 부드러운 모바일 앱을 익숙한 스타일로 만들고 싶다면, Yes! 성능과 개발자 경험 모두 플러터에서 완전하게 유지된다.
애니메이션은 60fps에 달하며 내장된 Cupertino-styleMaterial Design-style의 위젯이 번들로 제공되며 간단히 말해, native 성능을 희생하지 않고 플러터에서 얼마나 빨리 생산적으로 작업 할 수 있는지는 믿기 힘들정도 이다.

Comment and share

크리에이티브 커먼즈 라이선스
이 저작물은 크리에이티브 커먼즈 저작자표시-비영리-변경금지 2.0 대한민국 라이선스에 따라 이용할 수 있습니다.
Author's picture

Jace Shim


Seoul, Korea