The Python Tutorial #3 – Interpreter

Using the Python Interpreter

Invoking the Interpreter

파이썬 인터프리터는 보통 /usr/local/bin/python3.12에 설치가 됩니다. 유닉스 쉘에서 /usr/local/bin폴더 아래에 설치된 파일들은 아래와 같이 간단한 명령어로 실행할 수 있게 됩니다.

python3.12

파이썬 인터프리터가 설치되는 경로는 설치할때 옵션으로 바꿀수가 있어요. 혹시라도 파이썬이 다른곳에 설치되어있을수도 있으니 그때는 파이썬 Guru나 관리자에게 설치경로를 물어보세요. 사람들이 주로 사용하는 또 다른 경로에는 /usr/local/python도 있으니 참고하세요.

윈도우의 경우에는 Microsoft Store에서 파이썬을 설치하는데 설치후 바로 python3.12명령어를 바로 사용할 수 있습니다. 만약에 py.exe launcher를 추가로 설치하셨다면 py명령어로도 파이썬을 실행할 수 있습니다. 또 다른 방법으로 파이썬을 실행하고 싶으시면 Excursus: Setting environment variables를 참고해주세요.

파이썬 인터프리터를 종료하고 싶으시면 파일을 끝으로 가는 단축키 (유닉스에서는 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를 넘겨주면 됩니다.

파이썬 호출 명령어의 전체 옵션들은 Command line and environment에서 확인해주시기 바랍니다.

Argument Passing

스크립트와 추가적인 옵션들은 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!

더 자세한 Interactive(대화형) 모드를 알고 싶으시면, Interactive Mode를 참고해 주세요.

The Interpreter and Its Environment

Source Code Encoding

기본적으로 파이썬의 소스코드들은 UTF-8로 캐릭터셋이 설정이 되었다는 가정하에 파일들을 처리합니다. 해당 엔코딩에서 제공하는 캐릭터들은 대부분의 언어에서 사용되는 문자열이고 구분자이며, 주석입니다. 비록 스탠다드 라이브러리에서는 오직 구분자등을 위해서 ASCII 캐릭터만 사용하지만 말이죠. 이런 다양한 문자들을 제대로 보여주기 위해서는, 코딩하는 개발자가 파일이 UTF-8인지 왜 그래야하는지 등을 정확하게 알고 있어야합니다. 또한 파일안의 모든 문자들을 지원하는 폰트를 반드시 사용해야합니다.

기본적으로 설정된 엔코딩 이외의 엔코딩을 정의하려면 파일의 첫번째 줄에 특별히 약속된 문구를 넣어줘야합니다.

# -*- coding: encoding -*-

위의 명령어에서 encoding부분은 반드시 실제 시스템들이 지원하는 코덱이어야합니다.

예를 들면, 해당 소스파일은 윈도우-1252 엔코딩이 사용될 것이라고 정의하고 싶다면 코드의 가장 첫번째 줄에 아래처럼 명시를 해주어야합니다.

# -*- coding: cp1252 -*-

첫번째 줄에 엔코딩이 들어가야한다는 규칙에는 단 하나의 예외가 존재합니다. 바로 소스코드가 UNIX “shebang” line일때는 첫번째 줄에 엔코딩을 넣지 않아도 됩니다. 그 경우에는 엔코딩은 아래와 같이 두번째 줄에 들어갑니다.

#!/usr/bin/env python3
# -*- coding: cp1252 -*-

References

The Python Tutorial #2 – Appetite

Whetting Your Appetite

만약 여러분이 컴퓨터로 일을 많이 하시는 분이시라면, 자주 반복되는 일들을 자동으로 처리하게 하고 싶다는 생각을 한번쯤은 해보셨을 겁니다. 예를 들면, 엄청나게 많은 파일이 있는데, 그 안에서 어떤 특정 단어를 찾아서 다른 단어로 바꿔야 한다던가, 아니면 엄청나게 많은 이미지파일들이 있는데, 각 이미지파일들의 이름을 변경하여 정리를 하게 한다던가 하는 일 말입니다. 그걸 일일이 수동으로 하는 것 보다는 필요한 데이타베이스를 생성하고, 간단한 화면에 연동하여 반복되는 작업들을 한번의 클릭으로 간편하게 처리할수 있도록 프로그램으로 만들고 싶으실겁니다.

여러분중에 전문 소프트웨어 개발자가 계시다면, 아마도 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에 관한 이야기를 정식 매뉴얼에서 언급해도 되는건지 상사에게 허락은 받았냐고요? ㅎㅎ 오히려 꼭 좀 언급 해달라고 부탁을 받을 정도였습니다.

이제 파이썬에 대해 뭔가 기대가 좀 되지 않으시나요? 더 깊이 있게 배워보고 싶으실거에요. 언어를 배우는데 가장 좋은 방법은 바로 써보는 것입니다. 앞으로 소개할 튜토리얼에서는 여러분을 파이썬 인터프리터의 세계로 안내해드릴거에요.

다음 챕터에서는 인터프리터를 사용하는 장비들에 대해서 설명드릴거에요. 이건 그냥 평범한 설명이라 중요하지는 않지만 그 뒤에 나오는 예제는 정말 중요한거니까 반드시 실행해 보시기를 권장드릴게요.

남은 튜토리얼은 파이썬 언어와 체계에 관한 매우 다양한 기능들을 소개하고 있으니까요, 예제나 간단한 표현들, 각종 문법들과 데이타 타입, 그리고 함수와 모듈, 그리고 좀더 심도높은 개념의 예외처리와 사용자정의 클래스등 다양한 것들 다룰 예정입니다.

References

The Python Tutorial #1

The Python Tutorial

파이썬은 배우기 쉽고, 매우 강력한 프로그램 언어입니다. 파이썬은 매우 효율적인 데이타구조를 지니며, 간단 명료한 객체지향형 프로그램을 지원합니다. 파이썬의 수준높은 문법 그리고 역동적인 데이타타입등은 자연스럽고, 미래지향적인 언어임을 입증하고 있으며, 이는 다양한 분야, 각종 플랫폼에서 신속한 개발을 가능하게 합니다.

파이썬 인터프리터와 광범위한 기본제공 라이브러리는 소스코드에서 자유롭게 접근할수 있고, 대부분의 플랫폼에서 쓸수 있는 바이너리형태로도 제공됩니다(소스나 바이너리 코드는 https://www.python.org에서 다운받으세요). 다운 받은 코드는 자유롭게 배포하셔도 됩니다. 사이트에 가보시면 여러 분야에서 종사하고 계시는 개발자 분들이 올려주신 다양한 파이썬 모듈, 프로그램, 유용한 툴, 그리고 각종 문서들도 함께 보실수 있습니다.

파이썬 인터프리터는 매우 쉽게 C나 C++로 개발된 함수나 데이타 타입과 연동이 가능합니다. 또한 파이썬은 기존에 구현된 응용 프로그램에 확장된 형태로 추가하여 개발할 수도 있습니다.

본 튜토리얼은 기본적인 파이썬의 컨셉과 기능들을 소개할것이며, 또한 각종 경험을 해보실수 있는 예제들을 제공하여 오프라인 환경에서도 튜토리얼을 습득할수 있습니다.

기본 객체와 모듈들에 관해서는 The Python Standard Library와 The Python Language Reference를 참고하시면 파이썬 언어의 더욱 자세한 원리들 까지도 이해할수 있습니다. C나 C++에 확장패키지로 파이썬을 사용하고 싶으시다면, Extending and Embedding the Python Interpreter 와 Python/C API Reference Manual를 참고해주세요. 그밖에 시중에 나온 많은 좋은 책들을 참고하시는 것도 파이썬을 공부하는 좋은 방법이 될수 있습니다.

본 튜토리얼은 파이썬의 전반적인 모든 기능들을 커버하지는 않습니다. 여기에서는 주로 사용되는 기능들을 알려주기 보다는 파이썬의 두드러진 특징들을 소개하도록 할것입니다. 이로 인해 파이썬이라는 언어가 어떤 스타일, 어떤 취향의 언어인지를 이해하는데 도움을 드리도록 할것입니다. 이 튜토리얼을 마친뒤 여러분은 파이썬 모듈과 프로그램을 읽고 쓸줄 알게 될것이며, 파이썬 기본라이브러리에 있는 다양한 모듈들을 익힐 준비가 되어있으실겁니다.

파이썬 어휘사전 Glossary를 한번 훑어보시는 것도 훌륭한 참고가 되실거에요.

References

Python Kivy – Pong Game Tutorial

Simple Graphic

이번시간에는 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()함수를 호출 하도록 해준거에요. 그리고 PongAppApp클래스로 정의했는데 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이 이 앱의 실제 이름인데 소문자로 파일명을 만들어 주면 해당 앱이 로딩될때 자동으로 해당 파일도 로딩이 되어 별도의 설정이나 코딩없이 바로 그래픽이 적용이 됩니다.

main.py과 같은 폴더에 생성해 준 pong.kv라는 파일에 아래 코드를 입력합니다.

#:kivy 1.0.9

<PongGame>:    
    canvas:
        Rectangle:
            pos: self.center_x - 5, 0
            size: 10, self.height
            
    Label:
        font_size: 70  
        center_x: root.width / 4
        top: root.top - 50
        text: "0"
        
    Label:
        font_size: 70  
        center_x: root.width * 3 / 4
        top: root.top - 50
        text: "0"

pong.kv파일의 가장 첫번째 줄을 보면 #:kivy 1.0.9라고 주석이 달려있습니다. 이는 Kivy버젼을 명시하는 것인데, 버젼에 따라 설정파일의 형식이 달라지므로 kv파일의 가장 첫 줄에는 무조건 #:kivy로 시작되는 버젼명을 명시하도록 하는것이 필수사항입니다.

그 밑으로 <PongGame>이라고 되어있는데 kv파일에서 <>로 감싸여 있는 것은 바로 소속 위젯을 뜻합니다. 즉슨, kv파일의 <PongGame>에 나열된 그래픽이 main.pyPongGame이라는 위젯의 객체로 소속이 된다는 의미입니다.

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.pyPongBall이라는 위젯을 만들어서 그 안에 그래픽을 넣어야합니다. main.py에 새로운 위젯클래스 PongBall을 하나 추가합니다. 아직 안에 들어갈 내용은 없으니 그냥 pass만 넣을게요. 다른 위젯에서 불러쓸 위젯이니까 제일 위에 정의하는게 좋겠죠? 참고로 마찬가지로 Widget을 확장해서 선언했습니다.

class PongBall(Widget):
    pass

그리고 그 위젯 안에 그래픽을 넣을거에요. pong.kv를 열어서 PongBall에 들어갈 그래픽을 정의합니다.

<PongBall>:
    size: 50, 50 
    canvas:
        Ellipse:
            pos: self.pos
            size: self.size     

kv파일의 <PongBall>은 코드의 PongBall위젯과 같은 의미입니다. 그래서 kv에서 정의된 그래픽들은 해당 위젯에 포함이 됩니다. PongBall에서 사용할 그래픽의 크기는 가로세로 50픽셀입니다. 그리고 그 안에 canvas를 정의하여 Ellipse로 동그라미를 그려줍니다. 그리고 동그라미의 위치는 현재 PongBall의 위치로, 그리고 동그라미의 크기는 현재 PongBall의 크기로 설정해줍니다. Widget에는 각종 기본 속성 들이 들어가있는데 self.pos를 따로 선언해준 적이 없는데 Ellipse.posself.pos를 할당해준것도 self.pos가 기본 속성으로 제공되기 때문입니다.

그러면 이제 만든 탁구공을 게임에 추가시켜볼게요. 기존의 PongGame위젯 에서 공에 쉽게 접근할수 있도록 ball이라는 클래스변수를 하나 만들고 거기에다가 PongBall을 구현해서 객체로 저장해서 사용하도록 할텐데요. 일단 코드에서는 해당 변수에 ObjectProperty를 할당한뒤 나중에 pong.kv에서 PongBall를 할당할거에요.

PongGameball을 선언해서 임시로 ObjectProperty객체를 할당합니다.

from kivy.properties import ObjectProperty

class PongGame(Widget):
    ball = ObjectProperty(None)

이제 임시로 넣어준 객체를 실제 공으로 바꿀 차례인데요. 그 액션은 pong.kv설정파일에서 합니다.

pong.kv파일을 열어서 가장 먼저 <PongGame>아래에 PongBall라는 속성을 추가해서 해당 객체를 구현하도록 합니다. 그리고 나서 해당 객체에 pong_ball이라는 이름을 준뒤에 그 이름을 ball변수에 할당함으로써 PongBall의 객체를 ball에 넣습니다.

<PongGame>:
    ball: pong_ball
...
    PongBall:
        id: pong_ball
        center: self.parent.center

종합해서 정리를 해드리자면, PongBall을 코드에서 위젯으로 생성하고 설정파일에서 그래픽을 추가합니다. 그 뒤에 PongGame위젯에 ball이라는 변수를 하나 생성하고 설정파일에 들어가서 <PongGame>아래에 PongBall을 차일드로 추가한뒤 그 객체에 pong_ball라는 이름을 주어 PongGame위젯에서 선언한 ball에 할당함으로써 초기값으로 ObjectProperty의 객체를 가지던 ballPongBall위젯으로 대체하게 한거죠.

여기까지 완성된 코드는 다음과 같습니다.

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()

pong.kv

#:kivy 1.0.9

<PongBall>:
    size: 50, 50 
    canvas:
        Ellipse:
            pos: self.pos
            size: self.size     

<PongGame>:
    ball: pong_ball
    
    canvas:
        Rectangle:
            pos: self.center_x - 5, 0
            size: 10, self.height
    
    Label:
        font_size: 70  
        center_x: root.width / 4
        top: root.top - 50
        text: "0"
        
    Label:
        font_size: 70  
        center_x: root.width * 3 / 4
        top: root.top - 50
        text: "0"
    
    PongBall:
        id: pong_ball
        center: self.parent.center

자 그러면 여기까지 한번 실행을 해볼까요?

코드에 입력한 대로 정 가운데 동그란 공이 하나 생겼습니다.

Adding Ball Animation

공이 움직이려면 공이 어느 방향으로 움직여야할지를 먼저 알아야합니다. 그 정보를 PongBallvelocity_xvelocity_y에 각각 저장하여, 다음 스텝에서 x로는 얼마를 가고, y로는 얼마를 갈지를 참고하도록 합니다.

  • 현재 위치의 xy에 각각 더해줄 velocity_xvelocity_y의 픽셀값이 바로 진행할 방향과 속도가 됩니다.
  • velocity_xvelocity_y의 절대값이 크면 클수록 탁구공의 속도가 빨라지는 것이고,
  • velocity_xvelocity_y의 값이 차이가 많이 나면 날수록 비례 곡선에서 벗어나는 방향이 되는 것입니다
  • velocity_x의 절대값이 클수록 가로 움직임이 커지고, velocity_y의 절대값이 클수록 세로 움직임이 커집니다.

현재 공의 위치는 pong.kv<PongBall>아래에 pos으로 저장되어 있으니까 공의 방향을 저장할 변수를 아래와 같이 정의합니다.

from kivy.properties import (
    NumericProperty, ReferenceListProperty, ObjectProperty
)
from kivy.vector import Vector

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

PongBall에 공의 방향과 속도를 저장할 velocity_xvelocity_yNumericProperty로 정의하고, 편의를 위해 이 두개 값을 ReferenceListProperty로 묶어서 velocity라는 변수에 다시 저장합니다. 이렇게 함으로써 추후에 velocity에만 접근하면 velocity_xvelocity_y를 한번에 가져올수 있거든요.

그리고 PongBallmove라는 함수를 하나 선언합니다. 이 함수는 호출될때마다 공을 1스텝 움직이도록 하는 함수입니다. 함수가 호출되면 현재 위치 x, y에 이동해야할 위치값 velocity_x, velocity_y을 더해서 다시 현재 위치값으로 할당함으로써 탁구공을 1스텝씩 이동하도록 해줍니다.

1스텝은 velocity_xvelocity_y에 저장된 만큼이 1스텝입니다.

여기서 주의해야할 점은 pong.kv에서 정의한 PongBallpos는 현재위치의 xy값을 Vector로 저장하고 있습니다. 그렇기때문에 ReferenceListProperty로 저장된 velocityVector로 변환한 후에 self.pos와 더하기를 할수 있습니다.

이제 어디선가 move 함수를 호출해줘야 공이 움직이겠죠? PongBallmove함수를 호출해줄 곳은 바로 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함수가 호출되면 바로 PongBallmove함수를 호출합니다. 일전에 PongBallPongGameball안에 객체화 해서 넣어두었었지요? 그래서 PongGameself.ball.move()PongBallmove를 호출할수 있습니다.

그리고 여기에서 공이 정해진 공간을 벗어나지 않도록 코딩을 해주는데요. 공의 현재 위치의 y좌표가 0이면 화면의 가장 윗쪽인데 그 픽셀보다 더 위로 올라가 있으면 방향을 반대로 틀어야겠죠? 그건 현재 속도에 -1을 곱해주면 같은 속도로 반대쪽으로 가도록 할수 있습니다. 마찬가지로 y가 현재 화면의 높이보다 크거나, x0보다 작거나, x가 가로화면 보다 크면 그때마다 -1을 곱해주어 같은 속도에 방향만 반대로 틀도록 합니다.

이제 앱이 처음 실행되고 PongApp위젯이 build될때 1초에 60번씩 PongGameupdate함수를 호출하도록 해줄게요.

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을 호출할때마다 공이 가운데로 와서 다시 시작하는데 이때 진행할 방향과 속도도 여기에서 정해줍니다.

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))

