2. 액티비티 구성하기¶
연극 무대를 떠올려 봅시다. 관객에게는 한 번에 하나의 무대만 보입니다. 장면이 바뀌면 새 무대가 앞으로 나오고, 이전 무대는 사라지는 것이 아니라 뒤에서 대기합니다. 커튼콜이 되면 대기하던 무대들이 역순으로 다시 나올 수도 있습니다.
안드로이드의 화면이 정확히 이렇게 동작합니다. 화면 하나하나가 액티비티(Activity) 라는 무대이고, 새 화면을 열면 이전 화면은 그 뒤에 쌓입니다. 뒤로 가기 버튼은 "맨 앞 무대를 치우고 그 뒤의 무대를 다시 보여라"는 명령입니다.
이번 장에서는 버킷맵의 무대 4개를 전부 만들고, 무대 사이를 오가는 방법과 무대끼리 데이터를 건네는 방법을 배웁니다.
버킷맵의 화면 지도¶
버킷맵은 화면 4개로 이루어집니다. 화살표는 "버튼을 눌러 이동하는 방향"입니다.
┌──────────────────┐
┌──────▶│ SearchActivity │─────┐
│ │ (장소 찾기) │ │ 결과를 탭하면
│ └──────────────────┘ ▼
┌───────────────┐ ┌─────────────────────┐
│ MainActivity │ │ PlaceDetailActivity │
│ (지도 화면) │ │ (장소 상세) │
└───────────────┘ └─────────────────────┘
│ ┌──────────────────┐ ▲
└──────▶│ MyPlacesActivity │─────┘ 저장한 장소를 탭해도
│ (내 장소) │ 같은 상세 화면
└──────────────────┘
- MainActivity — 지도가 깔린 홈 화면. 5장에서 진짜 지도가 들어온다.
- SearchActivity — 장소를 검색하는 화면. 6장에서 진짜 검색이 붙는다.
- PlaceDetailActivity — 장소 하나의 상세 정보. 7장에서 찜/방문 버튼이 완성된다.
- MyPlacesActivity — 내가 담은 장소 목록.
이번 장에서는 이 4개를 임시 내용물로 만들어 서로 오가게만 합니다. 뼈대를 먼저 세우고 살은 다음 장부터 붙입니다.
액티비티 3개 추가하기¶
MainActivity 는 이미 있으니, 나머지 3개를 만듭니다. Android Studio 의 마법사를 쓰면 Java 파일 + 레이아웃 XML + Manifest 등록까지 한 번에 처리해 줍니다.
[CH-1 아래 과정을 그대로 진행하시오.]¶
- 프로젝트 창에서
java/com.example.myapplication패키지를 마우스 오른쪽 클릭한다. - New > Activity > Empty Views Activity 를 선택한다.
- Activity Name 에
SearchActivity를 입력하고 Finish 를 누른다. (Language 가 Java 인지 확인) - 같은 방법으로
PlaceDetailActivity,MyPlacesActivity도 만든다.
세 번 반복하면 Java 파일 3개와 레이아웃 XML 3개가 생깁니다. 이때 AndroidManifest.xml 을 열어 보면 마법사가 새 액티비티들을 자동으로 등록해 둔 것을 볼 수 있습니다. 아래와 같은 모양이 되도록 확인하고, 다르면 맞춰 줍니다.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.BucketMap"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".SearchActivity" android:exported="false" />
<activity android:name=".PlaceDetailActivity" android:exported="false" />
<activity android:name=".MyPlacesActivity" android:exported="false" />
</application>
</manifest>
[설명]
- 액티비티는 Manifest 에 등록해야만 존재를 인정받는다. 등록 없이 열려고 하면 앱이 그 자리에서 죽는다(ActivityNotFoundException). 마법사가 아니라 손으로 클래스를 만들었다면
<activity>줄도 손으로 추가해야 한다. MainActivity에만 붙어 있는<intent-filter>의 MAIN/LAUNCHER 는 "앱 아이콘을 누르면 이 무대부터 올려라"는 표시다. 그래서 시작 화면이 MainActivity 다.android:exported="false"는 "다른 앱이 이 화면을 직접 열 수 없다"는 뜻이다. 시작 화면이 아닌 내부 화면은 false 가 기본이다.- 여러분의 테마 이름은
Theme.BucketMap이 아니라Theme.My_Application처럼 보일 수 있다 — 프로젝트 생성 시 이름에 따라 달라질 뿐 동작은 같다. 3장에서 테마 파일을 통째로 정리한다.
인텐트로 화면 이동하기¶
무대와 무대 사이를 오가려면 시스템에 "이 무대를 올려 주세요" 라고 부탁하는 쪽지를 보내야 합니다. 이 쪽지가 인텐트(Intent) 입니다. 받는 무대의 클래스 이름을 정확히 적어 보내는 방식을 명시적 인텐트라고 합니다.
MainActivity 에 버튼 두 개를 놓고, 각각 검색 화면과 내 장소 화면으로 이동해 보겠습니다.
[CH-2 아래 코드를 그대로 작성하고 실행하시오.]¶
먼저 res/values/strings.xml 에 버튼 문자열 두 개를 추가합니다.
<resources>
<string name="app_name">버킷맵</string>
<string name="main_welcome">버킷맵에 오신 것을 환영합니다!</string>
<string name="btn_find_place">장소 찾기</string>
<string name="btn_my_places">내 장소</string>
</resources>
res/layout/activity_main.xml 에 버튼 두 개를 추가합니다.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/main_welcome"
android:textSize="20sp" />
<Button
android:id="@+id/btn_find_place"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="@string/btn_find_place" />
<Button
android:id="@+id/btn_my_places"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/btn_my_places" />
</LinearLayout>
MainActivity.java 를 아래처럼 고칩니다.
package com.example.myapplication;
import android.content.Intent;
import android.os.Bundle;
import android.widget.Button;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button findButton = findViewById(R.id.btn_find_place);
Button myPlacesButton = findViewById(R.id.btn_my_places);
findButton.setOnClickListener(v -> {
Intent intent = new Intent(this, SearchActivity.class);
startActivity(intent);
});
myPlacesButton.setOnClickListener(v -> {
Intent intent = new Intent(this, MyPlacesActivity.class);
startActivity(intent);
});
}
}
실행 결과

