# 파이썬 ADB 자동화 튜토리얼: 안드로이드 에뮬레이터 제어

> Macro Automation Studio용 파이썬 ADB 자동화 튜토리얼: 앱이 adb를 대신 관리하는 동안 BlueStacks에서 탭, 스와이프, 입력, 키 누름, 스크린샷을 파이썬으로 실행합니다.

Source: https://automationmacro.com/ko/docs/guides/control-an-emulator-from-python (가이드, updated 2026-09-05)

대부분의 파이썬 ADB 자동화 튜토리얼은 `adb shell input tap`을 감싼 `subprocess` 래퍼로 끝납니다. Macro Automation Studio(MAS)용 이 파이썬 ADB 자동화 튜토리얼은 다른 길을 갑니다: 앱이 adb를 갖고, 스크립트는 `mas` 패키지로 앱과 통신합니다. 이 가이드는 새 프로젝트에서 시작해 앱을 열고, 검색어를 찾고, 스크롤하고, 스크린샷을 저장하는 스크립트까지 갑니다. adb 호출을 쓰지 않고 BlueStacks, LDPlayer, MuMu Player, MEmu를 조작하고 싶은 파이썬 개발자를 위한 가이드입니다.

## 시작하기 전에

- MAS가 설치되어 있고 체험 또는 요금제로 로그인한 상태. [MAS 설치](/docs/install)를 참고하세요.
- ADB가 켜진 에뮬레이터가 실행 중이고 **Device Groups**에 기기로 추가된 상태. [기기](/docs/devices)를 참고하세요.
- 기본 파이썬을 알고 있어야 합니다. 그 밖에 직접 설치할 것은 없습니다. MAS는 `mas`가 들어 있는 자체 Python 3.13 환경을 함께 제공합니다.

## 구성 요소가 맞물리는 방식

MAS는 프로젝트 폴더를 작업 디렉터리로 하여 스크립트를 `python -u -m src.app`로 시작합니다. 연결 정보는 환경 변수(`MAS_RPC_PORT`, `MAS_RPC_HOST`, `MAS_DEVICE_ID`, `MAS_SESSION_TOKEN`)로 넘깁니다. 모든 `mas.*` 호출은 앱으로 가는 JSON-RPC 2.0 요청이 됩니다. 앱이 adb 명령, 템플릿 검색 또는 OCR을 실행하고 결과를 돌려줍니다. 첫 줄이 실행되기 전에 기기가 바인딩되어 있으므로 `connect()` 호출도, 관리할 시리얼도 없습니다. [개념](/docs/concepts)을 참고하세요.

## 프로젝트 만들기

1. **Macros**를 열고 **Create New Project**를 클릭합니다.
2. **Code-Based**를 선택하고 **Target Device**를 mobile로 설정한 뒤 프로젝트 이름을 입력하고 **Create Project**를 클릭합니다.
3. 프로젝트를 엽니다. Code Editor에 `src/app.py`가 표시됩니다.
4. 기기 선택기에서 에뮬레이터를 고르고, 코드 조각을 시험하고 싶을 때마다 **Run**(<kbd>F5</kbd>)을 클릭합니다. **Stop**은 <kbd>Shift</kbd>+<kbd>F5</kbd>입니다.

## 화면 크기와 좌표

SDK에 넘기는 모든 좌표는 기기 화면 왼쪽 위 모서리 기준의 픽셀 오프셋이며, 기기 자체 해상도 기준입니다. 모니터에 보이는 에뮬레이터 창의 크기는 상관없습니다.

```python
import mas

size = mas.get_screen_size()
print(f"Screen: {size.width}x{size.height}")

center = (size.width // 2, size.height // 2)
mas.click(*center)
```

`get_screen_size()`는 `width`와 `height`가 있는 `ScreenSize`를 반환합니다. `get_device_info()`는 거기에 기기 `name`, `type`, `connected` 플래그를 더해 반환합니다. 스크립트가 해상도 변경을 견뎌야 한다면 끝의 예제처럼 위치를 크기의 비율로 계산하세요. 템플릿 이미지와 OCR 영역은 해상도 변경을 견디지 못합니다. [이미지 인식 가이드](/docs/guides/image-recognition-macros)를 참고하세요.

## 파이썬 adb 스크린샷 찍기

```python
import base64
import mas

shot = mas.take_screenshot()
print(shot.width, shot.height, shot.timestamp)

with open("screen.png", "wb") as f:
    f.write(base64.b64decode(shot.base64))
```

`take_screenshot()`은 `Screenshot`을 반환합니다: `base64`에 PNG가 담기고, `width`와 `height`는 픽셀 크기, `timestamp`는 ISO 8601 문자열입니다. 위 파일은 작업 디렉터리인 프로젝트 폴더에 저장됩니다. 같은 객체를 `screenshot=shot`으로 여러 `find_object` 호출에 넘기면 다시 캡처하지 않고 모두 한 프레임에서 찾습니다.

