파이썬 인터프리터를 종료하고 싶으시면 파일을 끝으로 가는 단축키 (유닉스에서는 Ctrl+D, 윈도우에서는 Ctrl+Z)를 눌러주세요. 이 액션은 파이썬을 아무 문제없이 종료한다는 의미의 zero-exit-status로 안전하게 종료하게 해줍니다. 만약 단축키가 잘 안먹으면 실행창에 quit()라고 치고 엔터를 누르면 마찬가지로 종료가 됩니다.
파이썬 인터프리터의 라인별 수정기능은요, 대화형 코딩과 기존 코딩을 불러와 변경하여 실행할수 있는 기능, 그리고 GNU Readline에 입각한 코드완성기능까지 지원합니다. 커맨드라인 명령어 수정을 지원 하는지를 확인하기 위한 가장 빠른 방법은 파이썬 프롬프트를 실행하고 바로 Ctrl+P를 입력하는 것입니다. 만약에 삐하는 소리가 들리면 커맨드라인 수정이 가능하다는 뜻입니다. 각 단축키들의 설명을 위해 Interactive Input Editing and History Substitution를 확인해 주세요. 만약 Ctrl+P를 쳤는데 아무 소리도 나지 않거나 ^P가 출력이 된다면 커맨드라인 명령어 수정은 지원하지 않는 개발환경입니다. 그런 경우에는 불편하시더라도 Backspace키를 눌러 수정할 명령어를 삭제하고 다시 입력하여 수정을 진행해주세요.
파이썬 인터프리터는 어떻게 보면 유닉스 쉘같기도 해요. 마우스나 키보드같은 일반적인 입력장치가 TTY장비와 연결이 되면 입력받은 명령어를 읽고 실행하는 것을 주거니 받거니 할수 있습니다. 만약 인터프리터의 입력이 입력장치가 아닌 파일이라면 해당 스크립트를 실행하라는 의미로 받아들이고 읽고 실행하여 결과를 화면에 출력합니다.
두번째로 파이썬 인터프리터를 실행하는 방법은 python -c command [arg] ...명령어를 이용하는 방법입니다. 이것은 쉘에서 -c 옵션을 사용한것과 비슷하게 -c 이후에 오는 명령어를 실행합니다. 파이썬 코드에 공백이 들어가는 것이 일반적이기 때문에 보통 명령어부분을 통째로 따옴표로 감싸는 것을 권장해 드립니다.
파이썬 모듈들도 스크립트 못지않게 유용하게 사용이 됩니다. python -m module [arg] ...를 통해 실행할수 있는데요, 명령어에 -m다음에 특정 모듈의 이름을 명명함으로써 소스파일내에 특정 모듈만 실행하도록 하는것입니다.
스크립트 파일을 이용해서 파이썬을 돌릴때는요, 일단 스크립트를 돌리고 나중에 대화형모드로 들어가도록 설정할수 있습니다. 이 설정은 명령어를 입력할때 스크립트 파일명 전에 -i를 넘겨주면 됩니다.
스크립트와 추가적인 옵션들은 sys모듈 안에 있는 argv변수 뒤에 문자열 목록으로 나열함하여 전달합니다. 이 목록은 import sys를 실행하여 접근이 가능합니다. 목록의 길이는 적어도 하나의 항목을 가져야하며, 만약 스크립트도 없고 전달변수도 없는 경우에는 sys.argv[0]가 빈 문자열로 출력이됩니다. 만약 스크립트 이름이 -이면 (기본 인풋을 의미합니다), sys.argv[0]는 -으로 설정이 됩니다. 만약 -c옵션이 사용되면, sys.argv[0]는 -c로 설정됩니다. 만약 -m옵션이 사용되었다면, sys.argv[0]는 지정한 모듈의 이름으로 설정됩니다. -c나 -m옵션의 뒤에 따라오는 명령어들은 파이썬 인터프리터에 의해 처리되지 않습니다. sys.argv에 저장되어 다음 명령이나 처리할 모듈을 기다립니다.
Interactive Mode
명령어가 TTY로 부터 읽혀질때, 인터프리터는 interactive(대화형) 모드로 들어가라고 명령을 받게 됩니다. 이 모드에서는 프롬프트에서 Primary Prompt로서 다음 명령을 기다립니다. 이때 프롬프트는 보통 3개의 꺽쇠 (>>>)로 표현됩니다. 만약 명령이 다음 줄로 이어진다면 이때는 Secondary Prompt로써 명령어를 기다리게 되는데 이때 프롬프트는 3개의 점(…)으로 구성됩니다. 인터프리터를 실행하면 제일 처음 환영 메세지와 버젼을 화면에 출력하여 정보를 보여주고, 이어서 저작권정보 등도 보여준 후에 첫번째 프롬프트를 띄워 다음명령어를 기다립니다.
$ python3.12
Python 3.12 (default, April 4 2022, 09:25:04)
[GCC 10.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>
만약 여러분이 인터프리터 모드에서 여러개의 라인에 하나의 statement을 표현하고 싶다면, 연속라인 모드로 표현하게 됩니다. 예들 들자면 아래코드의 if statement같이 말입니다.
>>> the_world_is_flat = True
>>> if the_world_is_flat:
... print("Be careful not to fall off!")
...
Be careful not to fall off!
기본적으로 파이썬의 소스코드들은 UTF-8로 캐릭터셋이 설정이 되었다는 가정하에 파일들을 처리합니다. 해당 엔코딩에서 제공하는 캐릭터들은 대부분의 언어에서 사용되는 문자열이고 구분자이며, 주석입니다. 비록 스탠다드 라이브러리에서는 오직 구분자등을 위해서 ASCII 캐릭터만 사용하지만 말이죠. 이런 다양한 문자들을 제대로 보여주기 위해서는, 코딩하는 개발자가 파일이 UTF-8인지 왜 그래야하는지 등을 정확하게 알고 있어야합니다. 또한 파일안의 모든 문자들을 지원하는 폰트를 반드시 사용해야합니다.
기본적으로 설정된 엔코딩 이외의 엔코딩을 정의하려면 파일의 첫번째 줄에 특별히 약속된 문구를 넣어줘야합니다.
만약 여러분이 컴퓨터로 일을 많이 하시는 분이시라면, 자주 반복되는 일들을 자동으로 처리하게 하고 싶다는 생각을 한번쯤은 해보셨을 겁니다. 예를 들면, 엄청나게 많은 파일이 있는데, 그 안에서 어떤 특정 단어를 찾아서 다른 단어로 바꿔야 한다던가, 아니면 엄청나게 많은 이미지파일들이 있는데, 각 이미지파일들의 이름을 변경하여 정리를 하게 한다던가 하는 일 말입니다. 그걸 일일이 수동으로 하는 것 보다는 필요한 데이타베이스를 생성하고, 간단한 화면에 연동하여 반복되는 작업들을 한번의 클릭으로 간편하게 처리할수 있도록 프로그램으로 만들고 싶으실겁니다.
여러분중에 전문 소프트웨어 개발자가 계시다면, 아마도 C, C++또는 자바로 필요한 코딩을 한뒤 반복적으로 컴파일하고 테스트하고 코드를 수정하고 재 컴파일하는 방식으로 일을 하셨을겁니다. 하지만 이 방법은 매번 코드를 수정할때마다 컴파일을 다시해야하니까 개발속도가 매우 느리죠. 아마도 그렇게 개발을 해야 한다면 테스트 코드를 쓰는 일이 매우 길고 지루한 작업이라고 여겨질 것입니다. 아니면 해당부분을 보완할수 있는 별도의 프로그램을 만들어서 사용하셨을수도 있겠지만 그걸 위해 새로운 언어를 설계하고 만드는 일은 하고 싶지 않으실거에요.
그런 분들을 위해 나온 언어가 바로 파이썬입니다.
이런 반복적인 일들을 해결하기 위해서 유닉스 쉘 스크립트를 짜거나 윈도우 배치파일을 만드시는 분도 계실거에요. 하지만 쉘스크립트는요 파일들을 옮기거나 파일안의 문자열을 변경하거나 할때는 유용하지만 사용자화면이나 게임을 만들때는 좋지가 않죠. 물론 C, C++ 또는 자바를 이용해서 프로그램을 짤수도 있지만 이런 언어들로는 첫화면을 만드는 것 만도 시간이 만만치 않게 걸릴거에요. 그에 반해 파이썬은 매우 간단하게 다양한 플랫폼에서 원하는 작업을 빠르게 끝낼수 있도록 여러분을 도와줄거에요.
파이썬은 매우 심플합니다. 동시에, 굉장히 복잡한 작업, 대용량 작업들을 한번에 개발할 수 있는 프로그래밍 언어입니다. 단순 작업만 할수 있는 쉘스크립트나 배치파일과는 비교가 안되죠. 심지어 파이썬은 C보다 더 정교한 에러 찾기 기능을 제공하고 있고, 동적배열이나 사전과 같은 데이타 구조를 지원하는 high-level언어입니다 (여기서 high-level은 기계와 더 가까운 밑단을 의미하고 low-level이 사용자 화면과 가까운 쪽을 의미합니다). 이러한 다양한 데이타 타입 덕분에 파이썬은 Awk나 Perl보다 더 큰 프로젝트에 사용되어 질 수 있으며, 이 두 언어가 제공하는 간편한 기능들은 상당부분 파이썬도 손쉽게 구현할 수 있도록 제공하고 있습니다.
파이썬은 여러분의 모듈들을 나눠서 다른 프로그램에서 다시 사용할수 있도록 허용합니다. 파이썬은 매우 광범위한 기본 모듈들을 가지고 있으며, 이런 모듈들은 매우 쉽게 파이썬 프로그램을 배울수 있게 해줍니다. 이 모듈들은 파일은 열고,닫거나, 운영체제와 통신하기도 하며, 소켓프로그래밍, 그리고 심지어는 Tk와 같은 사용자 화면을 구성하는 툴킷도 제공하고 있습니다.
파이썬은 인터프리터 언어입니다. 말인 즉슨 코딩을 하고나서 컴파일이나 링킹을 할 필요없이 바로 결과를 확인할수 있다는 걸 의미합니다. 파이썬 인터프리터는 커맨드 라인에서 바로바로 대화하듯이 결과를 보는것이 가능합니다. 굳이 프로그램 파일을 만들어서 실행하지 않아도 각종 명령어나 함수들을 인터프리터에 직접 입력하여 결과를 보는 것이 가능하기때문에 간단한 코드를 만들어 테스트할때 좋고 따로 계산기 프로그램 열 필요없이 인터프리터가 열려있다면 계산공식을 넣어서 쓰는것도 매우 유용합니다.
파이썬은 다른 언어에 비해 매우 컴팩트하고 읽기에도 편합니다. C나 C++, 자바같은 프로그램에 비해 파이썬은 훨씬 짧은 코드로 같은 기능을 구현할 수 있는데 그 이유는 다음과 같습니다.
high-level 데이타 타입들이 복잡한 코드를 한줄로 쓸수 있게 해줍니다.
시작과 끝을 명시하는 괄호를 과감히 생략하고 들여쓰기로 단락을 구분합니다.
변수선언을 해줄 필요없이 그냥 바로 쓰면 됩니다.
파이썬은 확장성이 좋습니다. 여러분이 만약에 C 프로그래머라면 파이썬으로 빌트인 함수나 모듈을 만들수 있습니다. 그렇게 확장을 해도 속도에 예민한 프로그램이라도 최대속도로 운영이 가능합니다. 바로 인터프리터를 이용하는게 싫으시다면 파이썬 프로그램을 바이너리로 만들어서 더 빠르게 이용하실수도 있습니다. 파이썬 확장을 더욱 용이하게 하고 싶으시면, 아예 C로 만들어진 프로그램에 파이썬 인터프리터를 링크를 걸어서 필요할때마다 바로바로 프로그램에서 코딩을 하도록 구현할수도 있습니다.
참고로, 파이썬이라는 이름은 BBC에서 방영했던 코메디 시리즈 “Month Python’s Flying Circus”에서 따왔습니다. 많은 사람들이 오해하고 있는데, 뱀의 한 종류인 파이썬과는 전혀 관련이 없어요. Monty Python에 관한 이야기를 정식 매뉴얼에서 언급해도 되는건지 상사에게 허락은 받았냐고요? ㅎㅎ 오히려 꼭 좀 언급 해달라고 부탁을 받을 정도였습니다.
이제 파이썬에 대해 뭔가 기대가 좀 되지 않으시나요? 더 깊이 있게 배워보고 싶으실거에요. 언어를 배우는데 가장 좋은 방법은 바로 써보는 것입니다. 앞으로 소개할 튜토리얼에서는 여러분을 파이썬 인터프리터의 세계로 안내해드릴거에요.
다음 챕터에서는 인터프리터를 사용하는 장비들에 대해서 설명드릴거에요. 이건 그냥 평범한 설명이라 중요하지는 않지만 그 뒤에 나오는 예제는 정말 중요한거니까 반드시 실행해 보시기를 권장드릴게요.
남은 튜토리얼은 파이썬 언어와 체계에 관한 매우 다양한 기능들을 소개하고 있으니까요, 예제나 간단한 표현들, 각종 문법들과 데이타 타입, 그리고 함수와 모듈, 그리고 좀더 심도높은 개념의 예외처리와 사용자정의 클래스등 다양한 것들 다룰 예정입니다.
파이썬은 배우기 쉽고, 매우 강력한 프로그램 언어입니다. 파이썬은 매우 효율적인 데이타구조를 지니며, 간단 명료한 객체지향형 프로그램을 지원합니다. 파이썬의 수준높은 문법 그리고 역동적인 데이타타입등은 자연스럽고, 미래지향적인 언어임을 입증하고 있으며, 이는 다양한 분야, 각종 플랫폼에서 신속한 개발을 가능하게 합니다.
파이썬 인터프리터와 광범위한 기본제공 라이브러리는 소스코드에서 자유롭게 접근할수 있고, 대부분의 플랫폼에서 쓸수 있는 바이너리형태로도 제공됩니다(소스나 바이너리 코드는 https://www.python.org에서 다운받으세요). 다운 받은 코드는 자유롭게 배포하셔도 됩니다. 사이트에 가보시면 여러 분야에서 종사하고 계시는 개발자 분들이 올려주신 다양한 파이썬 모듈, 프로그램, 유용한 툴, 그리고 각종 문서들도 함께 보실수 있습니다.
파이썬 인터프리터는 매우 쉽게 C나 C++로 개발된 함수나 데이타 타입과 연동이 가능합니다. 또한 파이썬은 기존에 구현된 응용 프로그램에 확장된 형태로 추가하여 개발할 수도 있습니다.
본 튜토리얼은 기본적인 파이썬의 컨셉과 기능들을 소개할것이며, 또한 각종 경험을 해보실수 있는 예제들을 제공하여 오프라인 환경에서도 튜토리얼을 습득할수 있습니다.
본 튜토리얼은 파이썬의 전반적인 모든 기능들을 커버하지는 않습니다. 여기에서는 주로 사용되는 기능들을 알려주기 보다는 파이썬의 두드러진 특징들을 소개하도록 할것입니다. 이로 인해 파이썬이라는 언어가 어떤 스타일, 어떤 취향의 언어인지를 이해하는데 도움을 드리도록 할것입니다. 이 튜토리얼을 마친뒤 여러분은 파이썬 모듈과 프로그램을 읽고 쓸줄 알게 될것이며, 파이썬 기본라이브러리에 있는 다양한 모듈들을 익힐 준비가 되어있으실겁니다.
이번시간에는 Kivy를 가지고 탁구게임을 한번 만들어 보도록 할게요. 앱이름은 pong이라고 할거에요. 일단 소스코드를 보관할 pong이라는 폴더를 만드시고요, 그 안에 main.py파일을 생성해서 아래 코드를 입력해주세요.
from kivy.app import App
from kivy.uix.widget import Widget
class PongGame(Widget):
pass
class PongApp(App):
def build(self):
return PongGame()
if __name__ == '__main__':
PongApp().run()
위의 코드에서 한거라곤 이름 정한거 밖에 없어요. App의 이름은 Pong이구요. 그래서 맨밑에 실행하는 파일명이 main이면 PongApp()을 객체로 구현한뒤에 run()함수를 호출 하도록 해준거에요. 그리고 PongApp을 App클래스로 정의했는데 App 클래스가 호출이 되면 build()를 가장먼저 실행하는데 여기서 PongGame위젯을 객체화하여 반환하도록 했습니다. PongGame이 선언된 클래스를 보시면 아무것도 없이 pass만 넣어두었습니다.
이렇게 PongApp을 만들건데 실행되면 PongGame을 실행하라고 일단 윤곽만 만든거에요. 참고로 PongGame은 Kivy에서 지원하는 Widget클래스를 확장해서 만들었습니다. Widget은 화면에 보여주는 각종 속성들과 사용자 입력을 받아내는 이벤트들이 기본적으로 장착이 되어 있어서 화면에 보이는 모든 객체들은 전부 Kivy가 제공하는 Widget을 확장하여 정의해야합니다.
이제 화면구성을 해볼텐데요. Kivy가 좋은점이 몇가지 설정만으로 화면을 구성할수 있는 kv설정파일을 지원한다는 겁니다. main.py와 같은 폴더에 pong.kv라는 파일을 생성해주세요. 이곳에 화면그래픽에 필요한 설정을 해줄겁니다.
그래픽 설정 파일명이 pong.kv인 것은 그냥 우연이 아니라 main.py에서 지어준 앱의 이름이 PongApp이기 때문입니다. App빼고 Pong이 이 앱의 실제 이름인데 소문자로 파일명을 만들어 주면 해당 앱이 로딩될때 자동으로 해당 파일도 로딩이 되어 별도의 설정이나 코딩없이 바로 그래픽이 적용이 됩니다.
pong.kv파일의 가장 첫번째 줄을 보면 #:kivy 1.0.9라고 주석이 달려있습니다. 이는 Kivy버젼을 명시하는 것인데, 버젼에 따라 설정파일의 형식이 달라지므로 kv파일의 가장 첫 줄에는 무조건 #:kivy로 시작되는 버젼명을 명시하도록 하는것이 필수사항입니다.
그 밑으로 <PongGame>이라고 되어있는데 kv파일에서 <와 >로 감싸여 있는 것은 바로 소속 위젯을 뜻합니다. 즉슨, kv파일의 <PongGame>에 나열된 그래픽이 main.py의 PongGame이라는 위젯의 객체로 소속이 된다는 의미입니다.
pong.kv안의 코드를 보면 <PongGame>아래에 처음 설정된 키값이 바로 canvas입니다. canvas로 설정된 값에는 그림이나 도형이 들어간다는 것을 뜻합니다. canvas밑에 보면 Rectangle이 키로 설정되어있는데 바로 화면에 사각형을 하나 그리겠다는 의미입니다. 포지션은 x축은 가운데에서 왼쪽으로 5픽셀 간곳, 그리고 세로로는 가장 윗쪽에 위치시킨다고 설정이 되어있습니다. 그리고 사각형의 가로는 10픽셀, 그리고 세로는 PongGame에 할당된 모든 세로길이를 전부 세로길이로 갖습니다. 세로로는 끝까지 화면을 꽉채우라는 의미지요.
그리고 라벨이 두개 있는데, 폰트사이즈는 70픽셀, 가로위치는 하나는 왼쪽 1/4위치에 하나는 오른쪽 3/4위치에 놓으라고 설정되어있습니다. 그리고 그 안에 문자열 “0”을 각 라벨에 넣으라고도 되어있습니다.
여기까지 놓고 실행을 한번 해볼까요?
python pong/main.py
설명드린대로 가운데 얇은 사각형이 가로 10픽셀, 세로 화면 끝까지 그려져 있고, 각각 화면의 1/4, 3/4지점에 문자열 “0”이 라벨로 올라가 있습니다.
여기까지 보면 우리편 진영과 상대편 진영을 나누고 점수를 보여줄 점수판까지 만들어진 상태입니다.
Making a Ball
탁구게임에서 가장 중요한 “탁구공”을 만들어 보도록 하겠습니다. 공을 만들려면요. 물론 그래픽이 있어야겠지만 그래픽을 어디에 설정할거냐는 바로 코드에서 정해지는 것이기 때문에 main.py에 PongBall이라는 위젯을 만들어서 그 안에 그래픽을 넣어야합니다. main.py에 새로운 위젯클래스 PongBall을 하나 추가합니다. 아직 안에 들어갈 내용은 없으니 그냥 pass만 넣을게요. 다른 위젯에서 불러쓸 위젯이니까 제일 위에 정의하는게 좋겠죠? 참고로 마찬가지로 Widget을 확장해서 선언했습니다.
class PongBall(Widget):
pass
그리고 그 위젯 안에 그래픽을 넣을거에요. pong.kv를 열어서 PongBall에 들어갈 그래픽을 정의합니다.
kv파일의 <PongBall>은 코드의 PongBall위젯과 같은 의미입니다. 그래서 kv에서 정의된 그래픽들은 해당 위젯에 포함이 됩니다. PongBall에서 사용할 그래픽의 크기는 가로세로 50픽셀입니다. 그리고 그 안에 canvas를 정의하여 Ellipse로 동그라미를 그려줍니다. 그리고 동그라미의 위치는 현재 PongBall의 위치로, 그리고 동그라미의 크기는 현재 PongBall의 크기로 설정해줍니다. Widget에는 각종 기본 속성 들이 들어가있는데 self.pos를 따로 선언해준 적이 없는데 Ellipse.pos에 self.pos를 할당해준것도 self.pos가 기본 속성으로 제공되기 때문입니다.
그러면 이제 만든 탁구공을 게임에 추가시켜볼게요. 기존의 PongGame위젯 에서 공에 쉽게 접근할수 있도록 ball이라는 클래스변수를 하나 만들고 거기에다가 PongBall을 구현해서 객체로 저장해서 사용하도록 할텐데요. 일단 코드에서는 해당 변수에 ObjectProperty를 할당한뒤 나중에 pong.kv에서 PongBall를 할당할거에요.
PongGame에 ball을 선언해서 임시로 ObjectProperty객체를 할당합니다.
from kivy.properties import ObjectProperty
class PongGame(Widget):
ball = ObjectProperty(None)
이제 임시로 넣어준 객체를 실제 공으로 바꿀 차례인데요. 그 액션은 pong.kv설정파일에서 합니다.
pong.kv파일을 열어서 가장 먼저 <PongGame>아래에 PongBall라는 속성을 추가해서 해당 객체를 구현하도록 합니다. 그리고 나서 해당 객체에 pong_ball이라는 이름을 준뒤에 그 이름을 ball변수에 할당함으로써 PongBall의 객체를 ball에 넣습니다.
종합해서 정리를 해드리자면, PongBall을 코드에서 위젯으로 생성하고 설정파일에서 그래픽을 추가합니다. 그 뒤에 PongGame위젯에 ball이라는 변수를 하나 생성하고 설정파일에 들어가서 <PongGame>아래에 PongBall을 차일드로 추가한뒤 그 객체에 pong_ball라는 이름을 주어 PongGame위젯에서 선언한 ball에 할당함으로써 초기값으로 ObjectProperty의 객체를 가지던 ball을 PongBall위젯으로 대체하게 한거죠.
여기까지 완성된 코드는 다음과 같습니다.
main.py
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.properties import ObjectProperty
class PongBall(Widget):
pass
class PongGame(Widget):
ball = ObjectProperty(None)
class PongApp(App):
def build(self):
return PongGame()
if __name__ == '__main__':
PongApp().run()
PongBall에 공의 방향과 속도를 저장할 velocity_x와 velocity_y를 NumericProperty로 정의하고, 편의를 위해 이 두개 값을 ReferenceListProperty로 묶어서 velocity라는 변수에 다시 저장합니다. 이렇게 함으로써 추후에 velocity에만 접근하면 velocity_x와 velocity_y를 한번에 가져올수 있거든요.
그리고 PongBall에 move라는 함수를 하나 선언합니다. 이 함수는 호출될때마다 공을 1스텝 움직이도록 하는 함수입니다. 함수가 호출되면 현재 위치 x, y에 이동해야할 위치값 velocity_x, velocity_y을 더해서 다시 현재 위치값으로 할당함으로써 탁구공을 1스텝씩 이동하도록 해줍니다.
1스텝은 velocity_x와 velocity_y에 저장된 만큼이 1스텝입니다.
여기서 주의해야할 점은 pong.kv에서 정의한 PongBall의 pos는 현재위치의 x와 y값을 Vector로 저장하고 있습니다. 그렇기때문에 ReferenceListProperty로 저장된 velocity는 Vector로 변환한 후에 self.pos와 더하기를 할수 있습니다.
이제 어디선가 move 함수를 호출해줘야 공이 움직이겠죠? PongBall의 move함수를 호출해줄 곳은 바로 PongGame입니다. 현재 PongGame위젯에는 ball이라는 변수 하나만 선언되어 있는데요. 그 밑에 update라는 함수를 하나 선언해서 그 안에서 move를 호출해주는 로직을 만들어 보도록 할게요.
class PongGame(Widget):
ball = ObjectProperty(None)
def update(self, dt):
self.ball.move()
if (self.ball.y < 0) or (self.ball.top > self.height):
self.ball.velocity_y *= -1
if (self.ball.x < 0) or (self.ball.right > self.width):
self.ball.velocity_x *= -1
일단 위의 update함수가 호출되면 바로 PongBall의 move함수를 호출합니다. 일전에 PongBall은 PongGame의 ball안에 객체화 해서 넣어두었었지요? 그래서 PongGame은 self.ball.move()로 PongBall의 move를 호출할수 있습니다.
그리고 여기에서 공이 정해진 공간을 벗어나지 않도록 코딩을 해주는데요. 공의 현재 위치의 y좌표가 0이면 화면의 가장 윗쪽인데 그 픽셀보다 더 위로 올라가 있으면 방향을 반대로 틀어야겠죠? 그건 현재 속도에 -1을 곱해주면 같은 속도로 반대쪽으로 가도록 할수 있습니다. 마찬가지로 y가 현재 화면의 높이보다 크거나, x가 0보다 작거나, x가 가로화면 보다 크면 그때마다 -1을 곱해주어 같은 속도에 방향만 반대로 틀도록 합니다.
이제 앱이 처음 실행되고 PongApp위젯이 build될때 1초에 60번씩 PongGame의 update함수를 호출하도록 해줄게요.
from kivy.clock import Clock
...
class PongApp(App):
def build(self):
game = PongGame()
game.serve_ball()
Clock.schedule_interval(game.update, 1.0 / 60.0)
return game
Kivy에서 제공하는 Clock함수를 이용하여 매 1/60초마다 PongGame.update를 호출하도록 스케줄했습니다. 그리고 바로 위에 game.serve_ball()을 호출하도록 했는데요. serve_ball은 처음에 공을 던질때나 게임을 다시 시작할때 호출되는 함수입니다. serve_ball을 호출할때마다 공이 가운데로 와서 다시 시작하는데 이때 진행할 방향과 속도도 여기에서 정해줍니다.
이 함수에서 처음에 출발할 방향과 속도를 결정하는데 초기값을 x는 4, y는 0으로 준 Vector를 랜덤으로 0도에서 360도까지 중에서 하나 정해서 회전시키면 진행할 방향과 속도가 정해집니다.
사실 각 변수 x, y의 수치가 즉 속도이며 그 수치의 차이가 방향을 결정하는 것이지요.
예를 들어 x, y를 4와 0으로 각각 설정한 상태에서 랜덤으로 회전을 시켰는데 0도가 나왔다면 돌아가지 않고 그대로 x의 속도는 4, y의 속도는 0이 될것 입니다. 그러면 움직임은 x로만 진행되고 y는 0이기 때문에 아무리 더해도 0이됩니다. 그 말은 공이 중간에서 시작했다면 가로로 왔다 갔다 하는데 시작하는 x의 값이 양의 값이니까 x의 값이 큰쪽이 진행 방향이 되어 공은 처음에 오른쪽으로 갔다가 벽에 부딪히면 -가 되어 반대편으로 움직이겠지만 y의 값은 계속 0인 상태로 남아 세로로는 움직이지 않게 될것입니다. 만약 랜덤각도가 180도가 나왔다면 x의 값은 -4가 되고 y는 여전히 0인 상태로 남아 마찬가지로 가로 움직임을 하겠지만 시작방향이 x가 적은 숫자인 왼쪽 방향으로 먼저 움직인뒤 벽에 부딪히면 +로 전환되어 오른쪽으로 움직이지만 여전히 세로 움직임은 없겠죠. 그러면 그와 반대로 x, y를 마찬가지로 4와 0으로 설정했는데 랜덤으로 회전를 시켰더니 90도가 나왔다면 이때는 x가 0이 되고 y가 4가 되어 공은 y값이 큰쪽인 위로 먼저 움직이는데 이때 x값은 0이므로 0에 0을 아무리 더해도 여전히 0이니 이때는 가로 움직임은 없고 세로로만 왔다 갔다 할뿐입니다. 그런데 이때 회전하는 각도가 45도가 나왔다면 어떨까요? 이때는 x=3, y=3으로 초기화가 되어 우측 상단으로 x와 y가 동일하게 증가하여 비례그래프 처럼 사선으로 움직이게 됩니다. 그러다가 공이 천장에 부딪히면 y만 -3으로 변경되어 x로는 오른쪽 y로는 아래쪽으로 움직이다가 오른쪽 벽에 부딪히면 이때 x도 마저 -3으로 기호가 바뀌어 좌표그래프의 0쪽을 향하는 방향으로 탁구공이 사선으로 움직이게 됩니다.
여기까지의 전체 코드는 다음과 같습니다.
main.py
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.properties import (
NumericProperty, ReferenceListProperty, ObjectProperty
)
from kivy.vector import Vector
from kivy.clock import Clock
from random import randint
class PongBall(Widget):
velocity_x = NumericProperty(0)
velocity_y = NumericProperty(0)
velocity = ReferenceListProperty(velocity_x, velocity_y)
def move(self):
self.pos = Vector(*self.velocity) + self.pos
class PongGame(Widget):
ball = ObjectProperty(None)
def serve_ball(self):
self.ball.center = self.center
self.ball.velocity = Vector(4, 0).rotate(randint(0, 360))
def update(self, dt):
self.ball.move()
if (self.ball.y < 0) or (self.ball.top > self.height):
self.ball.velocity_y *= -1
if (self.ball.x < 0) or (self.ball.right > self.width):
self.ball.velocity_x *= -1
class PongApp(App):
def build(self):
game = PongGame()
game.serve_ball()
Clock.schedule_interval(game.update, 1.0 / 60.0)
return game
if __name__ == '__main__':
PongApp().run()
PongPaddle위젯에 가로는 25로 얇게 세로는 200으로 긴 사각형을 하나 추가할게요. 그리고 만든 탁구채를 <PongGame>에 추가 합니다. 탁구채의 위치는 왼쪽의 플레이어가 x값을 0을 갖게 하고, 해당 탁구채의 y 중간값은 화면의 중앙에 위치시킵니다. 오른쪽 플레이어는 화면의 가로크기에서 탁구채의 두께를 뺀 만큼으로 오른쪽 끝에 붙이고 마찬가지로 탁구채의 y의 중간값을 중앙에 위치시켜 초기화를 합니다.
이때 플레이어의 id를 player_left, player_right으로 주고, 코드에서 선언한 PongGame의 플레이어 변수 player1, palyer2에 각각 할당을 해줍니다.
여기까지 두명의 플레이어의 탁구채를 생성하고 필요한 위치에 위치하는 것까지 완료 했습니다. 이제 사용자가 이 탁구채를 움직일 수 있어야겠죠? 여기서 집중하여야할 것이 바로 Widget입니다. PongPaddle은 Widget을 확장하여 만든 클래스인데 Kivy에서 제공하는 이 Widget은 on_touch_down, on_touch_move, 또는 on_touch_up과 같은 사용자 입력을 기본적으로 제공합니다. 우리는 사용자가 탁구채를 잡고 옮기는 액션을 캐치해야하므로 PongGame위젯 안에 on_touch_move함수를 재선언합니다. on_touch_move는 물체를 잡고 드래그하는 동안 계속적으로 현재 위치를 읽어주게 되어 있습니다.
def on_touch_move(self, touch):
if touch.x < self.width/3:
self.player1.center_y = touch.y
if touch.x > self.width - self.width/3:
self.player2.center_y = touch.y
이 함수에서 전달받는 인자값은 self와 touch인데, touch는 현재 위치의 x, y값을 가지고 있어 사용자가 어디에서 드래그 하고 있는지 알수 있습니다. 여기에서 우리가 해야할 것은 사용자입력이 왼쪽 플레이어의 진영에서 이루어진것인지 오른쪽 플레이어의 진영에서 이루어진것인지를 알아야할 필요가 있습니다. touch.x의 위치가 화면을 3개로 나눈 1/3영역에서 이루어진 것이라면 왼쪽 플레이어로 간주하고 touch.y의 값을 player1.center_y에 할당하여 왼쪽 탁구채의 세로 중간값을 사용자가 입력한 값으로 대체해줍니다. 반대로 touch.x의 터치가 화면을 3개로 나눈 3/3영역에서 이루어진 것이라면 오른쪽 플레이어로 간주하고 touch.y의 값을 player2.center_y에 할당하여 오른쪽 탁구채의 세로 중간값을 입력한 값으로 대체해줍니다.
여기까지 사용자의 입력을 받고, 탁구채를 사용자가 입력한 대로 움직이도록 했습니다. 그렇다면 이제 움직이는 공을 탁구채로 쳐냈을때 어떻게 해야하는지 생각해봅시다. PongGame이 update될때마다 탁구공이 탁구채와 만났는지 확인을 해줄 필요가 있습니다. PongPaddle위젯에 bounce_ball라는 함수를 선언하여, 탁구채 위젯이 탁구공과 접촉을 했는지 collide_widget함수를 통해 확인을 합니다. 서로 만났다면 collide_widget함수가 True를 반환하여 필요한 처리를 해줄수 있게 됩니다.
일단 탁구채에 맞으면 x의 방향을 반대로 만들어 주어야합니다. 그건 기존 velocity.x의 값에 -1을 곱함으로써 해결이 됩니다. 그리고 탁구채에 한번 맞을 때마다 속도를 10% 올리는 것으로 합니다. 이는 velocity의 x, y값에 1.1을 곱해줌으로써 해결이 됩니다. 그리고 추가로 공이 탁구채의 어느위치에 맞냐에 따라서 각도를 조금씩 다르게 줍니다.
예를 들어 탁구공이 탁구채의 정중앙에 맞는다면 별다른 변화가 없겠지만 탁구공이 탁구채의 윗쪽에 맞을 수록 velocity.y의 수치를 조금더 높여주는 겁니다. velocity.y의 수치를 높여준다는 말은 탁구공의 방향이 조금더 윗쪽을 향하도록 틀어준다는 뜻입니다. 이는 탁구채의 윗쪽을 맞을 수록 더욱 수치가 높아지며, 반대로 탁구채의 아랫쪽을 맞으면 velocity.y의 값을 줄여줍니다. 아래에 맞으면 맞을수록 공을 더욱 아래쪽으로 기울이겠다는 의미입니다. 이때 공이 탁구채에 맞는 위치에 대한 변화는 velocity.x의 값에는 영향을 미치지 않습니다.
이제 PongGame이 update될때마다 위에 정의한 내용을 반영하도록 update함수를 수정합니다.
class PongGame(Widget):
...
def update(self, dt):
self.ball.move()
self.player1.bounce_ball(self.ball)
self.player2.bounce_ball(self.ball)
if (self.ball.y < self.y) or (self.ball.top > self.top):
self.ball.velocity_y *= -1
if self.ball.x < self.x:
self.player2.score += 1
self.serve_ball(vel=(4, 0))
if self.ball.right > self.width:
self.player1.score += 1
self.serve_ball(vel=(-4, 0))
기존에 move함수를 호출하는 부분은 그대로 둡니다. 그 밑에 player1과 player2의 bounce_ball을 호출하여 공이 탁구채에 접촉했는지 우선 확인하고 둘이 닿았다면 bounce_ball에서 방향을 틀고 속도를 올리는 등 필요한 처리를 해주게 됩니다.
공이 천장이나 바닥에 부딪혔을때는 원래 대로 y의 방향만 반대로 틀어 계속 진행하도록 하되, 단 왼쪽 벽이나 오른쪽 벽에 부딪혔을때는 다르게 처리해주어야합니다. 만약 공이 왼쪽 벽에 부딪혔다면, 왼쪽 탁구채에 맞지 않았다는 이야기 입니다. 탁구채에 맞았다면 벽에 닿기전에 공이 방향을 틀었을테니까요. 그런데 탁구공의 위치가 왼쪽벽을 뚫고 들어갔다면 이것은 왼쪽 플레이어가 게임에 진것입니다. 이때는 오른쪽 사용자의 점수를 올려주고 오른쪽 사용자가 먼저 공을 칠수 있도록 공을 오른쪽으로 서브해줍니다. 반대로 공의 오른쪽 끝이 현재 화면의 가로크기보다 더 넘어설때 왼쪽 사용자에게 1점 추가하고 왼쪽 플레이어에게 서브를 해줍니다.
여기까지 전체코드는 다음과 같습니다.
main.py
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.properties import (
NumericProperty, ReferenceListProperty, ObjectProperty
)
from kivy.vector import Vector
from kivy.clock import Clock
from random import randint
class PongPaddle(Widget):
score = NumericProperty(0)
def bounce_ball(self, ball):
if self.collide_widget(ball):
vx, vy = ball.velocity
bounced = Vector(-1 * vx, vy)
vel = bounced * 1.1
offset = (ball.center_y - self.center_y) / (self.height / 2)
ball.velocity = vel.x, vel.y
class PongBall(Widget):
velocity_x = NumericProperty(0)
velocity_y = NumericProperty(0)
velocity = ReferenceListProperty(velocity_x, velocity_y)
def move(self):
self.pos = Vector(*self.velocity) + self.pos
class PongGame(Widget):
ball = ObjectProperty(None)
player1 = ObjectProperty(None)
player2 = ObjectProperty(None)
def serve_ball(self, vel=(4, 0)):
self.ball.center = self.center
self.ball.velocity = Vector(vel).rotate(randint(0, 360))
def update(self, dt):
self.ball.move()
self.player1.bounce_ball(self.ball)
self.player2.bounce_ball(self.ball)
if (self.ball.y < self.y) or (self.ball.top > self.top):
self.ball.velocity_y *= -1
if self.ball.x < self.x:
self.player2.score += 1
self.serve_ball(vel=(4, 0))
if self.ball.right > self.width:
self.player1.score += 1
self.serve_ball(vel=(-4, 0))
def on_touch_move(self, touch):
if touch.x < self.width / 3:
self.player1.center_y = touch.y
if touch.x > self.width - self.width / 3:
self.player2.center_y = touch.y
class PongApp(App):
def build(self):
game = PongGame()
game.serve_ball()
Clock.schedule_interval(game.update, 1.0 / 60.0)
return game
if __name__ == '__main__':
PongApp().run()
안녕하세요. 오늘은 Python에서 제공하는 모듈의 하나인 Kivy를 가지고 모바일 앱을 만들어보도록 할거에요. Kivy로 만든 앱은 데스크탑 컴퓨터, macOS, Linux, BSD Unix, Windows등에서 실행하실수 있구요. iOS운영체제인 iPad나 iPhone에서도 실행가능하고 물론 Android운영체제를 가진 태블릿이나 핸드폰에서도 실행이 가능합니다. 그 밖에 TUIO(Tangible User Interface Objects)를 지원하는 모든 터치베이스 장비에서도 실행이 가능합니다.
Setup terminal and pip
현재버젼인 Kivy 2.3.0은 파이썬 3.7부터 3.12까지 지원합니다. 일단 파이썬 모듈을 관리하는 pip을 최신버젼으로 업그레이드할게요. 동시에 setuptools과 virtualenv도 같이 업그레이드 할거에요.
Kivy개발을 위해서 파이썬에 가상환경을 하나 만들게요. 가상환경을 만드는 이유는요 각 파이썬 모듈들의 버젼이 충돌하지 않게 하기위함인데요. 예를 들면, Kivy가 지원하는 어떤 파이썬 모듈의 버젼이 낮은데 Kivy가 게을러서 위에 버젼을 지원하도록 업그레이드를 안한 상태에서 다른 프로젝트에서 사용되는 어떤 모듈이 Kivy가 사용하는 모듈을 사용하는데 공교롭게도 최신버젼만 지원한다고 하면 Kivy를 돌릴때는 그 모듈을 downgrade했다가 다른 프로젝트 돌릴때는 그걸 또 upgrade했다가 할수 없잖아요. 그래서 Docker같은 가상환경을 만들고 그 환경에서 Kivy프로젝트를 돌리면 다른 환경에서 같은 모듈의 다른 버젼을 설치해서 각 프로젝트별로 모듈의 버젼이 충돌하는 상황을 피할수가 있거든요.
자 그럼 Kivy를 돌리는데 전용으로 사용할 가상환경을 한번 만들어 볼게요. 가상환경의 이름은 kivy_venv라고 할게요.
python -m venv kivy_venv
생성을 했으면 kivy_venv를 활성화 시킵니다. 아래 명령어는 MacOS 기준입니다. 다른 OS는 여기를 참조해주세요.
source kivy_venv/bin/activate
위의 명령어를 실행하면 프롬프트 앞에 (kivy_venv)라는 라벨이 추가됩니다. 현재 kivy_venv라는 가상환경안에서 개발하고 있다는 걸 의미합니다.
참고로 가상환경에서 빠져나오고 싶다면 deactivate 명령어를 실행하면 됩니다.
Install Kivy
kivy_venv가상환경 하에서 Kivy를 설치하도록 합니다. Kivy를 설치하는 가장 손쉬운 방법은 Kivy에서 제공하는 PyPi wheels를 통해서 Kivy와 예제들을 설치하는 방법입니다.
python -m pip install "kivy[base]" kivy_examples
위의 명령어로 Kivy를 설치하면 가장 미니멀한 Kivy가 설치가 되는데요, 혹시 앱에서 audio나 video를 지원하고 싶으시다면 kivy[base,media]나 kivy[full]로 설치를 해주시면 더욱 다양한 모듈을 사용하실수 있으세요.
그밖에 다양한 설치방법이 있는데 소스를 직접 설치하거나, Kivy사이트에서 직접 다운로드 하는 방법이 있습니다.
Development install
Kivy패키지를 수정하고 싶거나 새로운 기능을 제안하고 싶다면 GitHub에서 소스를 다운받아 Pull Request를 요청해주세요.
git clone https://github.com/kivy/kivy.git
그리고 해당 폴더에 들어가서 수정가능한 형태로 변경해주세요
cd kivy
python -m pip install -e ".[dev,full]"
이제 코드를 변경하고 PR요청을 하실수 있으세요. 다만 변경후 로컬에서 테스트 하실때는 아래 명령어를 통해 Kivy패키지를 재컴파일 해주셔한다는거 잊지 마세요.
python setup.py build_ext --inplace
아니면 bash를 사용하시거나 Linux환경이시라면 간단하게 make만 해주셔도 됩니다.
컴파일을 마친상태에서 반드시 pytest를 돌려주세요. 그래야 코드에 문제가 없는지 검증이 되니까요.
pytest kivy/tests
bash나 Linux환경이라면 make test로 대신할수 있습니다. 다른 환경에서 개발하고 계신분은 여기를 참고해주세요.
Checking the demo
이제 Kivy설치가 다 되었습니다. 이제 여러분의 파이썬 코드에서 import kivy를 사용하실수 있습니다. 그보다 간편하게 기존에 함께 다운로드된 데모프로그램을 한번 돌려볼까요? MacOS라면 아래 명령어를 실행하시면 되고 다른 분들은 여기에서 명령어를 찾으실수 있습니다. 아래 명령을 실행하기 전에 git으로 clone한 kivy폴더에 들어와 있는지 확인해주세요
python examples/demo/showcase/main.py
앗! 그런데 제가 데모를 돌려봤는데 pygame이 없어서 에러나네요. 아까 kivy설치할때 kivy[full]로 했었어야했나봐요. 암튼 저는 pygame만 따로 설치를 해주었더니 잘돌아갑니다.
그래도 명색이 첫 강의인데 Hello World는 만들어야겠죠. main.py파일 하나 생성해서 아래의 코드를 입력해주세요.
from kivy.app import App
from kivy.uix.label import Label
class MyApp(App):
def build(self):
return Label(text='Hello World')
if __name__ == '__main__':
MyApp().run()
터미널에서 python main.py으로 실행해주면 다음과 같이 까만화면에 하얀글씨로 Hello World가 쓰여진 모바앱이 만들어집니다.
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Text('We move under cover and we move as one'),
const Text('Through the night, we have one shot to live another day'),
const Text('We cannot let a stray gunshot give us away'),
const Text('We will fight up close, seize the moment and stay in it'),
const Text('It’s either that or meet the business end of a bayonet'),
const Text('The code word is ‘Rochambeau,’ dig me?'),
Text('Rochambeau!', style: DefaultTextStyle.of(context).style.apply(fontSizeFactor: 2.0)),
],
)
문제해결 (Troubleshooting)
세로길이에 제한이 정해져 있지 않을때
Column위젯 안에 하나이상의 Expanded나 Flexible항목이 존재한다고 가정해봅시다. 그런데 이 Column위젯이 최대 높이를 제한하지 않은 또다른 Column이나 ListView위젯 안에 있다면 실행시 Exception에러를 보게 될거에요. 그 이유는 자식들이 flex속성에 0이 아닌 값을 가지지만 세로 크기가 정해지지 않았기 때문입니다.
위에서 기술한 Exception에러가 나는 문제점은 Flexible이나 Expanded가 다른 자식칸들을 배치한 후에 남은 공간을 Flexible이나 Expanded로 감싼 애들끼리 나눠 가져야하는데, 이때 세로길이가 정해져 있지 않으면 남은 공간이 무제한이 되기때문에 무한대의 공간을 나눌수가 없어서 에러가 발생하는 겁니다.
이 문제는 Column이 왜 세로 길이를 정하지 않았는지를 정의하면 해결이 가능합니다. 이제 무슨 말이냐면요;
보통 이 문제가 발생하는 이유는 Column이 다른 Column안에 들어가 있을때 안쪽의 Column을 Expanded나 Flexible로 감싸고 있지 않기때문에 발생하는데요. 기본적으로 Column은 Expanded나 Flexible로 감싸져 있지 않은, 즉 세로 길이가 정해져 있지 않은 자식칸을 레이아웃 할때, 세로 길이를 자유롭게 정할수 있도록 제약을 하지 않습니다. 그 말인 즉슨, 세로길이를 정해놓지 않으면 자식칸 안의 컨텐츠에 따라서 세로 길이를 알맞게 줄이라는 의미입니다. 그래서 세로 길이가 정해져있지 않은 Column안에 또 다른 Column을 넣고 자식칸을 Expanded로 감싸서 나머지 공간을 채우라고 하면 안쪽의 Column은 바깥쪽의 Column에서 세로길이에 대한 정보를 받은게 전혀 없으니까, 나머지 공간이 얼만데? 하고 고개를 갸우뚱하게 되는거죠. 그래서 결론 부터 말씀드리자면 Column 안쪽에 또다른 Column을 넣을때는 서브 Column이 Expanded를 써야하는 경우에는 너에게 할당된 공간은 얼마다 하고 알려주는거죠. 그걸 알려주는 방법이 바로 inner Column을 Expanded나 Flexible로 감싸주는 것입니다.
이 에러가 뜨는 또 다른 이유중 하나는 Column이 ListView나 세로스크롤이 되는 위젯 안에 들어가 있는 경우입니다. 이 경우에 세로길이가 무한대가 되어 에러가 뜨게됩니다(근데 세로 스크롤의 핵심은 세로길이를 무한대로 주어 계속 스크롤 할수 있게 하려는 것입니다). 그러니까 이런 경우에는 항상 확인을 하세요. 안쪽에 들어간 Column의 자식칸에서 Expanded나 Flexible로 감싼게 있는지 말이죠. 그러면 여러분은 고민하게 되실겁니다. 아 그러면 도대체 세로길이를 얼마로 정해줘야 하는거야? 라고 말이죠. 그럴때는 고민하지 말고 안쪽의 Column의 자식칸에서 사용한 Expanded나 Flexible위젯을 지워 버리세요. 그러면 Column이 컨텐츠의 길이에 맞춰서 세로 길이를 자동으로 할당할겁니다.
고정된 Column으로 구성된 화면에서 Column의 세로길이보다 안에 컨텐츠가 더 긴 경우 보여주어야하는 내용이 넘쳐버리는 상황이 생기는데 이때 자리가 부족하면 컨텐츠가 짤려버립니다. 디버깅모드에서 노랑/검정 줄무늬 바는 컨텐츠가 정해진 범위를 넘어서 짤린다고 알려주는 경고입니다. 그리고 그 밑에 메세지는 얼마나 넘쳤는지 부족한 자리를 감지해서 알려줍니다.
이 문제를 해결하기 위해 보통 Column대신 ListView를 사용하여 세로공간이 부족한 경우 스크롤을 하도록 해서 모든 내용을 보여주도록 하는 것입니다.
Layout algorithm
이번 섹션에서는 프레임웤이 어떻게 Column을 화면에 보여주는지 렌더링 알고리즘에 대해서 설명하도록 하겠습니다. 만약, 박스 레이아웃 모델이 궁금하시다면 BoxConstraints를 참고해주세요.
각 자식 칸들을 flex속성에 “null”이나 “0”의 값을 주고 레이아웃을 요청합니다(단, 여기서 Expanded위젯을 사용하지 않았다는 전제하에서 말이죠). flex에 null이나 0을 주고 레이아웃을 요청하면 칸안쪽의 내용을 가늠해서 고정된 세로 크기를 임의로 할당 받게 됩니다. 이때 만약 flex속성에 특정 수치값을 주게 되면 해당 칸에 우선적으로 공간을 할당하고 남은 공간을 다른 칸에 나누어 부여합니다. 이때 만약 crossAxisAlignment이 CrossAxisAlignment.stretch로 설정되어있으면, incoming 최대 넓이를 갖는 타이트한 수평 제약을 사용합니다.
(마찬가지로, Expanded위젯을 사용한 자식칸이 없다는 전제하에) flex속성에 특정값이 주어진 칸에 우선적으로 세로길이를 할당하고, 남은 공간을 flex속성 값에 비례하여 칸을 나눕니다. 예를 들면 flex 속성에 2.0의 값을 가진 자식칸은 1.0의 값을 가진 칸의 두배크기의 세로 공간을 할당 받게 됩니다.
첫번째 스텝에서와 마찬가지로 남은 자식칸들을 같은 수직 제약으로 레이아웃을 해줍니다. 다만 이때, unbounded 수직 제약을 사용하는 것이 아니라 스텝2에서 할당받은 공간을 기반으로 수직제약을 사용합니다. Flexible.fit 속성이 FlexFit.tight인 자식 칸은 타이트한 제약을 받게 됩니다(바로 여기에서 정해진 모든 공간을 가득채우도록 하는것이에요). 그리고 Flexible.fit속성이 FlexFit.loose로 설정된 칸은 널널한 제약을 받게 됩니다. (이때는 할당된 모든공간을 꽉 채우도록 강요받지 않아요)
Column의 넓이는 자식의 넓이중 가장 넓은 것으로 정해집니다. (이것이 바로 언제나 incoming 수평 제약을 충족시키는 조건이죠)
Row 위젯은 위젯트리의 자녀 위젯들을 “하나의 행”안에 나누어 보여주는 수평식 배열구조를 제공합니다.
Row위젯의 자녀로 등록된 위젯 중 하나를 가지고 남은 공간을 꽉 채워서 보여주고 싶으시다면 Expanded 위젯을 사용해보세요.
해당 위젯이 빈 공간을 꽉 채우게 만들수 있습니다.
Row위젯은 한줄에 칸을 나누어 화면을 구성하는 만큼 스크롤이 되게 만들지 않습니다(일적으로 Row위젯의 자식들이 공간에 딱 맞게 구성이 안되고 자식이 너무 많거나 한 이유로 스크롤이 생길만큼 내려온다면 오류로 간주합니다). 만약 공간이 충분하지 않아서 스크롤을 해야하는 상황이라면 ListView를 사용해보세요.
자식 위젯들을 “하나의 열”에 수직으로 나열하고 싶으시면 Column을 사용해보세요. 만약 Row위젯 안에 자식이 단 하나만 존재할 경우에는 Align이나 Center위젯을 사용는 것을 권장드립니다.
아래 예제에서 Row위젯으로 공간을 가로로 3개로 나누어 처음 두 칸에는 텍스트를, 마지막 칸에는 이미지를 넣어보도록하겠습니다.
고정된 행으로 구성된 화면에서 행의 가로 크기보다 안에 내용이 더 넓은 경우 보여주어야하는 내용이 넘쳐버리는 상황이 생기는데 이를 “오버플로”가 되었다고 합니다. 이렇게 되면 남은 공간이 없게 되어서 내용을 보여줄수 없게 되는데 이때 넘친 가장자리에 노란색과 검은색 줄무늬로 경고상자를 그려 이를 알려줍니다. 행 바깥쪽에 공간이 있으면 경고문이 빨간색 글자로 경고상자 옆에 출력됩니다.
이해를 돕기 위해 아래 코드를 보면서 자세히 설명드리겠습니다.
const Row(
children: <Widget>[
FlutterLogo(),
Text("Flutter's hot reload helps you quickly and easily experiment, build UIs, add features, and fix bug faster. Experience sub-second reload times, without losing state, on emulators, simulators, and hardware for iOS and Android."),
Icon(Icons.sentiment_very_satisfied),
],
)
위의 코드를 보시면 Row의 첫번째 칸에는 로고가 들어가고, 두번째 칸에는 긴 문장이 들어가고, 세번째 칸에는 아이콘을 하나 넣도록 코딩을 했습니다. 첫번째 칸에서 호출한 FlutterLogo는 로고의 가로크기가 24픽셀로 보여지도록 만든 함수입니다 . 24픽셀정도는 써도 다음 위젯들을 위한 공간은 충분할 것입니다. 그 다음으로 두번째 칸의 자식을 위해 Row가 적절한 크기를 할당하게 됩니다.
그런데 이 시점에서 Row는 한정된 가로 공간을 고려하지 않고 단지 현재 칸에 들어간 문자열의 길이만 보고 너비를 결정합니다. 그렇게 되면 다음 칸에 보여줄 공간이 부족하게 되어 노랑/검정 경고상자와 빨간색 경고문자를 보여주면서 불평을 하는 것이지요.
이 문제를 해결하는 방법은 두번째 칸의 자식을 Expanded 위젯으로 감싸는 것입니다. 이렇게 하면 Expanded가 Row에게 그 두번째 칸은 다른 애들 다 쓰고 남은 공간을 할당해 주어야한다고 알려주게 됩니다.
const Row(
children: <Widget>[
FlutterLogo(),
Expanded(
child: Text("Flutter's hot reload helps you quickly and easily experiment, build UIs, add features, and fix bug faster. Experience sub-second reload times, without losing state, on emulators, simulators, and hardware for iOS and Android."),
),
Icon(Icons.sentiment_very_satisfied),
],
)
그러면 이제 Row는 첫번째 칸의 크기를 산정하고, 그 다음 세번째 칸의 크기를 산정한 다음 남은 공간을 모두 두번째 칸에 할당하게 되어 두번째 칸은 자신에게 정해진 공간이 한줄에 보여줄수 없음을 깨닫게 되고 여러줄에 나누어 보여줌으로써 좀더 융통성있는 화면 구성이 가능하게 됩니다.
만약 Row안의 자식들의 보여지는 순서를 이렇게 반대로 정렬하고 싶다면 Row의 textDirection속성을 이용하면 됩니다. 자식들을 출력하는 순서는 기본적으로 TextDirection.ltr, 즉 왼쪽에서 오른쪽으로 보여주는데, 이 속성을 아래 코드와 같이 TextDirection.rtl(right to left)로 변경하면 화면의 오른쪽에 첫번째 칸을 먼저 보여주게 되어 정렬이 반대로 됩니다.
const Row(
textDirection: TextDirection.rtl,
children: <Widget>[
FlutterLogo(),
Expanded(
child: Text("Flutter's hot reload helps you quickly and easily experiment, build UIs, add features, and fix bug faster. Experience sub-second reload times, without losing state, on emulators, simulators, and hardware for iOS and Android."),
),
Icon(Icons.sentiment_very_satisfied),
],
)
레이아웃 알고리즘 (Layout algorithm)
이번 섹션에서는 프레임웤이 어떻게 Row를 화면에 보여주는지 렌더링 알고리즘에 대해서 설명하도록 하겠습니다. 만약, 박스 레이아웃 모델이 궁금하시다면 BoxConstraints를 참고해주세요.
Row의 레이아웃을 처리하는 순서는 아래 6개의 단계가 있습니다:
각 자식 칸들을 flex속성에 “null”이나 “0”의 값을 주고 레이아웃을 요청합니다(단, 여기서 Expanded위젯을 사용하지 않았다는 전제하에서 말이죠). flex에 null이나 0을 주고 레이아웃을 요청하면 칸안쪽의 내용을 가늠해서 고정된 가로 크기를 임의로 할당 받게 됩니다. 이때 만약 flex속성에 특정 수치값을 주게 되면 해당 칸에 우선적으로 공간을 할당하고 남은 공간을 다른 칸에 나누어 부여합니다. 이때 unbounded 수평 제약, incoming 수직 제약을 사용하여 레이아웃을 요청하게 되는데, 만약 crossAxisAlignment이 CrossAxisAlignment.stretch로 설정되어있으면, incoming 최대 높이를 갖는 tight 수직 제약을 사용합니다.
(마찬가지로, Expanded위젯을 사용한 자식칸이 없다는 전제하에) flex속성에 특정값이 주어진 칸에 우선적으로 가로크기를 할당하고, 남은 공간을 flex속성 값에 비례하여 칸을 나눕니다. 예를 들면 flex 속성에 2.0의 값을 가진 자식칸은 1.0의 값을 가진 칸의 두배크기의 가로공간을 할당 받게 됩니다.
첫번째 스텝에서와 마찬가지로 남은 자식칸들을 같은 수직 제약으로 레이아웃을 해줍니다. 다만 이때, unbounded 수직 제약을 사용하는 것이 아니라 스텝2에서 할당받은 공간을 기반으로 수평제약을 사용합니다. Flexible.fit 속성이 FlexFit.tight인 자식 칸은 타이트한 제약을 받게 됩니다(바로 여기에서 정해진 모든 공간을 가득채우도록 하는것이에요). 그리고 Flexible.fit속성이 FlexFit.loose로 설정된 칸은 널널한 제약을 받게 됩니다. (이때는 할당된 모든공간을 꽉 채우도록 강요받지 않아요)
Row의 높이는 자식의 높이중 가장 높은 것으로 정해집니다. (이것이 바로 언제나 incoming 수직 제약을 충족시키는 조건이죠)
이번시간부터 가장 많이 사용 되는 5가지 위젯 Text, Row, Column, Stack Container를 하나씩 차례로 살펴볼건데요. Flutter 공식페이지에 있는 매뉴얼을 번역한 자료를 가지고 볼거에요. 매뉴얼은 읽기에 매우 지루합니다. 하지만 일단 이 5가지 위젯에 대한 매뉴얼에 익숙해 지신다면 다른 위젯들을 다룰때 매뉴얼을 찾아보기가 아주 쉬워지실겁니다. 지루하더라도 하나씩 꼼꼼히 보시고 매뉴얼의 패턴을 익혀주시기 바래요.
Text.rich안쪽에 TextSpan을 넣고 그 밑에 text와 style을 정의함으로써 문자열을 꾸미는데요. TextSpan에는 children속성이 있어서 배열로 여러개의 TextSpan을 넣어서 다양하게 스타일링 할수도 있습니다.
상호작용 (Interactivity)
Text위젯을 버튼처럼 누르게 만들고 싶다면 GestureDetector위젯으로 싸서 만들수 있긴 하지만 그닥 추천하는 바는 아닙니다. Text로 만들어진 문자열을 클릭하도록 만들고 싶으시면 TextButton 위젯을 쓰시기를 추천드립니다. TextButton을 사용할수 없는 환경이라면 적어도 InkWell을 사용하시기를 권장드립니다.
선택기능 (Selection)
Text는 기본적으로 선택을 할수 없게 만들어져 있습니다. 그럼에도 불구하고 Text상자를 선택하고자 하실때는 SelectionArea위젯으로 감싸면 이하 서브트리의 위젯들은 선택을 할수 있게 됩니다. 그리고 반대로 해당 서브트리 안에서 특정 위젯들만 선택을 하지 못하게 설정하고 싶을때는 필요한 위젯만 SelectionContainer.disabled로 감싸면 됩니다. 아래 예제를 보면서 더욱 자세히 설명드리도록 하겠습니다. 터미널을 열고 flutter create --sample=widgets.Text.3 mysample 명령어를 실행하시면 Flutter에서 제공하는 예제가 mysample 폴더안에 자동으로 생성됩니다. 생성된 mysample의 main.dart는 다음과 같습니다.
지난 시간에 배웠던 StatelessWidget이 이제 눈에 확들어오시죠? SelectionContainerDisabledExampleApp로 StatelessWidget로 정의하고 runApp에서 호출하도록 구성이 되어있습니다. 코드를 보시면요 build함수에 위젯트리가 머리속에 그려지시나요? MaterialApp이 위젯트리의 최상단에 위치하고 그 밑에 Scaffold가 위치하고 그 밑에 appBar와 body가 나란히 내려오는데 여기서 body를 보시면 Center위젯으로 감싸져있는데 child에 SelectionArea로 감싸줘서 이하 위젯트리들은 선택할수 있도록 만들었습니다. 그래서 Column의 children 중 하나인 Text, “Selectable text”는 선택이 가능하게 되고, 그 밑에 SelectionContainer.disabled로 추가로 감싸진 Text, “Non-selectable text”는 선택이 안되게 됩니다. 그 뒤에 Text도 SelectionContainer.disabled로 감싸주지 않았기 때문에 위젯트리에 입각해서 SelectionArea의 영향을 받아 선택할수 있게 됩니다. 만약 여기서 SelectionArea 바깥에 Text를 하나 넣었다면 Text는 기본적으로 선택이 안되기 때문에 선택할수 없는 문자열이 되겠죠?
SelectableRegion은 SelectionArea와 마찬가지로 그 안에 있는 위젯들을 선택할수 있게 바꿔주는데 SelectionArea가 Material library의 설정의 따른다면 SelectableRegion은 플랫폼에 연계된 설정이기 때문에 코드에서는 SelectableRegion보다는 SelectionArea가 사용이 용이합니다.
여기까지 Flutter공식페이지에서 제공하는 Text class에 대한 매뉴얼이었습니다. 반드시 원문 페이지에 들어가서 다시 한번 살펴보시기를 권장드리고 해당 페이지에서 설명이 충분하지 않은 부분은 링크를 클릭해서 직접 들어가 보시기 바랍니다. 그런 소소한 습관들이 나중에 필요한 정보를 습득하는데 큰 도움이 될거에요. 지루하셨을텐데 잘 따라와 주셔서 감사합니다. 우리는 다음시간에 Row class로 다시 만나요. 감사합니다.
Stateless와 Stateful 위젯을 사용했을때 이점은 실행시 사용자에게 속도개선을 가져다 줄 뿐만 아니라 코딩하는 개발자에게도 개발속도가 개선되는 이점이 있습니다. Stateless나 Stateful없이 아래와 같이 main()에 화면을 구성하는 코딩을 바로 넣었을때는 화면을 변경하고자 할때 전체 코드를 다시 컴파일해야하는 제약이 있습니다. 아래 backgroundColor의 색상을 teal에서 red로 바꾸고 저장을 해보면 다시 컴파일 하기 전까지는 화면에 아무런 변화가 일어나지 않습니다.
처음에 컴파일 할때 한번은 마찬가지로 시간이 걸리지만, appBar의 backgroundColor를 red로 바꾸고 저장을 하면 놀라운 일이 생깁니다. 아래 콘솔을 보시면 불과 0.6초 만에 화면이 갱신되었습니다.
Performing hot reload...
Syncing files to device AOSP on IA Emulator...
Reloaded 1 of 689 libraries in 606ms (compile: 76 ms, reload: 255 ms, reassemble: 227 ms).
이 기능이 바로 Hot Reload입니다. 콘솔 옆에 번개 모양 아이콘을 눌렀을때와 같은 기능인데 Flutter가 코드를 저장할때 자동으로 번개버튼을 누르도록 해서 저장시 바로바로 화면에 적용이 되도록 만들어 주는 것 입니다.
Hot Reset
콘솔에 보면 번개모양 아이콘 옆에 이렇게 생긴 아이콘이 하나 더 있는데 이게 바로 Hot Reset입니다. Hot Reload와 다른점은 Hot Reload는 다른 변수값들은 그대로 두고 변경된 코드만 살짝 바꿔주는 반면에 Hot Reset은 변경된 코드를 적용시킴과 동시에 변수들도 초기화를 시켜준다는 것입니다. 아래 Flutter앱을 새로 만들면 제공되는 카운터 앱을 가지고 테스트를 해보겠습니다.
AppBar의 backgroundColor를 teal로 초기화를 하고 실행을 한뒤 “+”버튼을 눌러서 숫가를 증가시킵니다. 그 뒤에 backgroundColor를 red로 바꾸고 Hot Reload버튼을 누르면 숫자는 그대로 있고 상단바의 배경색만 빨간색으로 바뀝니다. 반면 다시 backgroundColor를 blue로 바꾸고 Hot Reset버튼을 누르면 상단바의 배경색만 파란색으로 바뀌는 것이 아니라 숫자의 값도 “0”으로 초기화가 된것을 보실수 있습니다. Hot Reset은 각종 변수값들도 초기화를 시켜주기 때문에 Hot Reload보다는 시간이 좀 걸리지만 그래도 앱을 종료하고 다시 실행시킬때 보다는 시간이 절약된다는 장점이 있습니다.
코딩할때 Hot Reload와 Hot Reset을 사용해서 더욱 속도감 있게 개발을 하기위해서 StatelessWidget과 StatefulWidget은 반드시 써야하겠습니다. 가장 자주 사용되는 4가지 레이아 Widget에 대해서 배워보도록 하겠습니다. 감사합니다.
오늘은 StatelessWidget과 StatefulWidget에 대해서 배워보도록 하겠습니다.
시작에 앞서 일단 새로운 Flutter앱을 하나 만들게요. Android Studio에서 새로운 Flutter앱을 만드는 방법은 여기를 참고해주세요. 저는 my_first_app이라는 이름으로 앱을 하나 생성했습니다. Flutter는 앱을 새로 만들면 자동으로 데모 앱을 생성해주는데, 이 데모 앱은 화면에 보이는 “+”버튼을 누르면 숫자가 하나씩 더해지는 간단한 앱입니다.
main.dart에 제공된 코드를 보시면요. 우리가 처음에 만들었던 Hello World랑은 다르게 조금 복잡한 모양을 하고 있는데요, 이 구조가 바로 Flutter가 추구하는 이상적인 구조의 코드입니다. 이렇게 만들라고 샘플 코드를 넣어준거니까 어떻게 생겼는지 함께 찬찬히 살펴보고 앞으로 코드를 짤때는 이 구조에 입각해서 코드를 짜도록 하겠습니다.
코드를 줄여놓고 굵직한 것들만 보면 왼쪽의 그림과 같이 main()과 그 밑에 StatelessWidget이 하나 있고 그 밑에 StatefulWidget그리고 마지막으로 State클래스가 있습니다.
이렇게 코드를 4개로 크게 나눈 이유는 main()은 Flutter앱을 실행하면 자동으로 호출하는 함수니까 거기에는 다른 코드는 안넣고 바로 StatelessWidget을 호출하는 걸로 하고, StatelessWidget에서 StatefulWidget을 호출하는데 이렇게 나누어 놓은 이유는 다음과 같습니다.
StatelessWidget에는 변하지 않는 것을 코딩하고, StatefulWidget에는 변하는 것을 코딩하도록 구성했기 때문입니다.
예를 들면, 앱의 이름은 사용자가 앱을 이용하는 동안에 절대 바뀌지 않죠? 그러면 그건 StatelessWidget에 정의를 하고, 버튼을 눌렀을때 화면에 보여지는 카운트는 자꾸 변하니까 StatefulWidget에 정의합니다. 그러면 앱의 바탕색은 어디에 놓을까요? 그건 여러분 마음입니다. 앱의 바탕색을 절대 불변으로 하고 싶으시면 StatelessWidget에 정의를 하시면 되고 중간에 변화를 주고 싶으시면 StatefulWidget에 정의하셔야합니다. 이렇게 두개의 위젯을 구분해놓은 이유는 바로 Performance!! 운영시 효율성때문입니다. 고정된 애들은 구석에 박아 놓고, 갱신될 가능성이 있는 애들은 가까운 곳에 두고 따로 관리하여 바로바로 갱신이 이루어 지도록 하기 위해서 구분을 해놓는 것입니다.
그럼 이제 main.dart 코드의 가장 위에서 부터 함께 차근히 살펴 보도록 하겠습니다.
material.dart라이브러리를 import하고 main()함수를 선언하는 부분까지는 이전시간에 배운것과 똑같습니다. 다만 여기서는 main()함수에서 바로 코드를 작성하는 것이 아니라 MyApp()이라는 함수를 따로 선언해서 runApp()에 인자로 넣어줌으로써 main()이 실행되면 자동적으로 MyApp()으로 가도록 설계를 했네요. 그럼 MyApp()을 한번 따라가 볼까요?
class MyApp extends StatelessWidget { StatelessWidget을 확장하여 MyApp()을 정의한 부분입니다. 편의상 Flutter에서 제공한 주석은 삭제했습니다.
const MyApp({super.key}); 클래스 안에 가장 윗쪽에 자기자신을 호출한건 바로 생성자입니다. 생성자는 다른 언어와 마찬가지로 Dart에서도 클래스가 객체화 될때 가장 먼저 딱 한번 실행됩니다. 이때 부모클래스에 정의된 super.key를 함께 호출하는데요. 이 key는요 Widget이 만들어 질때마다 자동으로 생성되는 key인데요. runApp()에 인자로 들어가는 Widget이 위젯 트리의 root가 됩니다. 그리고 이하 다른 위젯들이 그 밑에 트리구조로 붙어서 트리안의 모든 위젯들은 key값을 통해 관리가 되고 key를 통해 각 위젯들을 제어할수도 있습니다.
Widget build(BuildContext context) { 그 다음 함수가 여기서 젤 중요한데요, 일단 함수 위에 @override를 명시함으로써 기존에 부모 클래스에 정의된 build()메서드를 여기서 재정의 하겠다고 알려줍니다. 여기서 정의된 build 위젯은요. StatelessWidget이 실행되면 자동으로 호출되는 메서드입니다. 이 build()에서 반환한 값을 위에서 호출한 runApp()에 전달하기 때문에 얘가 가장 중요한 함수입니다.
return MaterialApp( 지난 시간까지는 main()에서 MaterialApp()을 바로 호출했는데 이번 시간부터는 StatelessWidget의 build()에서 MaterialApp을 호출하도록합니다.
title: 'Flutter Demo', MaterialApp()의 인자로는 title, theme, 그리고 home이 있는데 그중 title은 이 앱의 이름을 설정하는 부분입니다. theme: ThemeData(colorScheme: ..,useMaterial3: true,) 여기서 앱의 테마를 정하고 각종 변하지 않는 각종 설정들을 추가해줍니다. home: const MyHomePage(title: 'Flutter Demo Home Page'), 그리고 가장 중요한 속성인 home에 실제 화면에 들어가는 레이아웃을 만들어 주는 함수를 호출합니다. 그럼 MyHomePage가 어떻게 정의 되었는지 아래 코드를 함께 보겠습니다.
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
MyHomePage는 StatefulWidget으로 정의된 클래스입니다. 다시 말해 이 클래스에 정의된 내용들은 앱이 실행된 이후에 각종 설정값들이 변경될 가능성이 있다는 의미입니다.
const MyHomePage({super.key, required this.title}); 그리고 이전 클래스와 마찬가지로 key를 가지고 생성자를 호출하는데요. 이때 MyHomePage호출시 인자로 넘겨받은 title도 함께 호출합니다.
State<MyHomePage> createState() => _MyHomePageState(); @override를 명시하여 StatefulWidget에 정의된 createState()을 재정의 하겠다고 명시합니다. 그리고 createState()은 아래 별도로 정의된 _MyHomePageState()클래스를 호출합니다.
int _counter = 0; _MyHomePageState클래스의 내부변수로 _counter를 선언하고 0으로 초기화 합니다.
void _incrementCounter() { 그 밑에 선언된 함수는 호출될때마다 _counter의 값을 하나씩 증가시킵니다.
Widget build(BuildContext context) { State클래스를 호출하면 자동으로 실행되는 build함수는 마찬가지로 코드를 실행하여 생성된 화면 레이아웃을 StatefulWidget에 전달합니다.
return Scaffold(appBar: .., body: .., floatingActionButton: ..) Scaffold를 이용해서 화면을 구성합니다. Scaffold의 appBar속성은 앱의 최상단에 보여지는 해당 페이지의 대표제목입니다. backgroundColor와 title을 정의하여 Flutter Demo Home Page라는 타이틀을 화면에 보여줍니다.
body: Center(child: Column(..)) body로는 일단 Center()를 호출하여 가운데 정렬을 하도록 하고, child에는 Column을 넣어 리스트 형태로 컨텐츠가 들어가게 합니다. Column의 children속성에 위젯 배열을 선언하여 보여주고자 하는 메세지 Text()와 카운트번호를 담을 Text()를 선언해줍니다. 첫번째 Text()는 고정된 글씨이므로 const를 넣어 선언하고, 카운터는 text 값에 '$_counter'를 넣어 값이 변하는 대로 화면에도 갱신되도록 해줍니다.
floatingActionButton: FloatingActionButton(..) 마지막 라인은 이전시간에 설명했으므로 생략하도록 하겠습니다.
위에서 설명드린대로 데모앱을 실행을 하면 StatelessWidget에 정의된것은 구석에, StatefulWidget에 정의된것은 근처에 둠으로써 운영상의 효율을 극대화 합니다. 눈에 보이지는 않지만 이렇게 고정된 레이아웃과 갱신이 되는 레이아웃을 나눠 놓음으로써 갱신시 속도가 매우 빠르고 메모리 효율도 좋은 앱을 만들수 있습니다.
오늘 강의를 통해서 StatelessWidget과 StatefulWidget의 차이를 확실히 아셨으리라 생각됩니다. 다음 시간에는 Stateless와 Stateful 위젯을 사용했을때 코딩속도가 빨라지는 Hot Reload와 Hot Reset에 대해서 공부해보도록 하겠습니다.