버튼을 누르면 흰 화면(새 액티비티의 빈 레이아웃)으로 넘어가고, 뒤로 가기를 하면 메인으로 돌아옵니다.
[설명]
android:id="@+id/btn_find_place"의@+id는 "btn_find_place 라는 번호표를 새로 만든다"는 뜻이다. Java 에서findViewById(R.id.btn_find_place)로 이 번호표를 찾아 버튼 객체를 손에 쥔다.setOnClickListener(v -> { … })는 "이 버튼이 눌리면 중괄호 안 코드를 실행하라"고 등록하는 것이다.v -> …는 람다(lambda)라는 축약 문법으로, "클릭됐을 때 실행할 코드 블록" 정도로 읽으면 된다.new Intent(this, SearchActivity.class)— 쪽지를 만든다. 첫 칸은 "보내는 곳(지금 화면)", 둘째 칸은 "열어 달라는 무대의 클래스"다. 클래스를 콕 집어 적었으므로 명시적 인텐트다.- 클래스를 적는 대신 "사진 찍을 수 있는 화면 아무거나"처럼 조건만 적어 보내는 암시적 인텐트도 있다 — 8장 도전과제에서 만난다.
startActivity(intent)가 실제로 쪽지를 시스템에 제출하는 순간이다. 시스템이 SearchActivity 무대를 만들어 맨 앞에 올린다.
임시 화면 채우기¶
새로 만든 화면 3개는 아직 텅 빈 흰 화면입니다. 어느 화면에 와 있는지 눈으로 확인할 수 있게 임시 내용물을 넣습니다. 이 레이아웃들은 3~4장에서 전부 갈아엎을 임시 골판지입니다 — 그래서 문자열도 리소스로 빼지 않고 그냥 적습니다.
[CH-3 아래 코드를 그대로 작성하고 실행하시오.]¶
res/layout/activity_search.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="여기는 검색 화면입니다 (임시)"
android:textSize="20sp" />
<Button
android:id="@+id/btn_open_detail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="상세 화면 열기 (테스트)" />
</LinearLayout>
res/layout/activity_my_places.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="여기는 내 장소 화면입니다 (임시)"
android:textSize="20sp" />
</LinearLayout>
MyPlacesActivity.java 는 마법사가 만든 코드를 아래처럼 최소로 정리합니다.
package com.example.myapplication;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
public class MyPlacesActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my_places);
}
}
실행 결과