이 함수에서 처음에 출발할 방향과 속도를 결정하는데 초기값을 x4, y0으로 준 Vector를 랜덤으로 0도에서 360도까지 중에서 하나 정해서 회전시키면 진행할 방향과 속도가 정해집니다.

사실 각 변수 x, y의 수치가 즉 속도이며 그 수치의 차이가 방향을 결정하는 것이지요.

예를 들어 x, y40으로 각각 설정한 상태에서 랜덤으로 회전을 시켰는데 0도가 나왔다면 돌아가지 않고 그대로 x의 속도는 4, y의 속도는 0이 될것 입니다. 그러면 움직임은 x로만 진행되고 y0이기 때문에 아무리 더해도 0이됩니다. 그 말은 공이 중간에서 시작했다면 가로로 왔다 갔다 하는데 시작하는 x의 값이 양의 값이니까 x의 값이 큰쪽이 진행 방향이 되어 공은 처음에 오른쪽으로 갔다가 벽에 부딪히면 -가 되어 반대편으로 움직이겠지만 y의 값은 계속 0인 상태로 남아 세로로는 움직이지 않게 될것입니다. 만약 랜덤각도가 180도가 나왔다면 x의 값은 -4가 되고 y는 여전히 0인 상태로 남아 마찬가지로 가로 움직임을 하겠지만 시작방향이 x가 적은 숫자인 왼쪽 방향으로 먼저 움직인뒤 벽에 부딪히면 +로 전환되어 오른쪽으로 움직이지만 여전히 세로 움직임은 없겠죠. 그러면 그와 반대로 x, y를 마찬가지로 40으로 설정했는데 랜덤으로 회전를 시켰더니 90도가 나왔다면 이때는 x0이 되고 y4가 되어 공은 y값이 큰쪽인 위로 먼저 움직이는데 이때 x값은 0이므로 00을 아무리 더해도 여전히 0이니 이때는 가로 움직임은 없고 세로로만 왔다 갔다 할뿐입니다. 그런데 이때 회전하는 각도가 45도가 나왔다면 어떨까요? 이때는 x=3, y=3으로 초기화가 되어 우측 상단으로 xy가 동일하게 증가하여 비례그래프 처럼 사선으로 움직이게 됩니다. 그러다가 공이 천장에 부딪히면 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()