## 탭

```python
mas.click(270, 800)                 # tap, then wait 1000 ms
mas.click(270, 800, delay_ms=300)   # shorter pause for tight loops
```

`click(x, y, delay_ms=1000)`은 한 번 탭하고 앱이 반응할 수 있도록 `delay_ms`만큼 기다립니다. 탭에는 누르고 있는 시간이 없습니다. 길게 누르려면 `duration_ms`와 함께 `key_press`를 쓰세요.

## 스와이프

```python
mas.swipe((270, 750), (270, 300), duration_ms=400)    # scroll a list down: drag from lower to upper
mas.swipe((100, 500), (400, 500), duration_ms=1000)   # drag and drop: slow and deliberate
```

`swipe(from_coords, to_coords, duration_ms=1000)`은 `(x, y)` 튜플 두 개를 받습니다. 짧은 지속 시간은 튕기고, 긴 지속 시간은 끕니다. 핀치 제스처는 별도 호출입니다: `zoom_in()`과 `zoom_out()`은 기본적으로 화면 중앙에서 `percent=50`으로 동작합니다.

## 입력

```python
mas.click(270, 120, delay_ms=500)          # focus the field first
mas.input_text("hello world")
mas.input_text("new value", clear=True)    # replace what is there
```

`input_text(text, delay_ms=0, clear=False)`는 포커스된 필드에 입력하므로 먼저 필드를 탭하세요. `clear=True`는 커서를 끝으로 옮기고 기존 문자를 지운 뒤 입력합니다.

## 키 누르기

```python
from mas import KeyCode

mas.key_press(KeyCode.BACK)
mas.key_press(KeyCode.HOME)
mas.key_press(KeyCode.ENTER)
mas.key_press(KeyCode.DELETE, repeat=10)          # ten presses in one command
mas.key_press(KeyCode.POWER, duration_ms=3000)    # long press
```

`key_press(key_code, modifiers=None, duration_ms=100, repeat=1)`은 Android 키 이벤트를 보냅니다. `duration_ms`가 500 이상이면 길게 누르기가 되며, 그 이상의 정확한 밀리초는 반영되지 않습니다. `repeat`는 1부터 100까지이며 파이썬 반복문보다 훨씬 빠릅니다. `KeyCode`에는 D패드, 볼륨, 미디어, 숫자 키도 있습니다.

## 앱 열기와 확인

```python
mas.open_app("com.android.settings", timeout_ms=5000)
print(mas.get_current_app())                        # package name in front
print(mas.is_app_focused("com.android.settings"))   # True or False

if mas.get_app_state("com.android.chrome") == mas.NOT_RUNNING:
    mas.open_app("com.android.chrome")

mas.close_app("com.android.settings")
```

`open_app(package_name, timeout_ms=2000)`은 패키지 이름으로 실행하고 `timeout_ms`만큼 기다립니다. 패키지 이름을 알아내려면 앱을 손으로 열고 `mas.get_current_app()`을 출력하세요. `get_app_state`는 `NOT_INSTALLED`, `NOT_RUNNING`, `RUNNING_IN_BACKGROUND_SUSPENDED`, `RUNNING_IN_BACKGROUND` 또는 `RUNNING_IN_FOREGROUND`를 반환합니다. `close_app`은 패키지를 강제 종료합니다.

## 완전한 스크립트

스크립트는 Android 설정을 열고, 검색어를 찾고, 결과를 스크롤하고, 스크린샷을 저장하고, OCR로 검색어가 화면에 있는지 확인합니다. 검색 상자는 Asset Lab에서 자른 템플릿입니다. ID를 본인 것으로 바꾸세요. 스와이프는 어떤 해상도에서도 동작하도록 화면 크기의 비율을 씁니다.

```python
import base64
import sys
import time

import mas
from mas import KeyCode

PACKAGE = "com.android.settings"
TERM = "Display"

images = mas.images({"search_box": 301})


def main():
    size = mas.get_screen_size()
    mas.log(f"Screen {size.width}x{size.height}")

    mas.open_app(PACKAGE, timeout_ms=5000)
    if not mas.is_app_focused(PACKAGE):
        mas.log(f"{PACKAGE} did not come to the front", level="error")
        sys.exit(2)

    box = mas.find_object_retry(images.search_box, total_tries=3, time_sleep=2.0)
    if box is None:
        mas.log("Search box not found; crop it in Asset Lab", level="error")
        sys.exit(2)
    mas.click(box.x, box.y, delay_ms=800)
    mas.input_text(TERM, clear=True)
    mas.key_press(KeyCode.ENTER)
    time.sleep(2)

    x = size.width // 2
    mas.swipe((x, int(size.height * 0.75)), (x, int(size.height * 0.35)), duration_ms=600)
    time.sleep(1)

    shot = mas.take_screenshot()
    with open("search_results.png", "wb") as f:
        f.write(base64.b64decode(shot.base64))
    mas.log(f"Saved search_results.png ({shot.width}x{shot.height})")

    text = mas.read_text(screenshot=shot, psm=11)
    mas.log(f"Term on screen: {TERM.lower() in text.text.lower()}")

    mas.key_press(KeyCode.HOME)


if __name__ == "__main__":
    main()
```