장소 찾기 버튼으로 들어온 검색 화면입니다. 아직 임시지만, 어느 화면에 와 있는지는 분명해졌습니다.
화면 사이에 데이터 건네기 — Extra¶
인텐트라는 쪽지에는 행선지만 적을 수 있는 것이 아닙니다. 짐도 실을 수 있습니다. 쪽지에 붙이는 짐을 Extra 라고 부르고, 짐마다 이름표(키)를 붙입니다.
검색 화면에서 상세 화면으로 "장소 이름"을 건네 보겠습니다. 지금은 진짜 검색이 없으니 "떡볶이 맛집"이라는 가짜 이름을 보냅니다.
[CH-4 아래 코드를 그대로 작성하고 실행하시오.]¶
받는 쪽부터 만듭니다. res/layout/activity_place_detail.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/text_place_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="24sp"
android:textStyle="bold" />
</LinearLayout>
PlaceDetailActivity.java:
package com.example.myapplication;
import android.os.Bundle;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
public class PlaceDetailActivity extends AppCompatActivity {
public static final String EXTRA_NAME = "extra_name";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_place_detail);
TextView nameText = findViewById(R.id.text_place_name);
String name = getIntent().getStringExtra(EXTRA_NAME);
nameText.setText(name);
}
}
보내는 쪽 SearchActivity.java:
package com.example.myapplication;
import android.content.Intent;
import android.os.Bundle;
import android.widget.Button;
import androidx.appcompat.app.AppCompatActivity;
public class SearchActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_search);
Button openDetailButton = findViewById(R.id.btn_open_detail);
openDetailButton.setOnClickListener(v -> {
Intent intent = new Intent(this, PlaceDetailActivity.class);
intent.putExtra(PlaceDetailActivity.EXTRA_NAME, "떡볶이 맛집");
startActivity(intent);
});
}
}
실행 결과

장소 찾기 → 상세 화면 열기 (테스트) 순서로 누르면, 검색 화면이 쪽지에 실어 보낸 "떡볶이 맛집"이 상세 화면에 나타납니다. 화면 사이의 데이터 전달이 완성됐습니다.
[설명]
putExtra(이름표, 값)으로 짐을 싣고, 받는 화면에서getIntent().getStringExtra(이름표)로 꺼낸다. 이름표가 한 글자라도 다르면 짐을 못 찾고null이 나온다.- 그래서 이름표 문자열을 양쪽에 따로 적지 않고
EXTRA_NAME이라는 상수 하나로 만들어 같이 쓴다. 오타가 원천 봉쇄된다. 상수를 받는 쪽(PlaceDetailActivity)에 둔 것은 "이 화면이 어떤 짐을 기대하는지"를 그 화면 스스로 문서화하게 하기 위해서다. - 문자열 말고도
putExtra는 정수, 실수, 불리언 등을 실을 수 있다. 6장에서 위도·경도(double)를 실어 나르게 된다. - 뒤로 가기를 누르면 상세 → 검색 → 메인 순서로 돌아간다. 무대가 쌓인 역순으로 걷히는 것이다. 코드에서 지금 무대를 스스로 걷어 내리고 싶을 때는
finish()를 호출한다.
[self Test 1] - 다섯 번째 액티비티 TestActivity 를 만들어 보시오. 메인 화면에 버튼을 하나 더 놓고, 자기 이름을 extra 로 실어 보내 TestActivity 화면 가운데에 표시할 것. (확인이 끝나면 버튼과 액티비티는 지워서 화면 4개로 되돌려 놓는다.)
[self Test 2] - CH-4 에서 SearchActivity 의 putExtra(...) 줄을 주석 처리하고 실행해 보시오. 상세 화면이 어떻게 보이는가? 왜 그렇게 되는지 [설명] 1번을 다시 읽고 생각해 볼 것.