pong.kv

#:kivy 1.0.9

<PongBall>:
    size: 50, 50 
    canvas:
        Ellipse:
            pos: self.pos
            size: self.size          

<PongGame>:
    ball: pong_ball
    
    canvas:
        Rectangle:
            pos: self.center_x - 5, 0
            size: 10, self.height
    
    Label:
        font_size: 70  
        center_x: root.width / 4
        top: root.top - 50
        text: "0"
        
    Label:
        font_size: 70  
        center_x: root.width * 3 / 4
        top: root.top - 50
        text: "0"
    
    PongBall:
        id: pong_ball
        center: self.parent.center

여기까지의 코드를 실행해보면 탁구공이 혼자 이리저리 돌아다니면서 벽에 부딪히면 방향을 바꿔 가는 것을 보실수 있으실거에요.

Connect Input Events

이제 이 공을 사용자의 입력을 받아 움직이게 할 차례입니다. 사용자가 내리칠 탁구채 만드는거랑 이겼을때 점수판에 점수만 올라가면 될거 같아요. 우선 탁구채를 만들게요. 마찬가지로 Kivy에서 제공하는 Widget을 기본으로 클래스를 선언합니다.

class PongPaddle(Widget):
    score = NumericProperty(0)

class PongGame(Widget):
    ball = ObjectProperty(None)
    player1 = ObjectProperty(None)
    player2 = ObjectProperty(None)

PongPaddle이라는 위젯을 만들고 탁구채를 사용자라고 생각하고 점수를 그 안에다 저장하도록 할게요. 그리고 해당 플레이어들을 저장할 변수 player1, palyer2PongGame에 선언해주었습니다.

그리고 pong.kv에서 탁구채 모양을 만들어 볼게요.

<PongPaddle>:
    size: 25, 200
    canvas:
        Rectangle:
            pos: self.pos
            size: self.size
...
<PongGame>:
    ball: pong_ball
    player1: player_left
    player2: player_right
...
    PongPaddle:
        id: player_left
        x: root.x
        center_y: root.center_y
        
    PongPaddle:
        id: player_right
        x: root.width - self.width
        center_y: root.center_y

PongPaddle위젯에 가로는 25로 얇게 세로는 200으로 긴 사각형을 하나 추가할게요. 그리고 만든 탁구채를 <PongGame>에 추가 합니다. 탁구채의 위치는 왼쪽의 플레이어가 x값을 0을 갖게 하고, 해당 탁구채의 y 중간값은 화면의 중앙에 위치시킵니다. 오른쪽 플레이어는 화면의 가로크기에서 탁구채의 두께를 뺀 만큼으로 오른쪽 끝에 붙이고 마찬가지로 탁구채의 y의 중간값을 중앙에 위치시켜 초기화를 합니다.

이때 플레이어의 idplayer_left, player_right으로 주고, 코드에서 선언한 PongGame의 플레이어 변수 player1, palyer2에 각각 할당을 해줍니다.

여기까지 두명의 플레이어의 탁구채를 생성하고 필요한 위치에 위치하는 것까지 완료 했습니다. 이제 사용자가 이 탁구채를 움직일 수 있어야겠죠? 여기서 집중하여야할 것이 바로 Widget입니다. PongPaddleWidget을 확장하여 만든 클래스인데 Kivy에서 제공하는 이 Widgeton_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

이 함수에서 전달받는 인자값은 selftouch인데, touch는 현재 위치의 x, y값을 가지고 있어 사용자가 어디에서 드래그 하고 있는지 알수 있습니다. 여기에서 우리가 해야할 것은 사용자입력이 왼쪽 플레이어의 진영에서 이루어진것인지 오른쪽 플레이어의 진영에서 이루어진것인지를 알아야할 필요가 있습니다. touch.x의 위치가 화면을 3개로 나눈 1/3영역에서 이루어진 것이라면 왼쪽 플레이어로 간주하고 touch.y의 값을 player1.center_y에 할당하여 왼쪽 탁구채의 세로 중간값을 사용자가 입력한 값으로 대체해줍니다. 반대로 touch.x의 터치가 화면을 3개로 나눈 3/3영역에서 이루어진 것이라면 오른쪽 플레이어로 간주하고 touch.y의 값을 player2.center_y에 할당하여 오른쪽 탁구채의 세로 중간값을 입력한 값으로 대체해줍니다.

여기까지 사용자의 입력을 받고, 탁구채를 사용자가 입력한 대로 움직이도록 했습니다. 그렇다면 이제 움직이는 공을 탁구채로 쳐냈을때 어떻게 해야하는지 생각해봅시다. PongGameupdate될때마다 탁구공이 탁구채와 만났는지 확인을 해줄 필요가 있습니다. PongPaddle위젯에 bounce_ball라는 함수를 선언하여, 탁구채 위젯이 탁구공과 접촉을 했는지 collide_widget함수를 통해 확인을 합니다. 서로 만났다면 collide_widget함수가 True를 반환하여 필요한 처리를 해줄수 있게 됩니다.

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 + offset