`psm=11`을 쓴 `read_text`는 프레임 전체의 흩어진 텍스트를 읽으므로 결과 목록에 알맞습니다. 카운터 하나를 읽기 위한 영역과 모드는 [OCR 가이드](/docs/guides/ocr-text-reading)에서 다룹니다. 직접 쓰기보다 작업을 설명하고 싶다면 [MAS Agent](/agent)가 이런 스크립트를 대신 써 줍니다.

## 이 파이썬 튜토리얼에서 adb가 하는 일

adb를 직접 호출하지는 않지만 밑에서 일하고 있는 것은 adb입니다.

- **바이너리.** 설치 프로그램이 adb를 함께 제공합니다. Windows에서는 `C:\ProgramData\MacroAutomationStudio\3rdparty`에 있고, 내장 사본이 대안입니다. Mac에서는 앱 번들 안에 있고, Homebrew의 adb가 대안입니다. PATH에는 아무것도 추가되지 않습니다. 자세한 내용은 [설치](/docs/install) 페이지에 있습니다.
- **연결.** 에뮬레이터는 로컬 TCP 포트로 adb를 노출합니다. **Start**나 **Run**을 클릭하면 MAS가 기기 카드의 포트로 `adb connect 127.0.0.1:<port>`를 실행하고 실행 동안 그 세션을 유지합니다.
- **입력.** `click`은 `adb shell input tap`, `swipe`는 `adb shell input touchscreen swipe`, `input_text`는 `adb shell input text`, `key_press`는 `adb shell input keyevent`가 됩니다. 핀치 제스처는 에뮬레이터의 터치 입력 장치에 직접 씁니다.
- **화면.** `take_screenshot`과 모든 `find_object`는 Windows에서 PNG를 망가뜨리지 않는 바이너리 안전 형식인 `adb exec-out screencap -p`로 캡처합니다. `get_screen_size`는 `wm size`를 읽고, `get_current_app`은 윈도우 매니저를 읽습니다.
- **앱.** `open_app`은 `monkey`로 런처 인텐트를 보내고 `am start`로 대체하며, `close_app`은 `am force-stop`을 실행합니다.
- **서버.** MAS는 자체 adb 서버를 실행합니다. PATH의 두 번째 adb 빌드가 자기 서버를 시작하면 둘이 서로를 교체하고 기기가 Connecting으로 떨어집니다. [ADB 문제 해결](/docs/adb-troubleshooting) 페이지에 포트별 해결법이 있습니다.

앱이 이 모든 것을 하므로 BlueStacks에서 쓴 스크립트는 LDPlayer, 클라우드 기기, 또는 로컬 포트로 연결한 본인 휴대폰에서도 수정 없이 실행됩니다.

## 잘못될 수 있는 것

### Could not discover RPC port

스크립트가 MAS가 아니라 터미널에서 시작되어 환경 변수가 없습니다. Code Editor의 **Run**이나 기기 카드에서 실행하세요.

### 탭이 엉뚱한 곳에 떨어짐

기기 픽셀 대신 창 픽셀을 쓰고 있거나 에뮬레이터 해상도가 바뀌었습니다. `mas.get_screen_size()`를 출력해 넘기는 좌표와 비교하세요. Asset Lab의 좌표는 이미 기기 픽셀입니다.

### input_text가 아무것도 입력하지 않음

포커스된 필드가 없습니다. `click`으로 필드를 탭하고 입력 전에 `delay_ms=500`을 주세요. 일부 앱은 필드를 덮는 키보드를 엽니다. 입력 후 `key_press(KeyCode.BACK)`이 닫아 줍니다.

### 기기를 찾을 수 없거나 offline

에뮬레이터에서 ADB가 꺼져 있거나, 기기 카드의 포트가 틀렸거나, 다른 adb 서버가 넘겨받았습니다. [ADB 문제 해결](/docs/adb-troubleshooting)을 따르세요.

조작하는 앱이 게임이라면 다른 자동화 계정과 똑같이 조심해서 다루세요. 100% 안전한 자동화 도구는 없으므로, 책임감을 가지고 본인의 판단에 따라 자동화하세요.