일단 탁구채에 맞으면 x의 방향을 반대로 만들어 주어야합니다. 그건 기존 velocity.x의 값에 -1을 곱함으로써 해결이 됩니다. 그리고 탁구채에 한번 맞을 때마다 속도를 10% 올리는 것으로 합니다. 이는 velocityx, y값에 1.1을 곱해줌으로써 해결이 됩니다. 그리고 추가로 공이 탁구채의 어느위치에 맞냐에 따라서 각도를 조금씩 다르게 줍니다.

예를 들어 탁구공이 탁구채의 정중앙에 맞는다면 별다른 변화가 없겠지만 탁구공이 탁구채의 윗쪽에 맞을 수록 velocity.y의 수치를 조금더 높여주는 겁니다. velocity.y의 수치를 높여준다는 말은 탁구공의 방향이 조금더 윗쪽을 향하도록 틀어준다는 뜻입니다. 이는 탁구채의 윗쪽을 맞을 수록 더욱 수치가 높아지며, 반대로 탁구채의 아랫쪽을 맞으면 velocity.y의 값을 줄여줍니다. 아래에 맞으면 맞을수록 공을 더욱 아래쪽으로 기울이겠다는 의미입니다. 이때 공이 탁구채에 맞는 위치에 대한 변화는 velocity.x의 값에는 영향을 미치지 않습니다.

이제 PongGameupdate될때마다 위에 정의한 내용을 반영하도록 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함수를 호출하는 부분은 그대로 둡니다. 그 밑에 player1player2bounce_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()

pong.kv

#:kivy 1.0.9

<PongBall>:
    size: 50, 50 
    canvas:
        Ellipse:
            pos: self.pos
            size: self.size          

<PongPaddle>:
    size: 25, 200
    canvas:
        Rectangle:
            pos: self.pos
            size: self.size

<PongGame>:
    ball: pong_ball
    player1: player_left
    player2: player_right
    
    canvas:
        Rectangle:
            pos: self.center_x - 5, 0
            size: 10, self.height
    
    Label:
        font_size: 70  
        center_x: root.width / 4
        top: root.top - 50
        text: str(root.player1.score)
        
    Label:
        font_size: 70  
        center_x: root.width * 3 / 4
        top: root.top - 50
        text: str(root.player2.score)
    
    PongBall:
        id: pong_ball
        center: self.parent.center
        
    PongPaddle:
        id: player_left
        x: root.x
        center_y: root.center_y
        
    PongPaddle:
        id: player_right
        x: root.width - self.width
        center_y: root.center_y

실행을 해보면 탁구채를 잡아서 공을 쳐낼수 있고 공을 못쳤을때 상대편 점수가 올라갑니다.

여기까지 따라오시느라 수고 많으셨습니다. 이상 Python 모듈 Kivy를 이용한 모바일앱 만들기 탁구게임편이었습니다. 시청해주셔서 감사합니다.

References

Let’s Create a Mobile Apps With Python Kivy

Overview

안녕하세요. 오늘은 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을 최신버젼으로 업그레이드할게요. 동시에 setuptoolsvirtualenv도 같이 업그레이드 할거에요.

python -m pip install --upgrade pip setuptools virtualenv

Create virtual environment

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]로 설치를 해주시면 더욱 다양한 모듈을 사용하실수 있으세요.

python -m pip install "kivy[base,media]" kivy_examples

그밖에 다양한 설치방법이 있는데 소스를 직접 설치하거나, 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만 따로 설치를 해주었더니 잘돌아갑니다.

python -m pip install pygame
python examples/demo/showcase/main.py

살짝 괴상하긴 하지만 데모앱이 실행이 잘 되었네요.

Hello World

그래도 명색이 첫 강의인데 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가 쓰여진 모바앱이 만들어집니다.

References

Column class

이하 본문은 Flutter공식페이지 매뉴얼의 Column class를 번역하여 작성한 블로그입니다.
원문: https://api.flutter.dev/flutter/widgets/Column-class.html

Overview

Column 위젯은 위젯트리의 자녀 위젯들을 “하나의 열” 안에 나누어 보여주는 수직의 배열구조를 제공합니다.

특정 자녀로 주어진 공간을 가득 채우고자 할때는 그 자식 위젯을 Expanded위젯으로 감싸면 할당된 공간의 남은 영역을 해당 위젯이 점유하게 됩니다.

Column 위젯은 스크롤이 안생기게 만듭니다(일반적으로 Column 안의 내용물에 스크롤이 생겼다면 오류로 간주합니다). 만약에 자식위젯들을 화면에 보이는 것보다 많이 두어 스크롤 하도록 만들고 싶다면 ListView 위젯을 사용할 것을 추천드립니다.

컨텐츠를 가로로 나누어 보여주고 싶다면 Row 위젯을 사용하세요.

만약 Column안에 자식이 오직 하나인 경우는 굳이 Column을 사용하지 마시고 Align 이나 Center를 사용할것을 권장합니다.

아래 예제는 Column을 사용하여 세로로 컨텐츠를 나열하고 마지막 항목이 남은 공간을 꽉 채웁니다.

const Column(
  children: <Widget>[
    Text('Deliver features faster'),
    Text('Craft beautiful UIs'),
    Expanded(
      child: FittedBox(
        child: FlutterLogo(),
      ),
    ),
  ],
)

위의 예제를 보면 각 행의 문자열과 로고가 가운데 정렬이 되어있습니다.

다음 예제에서 는 crossAxisAlignment 가 CrossAxisAlignment.start로 설정하여 자식칸 들을 왼쪽 정렬을 합니다. 그리고 mainAxisSize 속성은 MainAxisSize.min로 설정하여 칼럼의 세로 크기가 컨텐츠에 딱 맞게 줄어듭니다.

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위젯 안에 하나이상의 ExpandedFlexible항목이 존재한다고 가정해봅시다. 그런데 이 Column위젯이 최대 높이를 제한하지 않은 또다른 Column이나 ListView위젯 안에 있다면 실행시 Exception에러를 보게 될거에요. 그 이유는 자식들이 flex속성에 0이 아닌 값을 가지지만 세로 크기가 정해지지 않았기 때문입니다.

위에서 기술한 Exception에러가 나는 문제점은 Flexible이나 Expanded가 다른 자식칸들을 배치한 후에 남은 공간을 Flexible이나 Expanded로 감싼 애들끼리 나눠 가져야하는데, 이때 세로길이가 정해져 있지 않으면 남은 공간이 무제한이 되기때문에 무한대의 공간을 나눌수가 없어서 에러가 발생하는 겁니다.

이 문제는 Column이 왜 세로 길이를 정하지 않았는지를 정의하면 해결이 가능합니다. 이제 무슨 말이냐면요;

보통 이 문제가 발생하는 이유는 Column이 다른 Column안에 들어가 있을때 안쪽의 ColumnExpandedFlexible로 감싸고 있지 않기때문에 발생하는데요. 기본적으로 ColumnExpandedFlexible로 감싸져 있지 않은, 즉 세로 길이가 정해져 있지 않은 자식칸을 레이아웃 할때, 세로 길이를 자유롭게 정할수 있도록 제약을 하지 않습니다. 그 말인 즉슨, 세로길이를 정해놓지 않으면 자식칸 안의 컨텐츠에 따라서 세로 길이를 알맞게 줄이라는 의미입니다. 그래서 세로 길이가 정해져있지 않은 Column안에 또 다른 Column을 넣고 자식칸을 Expanded로 감싸서 나머지 공간을 채우라고 하면 안쪽의 Column은 바깥쪽의 Column에서 세로길이에 대한 정보를 받은게 전혀 없으니까, 나머지 공간이 얼만데? 하고 고개를 갸우뚱하게 되는거죠. 그래서 결론 부터 말씀드리자면 Column 안쪽에 또다른 Column을 넣을때는 서브 ColumnExpanded를 써야하는 경우에는 너에게 할당된 공간은 얼마다 하고 알려주는거죠. 그걸 알려주는 방법이 바로 inner ColumnExpandedFlexible로 감싸주는 것입니다.

이 에러가 뜨는 또 다른 이유중 하나는 Column이 ListView나 세로스크롤이 되는 위젯 안에 들어가 있는 경우입니다. 이 경우에 세로길이가 무한대가 되어 에러가 뜨게됩니다(근데 세로 스크롤의 핵심은 세로길이를 무한대로 주어 계속 스크롤 할수 있게 하려는 것입니다). 그러니까 이런 경우에는 항상 확인을 하세요. 안쪽에 들어간 Column의 자식칸에서 ExpandedFlexible로 감싼게 있는지 말이죠. 그러면 여러분은 고민하게 되실겁니다. 아 그러면 도대체 세로길이를 얼마로 정해줘야 하는거야? 라고 말이죠. 그럴때는 고민하지 말고 안쪽의 Column의 자식칸에서 사용한 ExpandedFlexible위젯을 지워 버리세요. 그러면 Column이 컨텐츠의 길이에 맞춰서 세로 길이를 자동으로 할당할겁니다.

제약에 대해 좀더 자세하게 알고 싶으시면 BoxConstraints를 참조하세요.

노란색과 검은색 줄무늬의 배너가 있는 경우

고정된 Column으로 구성된 화면에서 Column의 세로길이보다 안에 컨텐츠가 더 긴 경우 보여주어야하는 내용이 넘쳐버리는 상황이 생기는데 이때 자리가 부족하면 컨텐츠가 짤려버립니다. 디버깅모드에서 노랑/검정 줄무늬 바는 컨텐츠가 정해진 범위를 넘어서 짤린다고 알려주는 경고입니다. 그리고 그 밑에 메세지는 얼마나 넘쳤는지 부족한 자리를 감지해서 알려줍니다.

이 문제를 해결하기 위해 보통 Column대신 ListView를 사용하여 세로공간이 부족한 경우 스크롤을 하도록 해서 모든 내용을 보여주도록 하는 것입니다.

Layout algorithm

이번 섹션에서는 프레임웤이 어떻게 Column을 화면에 보여주는지 렌더링 알고리즘에 대해서 설명하도록 하겠습니다. 만약, 박스 레이아웃 모델이 궁금하시다면 BoxConstraints를 참고해주세요.

Column의 레이아웃을 처리하는 순서는 아래 6개의 단계가 있습니다:

  1. 각 자식 칸들을 flex속성에 “null”이나 “0”의 값을 주고 레이아웃을 요청합니다(단, 여기서 Expanded위젯을 사용하지 않았다는 전제하에서 말이죠). flex에 null이나 0을 주고 레이아웃을 요청하면 칸안쪽의 내용을 가늠해서 고정된 세로 크기를 임의로 할당 받게 됩니다. 이때 만약 flex속성에 특정 수치값을 주게 되면 해당 칸에 우선적으로 공간을 할당하고 남은 공간을 다른 칸에 나누어 부여합니다. 이때 만약 crossAxisAlignmentCrossAxisAlignment.stretch로 설정되어있으면, incoming 최대 넓이를 갖는 타이트한 수평 제약을 사용합니다.
  2. (마찬가지로, Expanded위젯을 사용한 자식칸이 없다는 전제하에) flex속성에 특정값이 주어진 칸에 우선적으로 세로길이를 할당하고, 남은 공간을 flex속성 값에 비례하여 칸을 나눕니다. 예를 들면 flex 속성에 2.0의 값을 가진 자식칸은 1.0의 값을 가진 칸의 두배크기의 세로 공간을 할당 받게 됩니다.
  3. 첫번째 스텝에서와 마찬가지로 남은 자식칸들을 같은 수직 제약으로 레이아웃을 해줍니다. 다만 이때, unbounded 수직 제약을 사용하는 것이 아니라 스텝2에서 할당받은 공간을 기반으로 수직제약을 사용합니다. Flexible.fit 속성이 FlexFit.tight인 자식 칸은 타이트한 제약을 받게 됩니다(바로 여기에서 정해진 모든 공간을 가득채우도록 하는것이에요). 그리고 Flexible.fit속성이 FlexFit.loose로 설정된 칸은 널널한 제약을 받게 됩니다. (이때는 할당된 모든공간을 꽉 채우도록 강요받지 않아요)
  4. Column의 넓이는 자식의 넓이중 가장 넓은 것으로 정해집니다. (이것이 바로 언제나 incoming 수평 제약을 충족시키는 조건이죠)
  5. Column의 길이는 mainAxisSize속성에 의해 결정됩니다. mainAxisSize 속성이 MainAxisSize.max인 경우 Column의 길이는 incoming 제약 조건의 최대 길이입니다. mainAxisSize 속성이 MainAxisSize.min이면 Column의 길이는 자식 길이의 합계입니다(incoming 제약 조건에 따라 다름).
  6. mainAxisAlignmentcrossAxisAlignment의 설정값에 따라 각 자식칸의 시작점을 ​​결정합니다. 예를 들어, mainAxisAlignmentMainAxisAlignment.spaceBetween인 경우 자식칸에 할당되지 않은 남은 공간은 균등하게 나누어 자식 칸들 사이에 공백으로 채워집니다.

그 밖에도:

  • Row, 같은 기능을 하되 수평으로 자식들을 나열함
  • Flex, 자식들을 수직으로 나열할지 수평으로 나열할지 결정되지 않은 경우에 사용.
  • Expanded, Expanded로 감싸인 자식칸이 남은 모든 공간을 차지하도록 함.
  • Flexible, 남은 공간을 함께 써야하는 자식칸들을 지정하는 데 사용되며 크기가 더 작아질수 있음 (나머지 공간 사용하지 않은채 남겨두기)
  • SingleChildScrollView,  Column을 스크롤이 가능한 컨테이너 안에 넣고 사용하는 방법
  • Spacer, flex 값에 비례하여 공간을 차지하는 위젯입니다.
  • 레이아웃 위젯 모음집

상속 계보 (Inheritance)

Object > DiagnosticableTree > Widget > RenderObjectWidget > MultiChildRenderObjectWidget > Flex > Column

생성자 (Constructors)

Column({Key? key, MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start, MainAxisSize mainAxisSize = MainAxisSize.max, CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center, TextDirection? textDirection, VerticalDirection verticalDirection = VerticalDirection.down, TextBaseline? textBaseline, List<Widget> children = const <Widget>[]})Creates a vertical array of children.
const

속성 (Properties)

children → List<Widget> 위젯트리에서 현재 위젯의 하위 위젯들을 나열하는 곳 (바로 여기에 자식칸들을 배열로 나열합니다.)
final,inherited

clipBehavior → Clip 이 옵션에 따라서 콘텐츠가 잘리거나 잘리지 않습니다.
final,inherited

crossAxisAlignment → CrossAxisAlignment 교차 축을 따라 자식칸들을 배치하는 방법을 기술합니다.
final,inherited

direction → Axis 주축으로 사용할 방향입니다.
final,inherited

hashCode → int 이 개체의 해시코드입니다.
read-only,inherited

key → Key? 하나의 위젯이 트리의 다른 위젯을 대체하는 방법을 제어합니다.
final,inherited

mainAxisAlignment → MainAxisAlignment 주축을 따라 하위 요소를 배치하는 방법입니다.
final,inherited

mainAxisSize → MainAxisSize 주축에서 얼마나 많은 공간을 차지해야할지 기술하는 부분입니다.
final,inherited

runtimeType → Type 객체의 런타임 유형을 나타냅니다.
read-only,inherited

textBaseline → TextBaseline? 기준선에 따라 항목을 정렬하는 경우 사용할 기준선입니다.
final,inherited

textDirection → TextDirection? 자식칸들을 가로로 배치하는 순서와 가로 방향의 시작과 끝을 해석하는 방법을 결정합니다.
final,inherited

verticalDirection → VerticalDirection자식칸들을 세로로 배치하는 순서와 세로 방향의 시작과 끝을 해석하는 방법을 결정합니다.
final,inherited

클래스 내부함수 (Methods)

createElement() → MultiChildRenderObjectElement RenderObjectWidgets은 항상 RenderObjectElement의 하위클래스로 확장됩니다.
inherited

createRenderObject(BuildContext context) → RenderFlexRenderObjectWidget에 설명된 구성을 사용하여 이 RenderObjectWidget이 나타내는 RenderObject 클래스의 인스턴스를 생성합니다.
inherited

debugDescribeChildren() → List<DiagnosticsNode> 이 노드의 하위 항목을 설명하는 DiagnosticsNode 객체 목록을 반환합니다.
inherited

debugFillProperties(DiagnosticPropertiesBuilder properties) → void 노드와 관련된 추가 속성을 추가합니다.
inherited

didUnmountRenderObject(covariant RenderObject renderObject) → void 이전에 이 위젯과 연관된 렌더링 객체가 트리에서 제거되었습니다. 지정된 RenderObject는 이 개체의 createRenderObject에서 반환된 것과 동일한 유형입니다.
inherited

getEffectiveTextDirection(BuildContext context) → TextDirection? RenderFlex.textDirection에 전달할 값입니다.
inherited

noSuchMethod(Invocation invocation) → dynamic 존재하지 않는 메서드나 속성에 액세스할 때 호출됩니다.
inherited

toDiagnosticsNode({String? name, DiagnosticsTreeStyle? style}) → DiagnosticsNode 디버깅 도구 및 DiagnosticsNode.toStringDeep에서 사용되는 개체의 디버그 표현을 반환합니다.
inherited

toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) → String 이 개체의 문자열 표현입니다.
inherited

toStringDeep({String prefixLineOne = ”, String? prefixOtherLines, DiagnosticLevel minLevel = DiagnosticLevel.debug}) → String 이 노드와 그 하위 항목의 문자열 표현을 반환합니다.
inherited

toStringShallow({String joiner = ‘, ‘, DiagnosticLevel minLevel = DiagnosticLevel.debug}) → String 객체에 대한 한 줄의 자세한 설명을 반환합니다.
inherited

toStringShort() → String 이 위젯에 대한 간단한 텍스트 설명입니다.
inherited

updateRenderObject(BuildContext context, covariant RenderFlex renderObject) → voidRenderObjectWidget에 의해 설명된 구성을 지정된 RenderObject에 복사합니다. 이는 이 객체의 createRenderObject에서 반환된 것과 동일한 유형입니다.
inherited

연산자 (Operators)

operator ==(Object other) → bool 같음 연산자입니다.
inherited

Row class

이하 본문은 Flutter공식페이지 매뉴얼의 Row class를 번역하여 작성한 블로그입니다.
원문: https://api.flutter.dev/flutter/widgets/Row-class.html

Overview

Row 위젯은 위젯트리의 자녀 위젯들을 “하나의 행”안에 나누어 보여주는 수평식 배열구조를 제공합니다.

Row위젯의 자녀로 등록된 위젯 중 하나를 가지고 남은 공간을 꽉 채워서 보여주고 싶으시다면 Expanded 위젯을 사용해보세요.

해당 위젯이 빈 공간을 꽉 채우게 만들수 있습니다.

Row위젯은 한줄에 칸을 나누어 화면을 구성하는 만큼 스크롤이 되게 만들지 않습니다(일적으로 Row위젯의 자식들이 공간에 딱 맞게 구성이 안되고 자식이 너무 많거나 한 이유로 스크롤이 생길만큼 내려온다면 오류로 간주합니다). 만약 공간이 충분하지 않아서 스크롤을 해야하는 상황이라면 ListView를 사용해보세요.

자식 위젯들을 “하나의 열”에 수직으로 나열하고 싶으시면 Column을 사용해보세요.
만약 Row위젯 안에 자식이 단 하나만 존재할 경우에는 Align이나 Center위젯을 사용는 것을 권장드립니다.

아래 예제에서 Row위젯으로 공간을 가로로 3개로 나누어 처음 두 칸에는 텍스트를, 마지막 칸에는 이미지를 넣어보도록하겠습니다.

const Row(
  children: <Widget>[
    Expanded(
      child: Text('Deliver features faster', textAlign: TextAlign.center),
    ),
    Expanded(
      child: Text('Craft beautiful UIs', textAlign: TextAlign.center),
    ),
    Expanded(
      child: FittedBox(
        child: FlutterLogo(),
      ),
    ),
  ],
)

문제해결 (Troubleshooting)

행에 노란색과 검은색 경고줄무늬가 있는 경우

고정된 행으로 구성된 화면에서 행의 가로 크기보다 안에 내용이 더 넓은 경우 보여주어야하는 내용이 넘쳐버리는 상황이 생기는데 이를 “오버플로”가 되었다고 합니다. 이렇게 되면 남은 공간이 없게 되어서 내용을 보여줄수 없게 되는데 이때 넘친 가장자리에 노란색과 검은색 줄무늬로 경고상자를 그려 이를 알려줍니다. 행 바깥쪽에 공간이 있으면 경고문이 빨간색 글자로 경고상자 옆에 출력됩니다.

이해를 돕기 위해 아래 코드를 보면서 자세히 설명드리겠습니다.

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개의 단계가 있습니다:

  1. 각 자식 칸들을 flex속성에 “null”이나 “0”의 값을 주고 레이아웃을 요청합니다(단, 여기서 Expanded위젯을 사용하지 않았다는 전제하에서 말이죠). flex에 null이나 0을 주고 레이아웃을 요청하면 칸안쪽의 내용을 가늠해서 고정된 가로 크기를 임의로 할당 받게 됩니다. 이때 만약 flex속성에 특정 수치값을 주게 되면 해당 칸에 우선적으로 공간을 할당하고 남은 공간을 다른 칸에 나누어 부여합니다. 이때 unbounded 수평 제약, incoming 수직 제약을 사용하여 레이아웃을 요청하게 되는데, 만약 crossAxisAlignmentCrossAxisAlignment.stretch로 설정되어있으면, incoming 최대 높이를 갖는 tight 수직 제약을 사용합니다.
  2. (마찬가지로, Expanded위젯을 사용한 자식칸이 없다는 전제하에) flex속성에 특정값이 주어진 칸에 우선적으로 가로크기를 할당하고, 남은 공간을 flex속성 값에 비례하여 칸을 나눕니다. 예를 들면 flex 속성에 2.0의 값을 가진 자식칸은 1.0의 값을 가진 칸의 두배크기의 가로공간을 할당 받게 됩니다.
  3. 첫번째 스텝에서와 마찬가지로 남은 자식칸들을 같은 수직 제약으로 레이아웃을 해줍니다. 다만 이때, unbounded 수직 제약을 사용하는 것이 아니라 스텝2에서 할당받은 공간을 기반으로 수평제약을 사용합니다. Flexible.fit 속성이 FlexFit.tight인 자식 칸은 타이트한 제약을 받게 됩니다(바로 여기에서 정해진 모든 공간을 가득채우도록 하는것이에요). 그리고 Flexible.fit속성이 FlexFit.loose로 설정된 칸은 널널한 제약을 받게 됩니다. (이때는 할당된 모든공간을 꽉 채우도록 강요받지 않아요)
  4. Row의 높이는 자식의 높이중 가장 높은 것으로 정해집니다. (이것이 바로 언제나 incoming 수직 제약을 충족시키는 조건이죠)
  5. Row의 넓이는 mainAxisSize속성에 의해 결정됩니다. mainAxisSize 속성이 MainAxisSize.max인 경우 Row의 너비는 incoming 제약 조건의 최대 너비입니다. mainAxisSize 속성이 MainAxisSize.min이면 Row의 너비는 자식 너비의 합계입니다(incoming 제약 조건에 따라 다름).
  6. mainAxisAlignmentcrossAxisAlignment에 따라 각 자식칸의 위치를 ​​결정합니다. 예를 들어, mainAxisAlignment가 MainAxisAlignment.spaceBetween인 경우 자식칸에 할당되지 않은 남은 공간은 균등하게 나누어 자식 칸들 사이에 공백으로 채워집니다.

그 밖에도:

  • Column, 같은 기능을 하되 수직으로 자식들을 나열함.
  • Flex, 자식들을 수직으로 나열할지 수평으로 나열할지 결정되지 않은 경우에 사용.
  • Expanded, Expanded로 감싸인 자식칸이 남은 모든 공간을 차지하도록 함.
  • Flexible, 남은 공간을 함께 써야하는 자식칸들을 지정하는 데 사용되며 크기가 더 작아질수 있음 (나머지 공간 사용하지 않은채 남겨두기)
  • Spacer, flex 값에 비례하여 공간을 차지하는 위젯입니다.
  • 레이아웃 위젯 모음집

상속 계보 (Inheritance)

Object > DiagnosticableTree > Widget > RenderObjectWidget > MultiChildRenderObjectWidget > Flex > Row

생성자 (Constructors)

Row({Key? key, MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start, MainAxisSize mainAxisSize = MainAxisSize.max, CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center, TextDirection? textDirection, VerticalDirection verticalDirection = VerticalDirection.down, TextBaseline? textBaseline, List<Widget> children = const <Widget>[]})Creates a horizontal array of children.
const

속성 (Properties)

children → List<Widget> 바로 여기에 자식칸들을 배열로 나열합니다.
final,inherited

clipBehavior → Clip 이 옵션에 따라서 콘텐츠가 잘리거나 잘리지 않습니다.
final,inherited

crossAxisAlignment → CrossAxisAlignment 교차 축을 따라 자식칸들을 배치하는 방법을 기술합니다.
final,inherited

direction → Axis 주축으로 사용할 방향입니다.
final,inherited

hashCode → int 이 개체의 해시코드입니다.
read-only,inherited

key → Key? 하나의 위젯이 트리의 다른 위젯을 대체하는 방법을 제어합니다.
final,inherited

mainAxisAlignment → MainAxisAlignment 주축을 따라 하위 요소를 배치하는 방법입니다.
final,inherited

mainAxisSize → MainAxisSize 주축에서 얼마나 많은 공간을 차지해야할지 기술하는 부분입니다.
final,inherited

runtimeType → Type 객체의 런타임 유형을 나타냅니다.
read-only,inherited

textBaseline → TextBaseline? 기준선에 따라 항목을 정렬하는 경우 사용할 기준선입니다.
final,inherited

textDirection → TextDirection? 자식칸들을 가로로 배치하는 순서와 가로 방향의 시작과 끝을 해석하는 방법을 결정합니다.
final,inherited

verticalDirection → VerticalDirection 자식칸들을 세로로 배치하는 순서와 세로 방향의 시작과 끝을 해석하는 방법을 결정합니다.
final,inherited

클래스 내부함수 (Methods)

createElement() → MultiChildRenderObjectElement RenderObjectWidgets은 항상 RenderObjectElement의 하위클래스로 확장됩니다.
inherited

createRenderObject(BuildContext context) → RenderFlexRenderObjectWidget에 설명된 구성을 사용하여 이 RenderObjectWidget이 나타내는 RenderObject 클래스의 인스턴스를 생성합니다.
inherited

debugDescribeChildren() → List<DiagnosticsNode> 이 노드의 하위 항목을 설명하는 DiagnosticsNode 객체 목록을 반환합니다.
inherited

debugFillProperties(DiagnosticPropertiesBuilder properties) → void 노드와 관련된 추가 속성을 추가합니다.
inherited

didUnmountRenderObject(covariant RenderObject renderObject) → void 이전에 이 위젯과 연관된 렌더링 객체가 트리에서 제거되었습니다. 지정된 RenderObject는 이 개체의 createRenderObject에서 반환된 것과 동일한 유형입니다.
inherited

getEffectiveTextDirection(BuildContext context) → TextDirection? RenderFlex.textDirection에 전달할 값입니다.
inherited

noSuchMethod(Invocation invocation) → dynamic 존재하지 않는 메서드나 속성에 액세스할 때 호출됩니다.
inherited

toDiagnosticsNode({String? name, DiagnosticsTreeStyle? style}) → DiagnosticsNode 디버깅 도구 및 DiagnosticsNode.toStringDeep에서 사용되는 개체의 디버그 표현을 반환합니다.
inherited

toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) → String 이 개체의 문자열 표현입니다.
inherited

toStringDeep({String prefixLineOne = ”, String? prefixOtherLines, DiagnosticLevel minLevel = DiagnosticLevel.debug}) → String 이 노드와 그 하위 항목의 문자열 표현을 반환합니다.
inherited

toStringShallow({String joiner = ‘, ‘, DiagnosticLevel minLevel = DiagnosticLevel.debug}) → String 객체에 대한 한 줄의 자세한 설명을 반환합니다.
inherited

toStringShort() → String 이 위젯에 대한 간단한 텍스트 설명입니다.
inherited

updateRenderObject(BuildContext context, covariant RenderFlex renderObject) → voidRenderObjectWidget에 의해 설명된 구성을 지정된 RenderObject에 복사합니다. 이는 이 객체의 createRenderObject에서 반환된 것과 동일한 유형입니다.
inherited

연산자 (Operators)

operator ==(Object other) → bool 같음 연산자입니다.
inherited

Text class

이번시간부터 가장 많이 사용 되는 5가지 위젯 Text, Row, Column, Stack Container를 하나씩 차례로 살펴볼건데요. Flutter 공식페이지에 있는 매뉴얼을 번역한 자료를 가지고 볼거에요. 매뉴얼은 읽기에 매우 지루합니다. 하지만 일단 이 5가지 위젯에 대한 매뉴얼에 익숙해 지신다면 다른 위젯들을 다룰때 매뉴얼을 찾아보기가 아주 쉬워지실겁니다. 지루하더라도 하나씩 꼼꼼히 보시고 매뉴얼의 패턴을 익혀주시기 바래요.

이하 본문은 Flutter의 매뉴얼 Text class를 번역하여 작성한 블로그입니다:
원문: https://api.flutter.dev/flutter/widgets/Text-class.html

Text 위젯은 하나의 문자열을 표현하기 위해 사용되고 해당 문자열에 스타일도 가미할수 있습니다. style속성은 반드시 기재해야하는 항목은 아니기 때문에 생략했을때는 가장 근접하게 설정된 DefaultTextStyle을 선택해서 스타일로 적용합니다.

맛보기 코드 #1

아래 코드는 문자열이 정해진 구역을 넘어갈때 어떻게 처리할지를 보여줍니다.

Text(
  'Hello Ruth, How are you?',
  textAlign: TextAlign.center,
  overflow: TextOverflow.ellipsis,
  style: const TextStyle(fontWeight: FontWeight.bold),
)

Text.overflow속성이 TextOverflow.ellipsis로 설정되어 있기때문에 문자열이 길어지면 뒷쪽 구간은 생략기호(…)으로 표시가 됩니다.

맛보기 코드 #2

Text.rich()를 이용하면 하나의 Text안에서 더욱 다양한 스타일의 문자표현을 할수 있습니다. 아래 코드는 Text.rich() 아래 TextSpan을 넣어서 다양한 스타일을 구성한 문자열을 보여줍니다.

const Text.rich(
  TextSpan(
    text: 'Hello', // default text style
    children: <TextSpan>[
      TextSpan(text: ' beautiful ', style: TextStyle(fontStyle: FontStyle.italic)),
      TextSpan(text: 'world', style: TextStyle(fontWeight: FontWeight.bold)),
    ],
  ),
)

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는 다음과 같습니다.

import 'package:flutter/material.dart';

void main() => runApp(const SelectionContainerDisabledExampleApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('SelectionContainer.disabled Sample')),
        body: const Center(
          child: SelectionArea(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Text('Selectable text'),
                SelectionContainer.disabled(child: Text('Non-selectable text')),
                Text('Selectable text'),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

지난 시간에 배웠던 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는 기본적으로 선택이 안되기 때문에 선택할수 없는 문자열이 되겠죠?

그 밖에도:

  • RichText로 Text보다 더 현란한 스타일을 표현할수 있고요
  • DefaultTextStyle로는 Text위젯의 기본 스타일을 정의 할수 있습니다.
  • SelectableRegion은 SelectionArea와 마찬가지로 그 안에 있는 위젯들을 선택할수 있게 바꿔주는데 SelectionArea가 Material library의 설정의 따른다면 SelectableRegion은 플랫폼에 연계된 설정이기 때문에 코드에서는 SelectableRegion보다는 SelectionArea가 사용이 용이합니다.

상속 계보도 (Inheritance)

Text가 상속받는 클래스의 계보도는 다음과 같습니다.

Object > DiagnosticableTree > Widget > StatelessWidget > Text

생성자 (Constructors)

Text(String data, {Key? key, TextStyle? style, StrutStyle? strutStyle, TextAlign? textAlign, TextDirection? textDirection, Locale? locale, bool? softWrap, TextOverflow? overflow, @Deprecated(‘Use textScaler instead. ‘ ‘Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ‘ ‘This feature was deprecated after v3.12.0-2.0.pre.’) double? textScaleFactor, TextScaler? textScaler, int? maxLines, String? semanticsLabel, TextWidthBasis? textWidthBasis, TextHeightBehavior? textHeightBehavior, Color? selectionColor})Creates a text widget.
const

Text.rich(InlineSpan textSpan, {Key? key, TextStyle? style, StrutStyle? strutStyle, TextAlign? textAlign, TextDirection? textDirection, Locale? locale, bool? softWrap, TextOverflow? overflow, @Deprecated(‘Use textScaler instead. ‘ ‘Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ‘ ‘This feature was deprecated after v3.12.0-2.0.pre.’) double? textScaleFactor, TextScaler? textScaler, int? maxLines, String? semanticsLabel, TextWidthBasis? textWidthBasis, TextHeightBehavior? textHeightBehavior, Color? selectionColor})Creates a text widget with a InlineSpan.
const

속성 (Properties)

data → String? 화면에 보여줄 문자열
final

hashCode → int 객체의 해쉬코드
read-only inherited

key → Key? 위젯트리에서 서로 자리를 옮기거나 할때 필요한 키값
final inherited

locale → Locale? 사용자의 지역에 따라서 유니코드가 다르게 랜더링될때 필요에 따라 적절한 폰트를 선택하는데 사용
final

maxLines → int? 문자열이 많은 경우 줄바꿈을 하여 보여주다가 줄바꿈 한 라인이 여기서 정한 최대값을 넘어서면 overflow에 정의한대로 행동
final

overflow → TextOverflow? 문자열이 많아서 지정한 구역을 넘어갈때 어떻게 대처할지는 정하는 곳
final

runtimeType → Type 객체의 런타임 유형
read-only inherited

selectionColor → Color? 문자열을 선택했을때 보여줄 배경색상
final

semanticsLabel → String? data로 넣은 문자열이 무엇을 의미하는지 설명해 놓는곳
final

softWrap → bool? 문자열의 Wrap방법을 soft로 할지 정하는 곳. softWrap을 true로 설정하면 해당 문자열이 하나의 문단으로 취급되어 단락이 나뉘지 않는다.
final

strutStyle → StrutStyle? 입력된 텍스트의 높이가 할당된 공간에 맞도록 하고자 할때

style → TextStyle? 문자열을 꾸며줄 스타일을 정의하는 곳
final

textAlign → TextAlign? 수평정렬
final

textDirection → TextDirection? 문자열의 방향
final

textHeightBehavior → TextHeightBehavior? 문자열의 위나 아래에 TextStyle.height을 어떻게 설정할지 결정
final

textScaleFactor → double?이 속성은 더이상 지원하지 않습니다. 기능은 textScaler로 대체할수 있습니다.

textScaler → TextScaler? 문자열을 배치하고 렌더링할때 사용할 폰트크기를 조정
final

textSpan → InlineSpan? 문자열을 InlineSpan으로 보여주고자 할때 사용
final

textWidthBasis → TextWidthBasis? 문자열의 가로너비를 측정하는 방법을 정의
final

클래스 내부함수 (Methods)

build(BuildContext context) → Widget
위젯들을 정의하여 화면을 구성하는 실제 UI를 구성하는데 사용
override

createElement() → StatelessElement
위젯트리에서 해당 위젯의 위치를 관리하기 위해 StatelessElement를 생성
inherited

debugDescribeChildren() → List<DiagnosticsNode>
이 노드의 하위 항목을 설명하는 DiagnosticsNode객체 목록을 반환
inherited

debugFillProperties(DiagnosticPropertiesBuilder properties) → void
노드와 관련된 추가 속성을 정의
override

noSuchMethod(Invocation invocation) → dynamic
존재하지 않는 메서드나 속성에 액세스할 때 호출
inherited

toDiagnosticsNode({String? name, DiagnosticsTreeStyle? style}) → DiagnosticsNode
디버깅 도구 및 DiagnosticsNode.toStringDeep에서 사용되는 개체의 디버그 표현을 반환
inherited

toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) → String
이 개체를 문자열로 표현하여 반환
inherited

toStringDeep({String prefixLineOne = ”, String? prefixOtherLines, DiagnosticLevel minLevel = DiagnosticLevel.debug}) → String
이 노드와 그 하위 항목의 문자열 표현을 반환
inherited

toStringShallow({String joiner = ‘, ‘, DiagnosticLevel minLevel = DiagnosticLevel.debug}) → String
객체에 대한 한 줄의 자세한 설명을 반환
inherited

toStringShort() → String
이 위젯에 대한 간단한 텍스트 설명
inherited

연산자 (Operators)

operator ==(Object other) → bool
동일함을 비교하는 연산자
inherited

여기까지 Flutter공식페이지에서 제공하는 Text class에 대한 매뉴얼이었습니다. 반드시 원문 페이지에 들어가서 다시 한번 살펴보시기를 권장드리고 해당 페이지에서 설명이 충분하지 않은 부분은 링크를 클릭해서 직접 들어가 보시기 바랍니다. 그런 소소한 습관들이 나중에 필요한 정보를 습득하는데 큰 도움이 될거에요. 지루하셨을텐데 잘 따라와 주셔서 감사합니다. 우리는 다음시간에 Row class로 다시 만나요. 감사합니다.

References

Hot Reload and Hot Reset

Stateless와 Stateful 위젯을 사용했을때 이점은 실행시 사용자에게 속도개선을 가져다 줄 뿐만 아니라 코딩하는 개발자에게도 개발속도가 개선되는 이점이 있습니다. Stateless나 Stateful없이 아래와 같이 main()에 화면을 구성하는 코딩을 바로 넣었을때는 화면을 변경하고자 할때 전체 코드를 다시 컴파일해야하는 제약이 있습니다. 아래 backgroundColor의 색상을 teal에서 red로 바꾸고 저장을 해보면 다시 컴파일 하기 전까지는 화면에 아무런 변화가 일어나지 않습니다.

import 'package:flutter/material.dart';

void main() {
  runApp(
    MaterialApp(
      title: 'Flutter Demo',
      home: Scaffold(
        appBar: AppBar(
          backgroundColor: Colors.teal,
          title: const Text('Title'),
        ),
      ),
    ),
  );
}

위와 같이 코딩을 하면 변경된 코드를 적용하기 위해 앱을 재실행해야하고 이때는 처음부터 컴파일을 하기 때문에 아래 Console화면에 따르면 이 짧은 코드를 실행하는데 30초가 걸렸습니다.

Performing hot restart...
Syncing files to device AOSP on IA Emulator...
Restarted application in 3,032ms.

Hot Reload

이 코드를 아래와 같이 StatelessWidget에 넣고 main()에서는 StatelessWidget으로 선언된 클래스를 호출하도록 변경을 하면 얘기가 달라집니다.

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: Scaffold(
        appBar: AppBar(
          backgroundColor: Colors.teal,
          title: const Text('Title'),
        ),
      ),
    );
  }
}

처음에 컴파일 할때 한번은 마찬가지로 시간이 걸리지만, 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앱을 새로 만들면 제공되는 카운터 앱을 가지고 테스트를 해보겠습니다.

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

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

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.teal,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

AppBar의 backgroundColor를 teal로 초기화를 하고 실행을 한뒤 “+”버튼을 눌러서 숫가를 증가시킵니다. 그 뒤에 backgroundColor를 red로 바꾸고 Hot Reload버튼을 누르면 숫자는 그대로 있고 상단바의 배경색만 빨간색으로 바뀝니다. 반면 다시 backgroundColor를 blue로 바꾸고 Hot Reset버튼을 누르면 상단바의 배경색만 파란색으로 바뀌는 것이 아니라 숫자의 값도 “0”으로 초기화가 된것을 보실수 있습니다. Hot Reset은 각종 변수값들도 초기화를 시켜주기 때문에 Hot Reload보다는 시간이 좀 걸리지만 그래도 앱을 종료하고 다시 실행시킬때 보다는 시간이 절약된다는 장점이 있습니다.

코딩할때 Hot Reload와 Hot Reset을 사용해서 더욱 속도감 있게 개발을 하기위해서 StatelessWidget과 StatefulWidget은 반드시 써야하겠습니다. 가장 자주 사용되는 4가지 레이아 Widget에 대해서 배워보도록 하겠습니다. 감사합니다.


Stateless and Stateful 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 코드의 가장 위에서 부터 함께 차근히 살펴 보도록 하겠습니다.

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

material.dart라이브러리를 import하고 main()함수를 선언하는 부분까지는 이전시간에 배운것과 똑같습니다. 다만 여기서는 main()함수에서 바로 코드를 작성하는 것이 아니라 MyApp()이라는 함수를 따로 선언해서 runApp()에 인자로 넣어줌으로써 main()이 실행되면 자동적으로 MyApp()으로 가도록 설계를 했네요. 그럼 MyApp()을 한번 따라가 볼까요?

아래는 MyApp을 정의해 놓은 StatelessWidget 클래스입니다.

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

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()클래스를 호출합니다.

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

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),methods.
    );
  }
}

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에 대해서 공부해보도록 하겠습니다.

References