본문으로 바로가기





안녕하세요. 안드로이드 PEACE-입니다.

지난 포스팅에서 Firebase(파이어베이스)가 무엇인지 알아봤습니다. 이번 포스팅에서는 파이어베이스의 특 장점인 Realtime Database를 사용해서 간단한 채팅 앱을 구현해보겠습니다.



1. Realtime Database


참고 : 파이어베이스리얼타임 데이터베이스Android, iOS, Web에서 사용이 가능합니다!


'리얼타임 데이터베이스'란 데이터 저장 공간인 데이터베이스에서 데이터를 실시간으로 주고받는 것을 의미합니다. Firebase데이터베이스는 개발자가 직접 구축하는게 아니라 Firebase에서 기본적으로 제공하는 폼을 사용합니다. 이 데이터베이스는 Android에서 API 사용으로 몇줄의 소스만을 통해 실시간으로 앱 데이터를 저장 및 동기화를 해줍니다. 그렇기 때문에 서버관리에서 발생할 수 있는 버그도 없으며, 데이터구조를 구축하는데에 시간을 쏟을 일도 없습니다. 또한 앱 개발에 집중할 수 있습니다. 




2. Firebase for android 환경 만들기


본 포스팅은 안드로이드 스튜디오에서 파이어베이스를 사용하는 예제를 구현했습니다. 안드로이드 프로젝트에서 파이어베이스 API를 사용하기 위해서는 기본적으로 Firebase에 프로젝트를 생성하여 내 안드로이드 프로젝트에서 Firebase를 사용할 수 있도록 해야합니다. 그리고 리얼타임 데이터베이스를 사용하기 위해 데이터베이스 규칙에 쓰기/읽기와 같은 규칙을 정해야합니다. 그 과정은 아래에 사진에 담았습니다.



안드로이드 프로젝트에 파이어베이스를 등록하는 과정을 요약한 것입니다.

 

1) 파이어베이스 홈페이지 접속

 2) 파이어베이스 프로젝트 생성

 3) 안드로이드 앱에 파이어베이스 추가

   3-1) 안드로이드 프로젝트 패키지 등록

   3-2) 구글 서비스 플러그인에서 'google-services.json' 다운로드 및 안드로이드 프로젝트에 붙여넣기

   3-3) build.gradle를 수정하여 플러그인을 사용할 수 있도록 한다.

 4) 파이어베이스에서 데이터베이스 규칙 변경




실제 안드로이드 프로젝트에 파이어베이스를 등록하는 과정은 아래와 같습니다.


1) 파이어베이스 홈페이지(https://firebase.google.com/) 접속





2) 파이어베이스 프로젝트 생성





3) 안드로이드 앱에 파이어베이스 추가


현재 개발중인 안드로이드 프로젝트의 패키지명을 작성해줍니다. (패키지명은 안드로이드 프로젝트의 메니페스트파일  참고)




구글 서비스 플러그인에서 제공하는 json형식의 파일을 다운받은 후, 안드로이드 프로젝트(Project - app)에 넣어줍니다.




안드로이드 프로젝트의 build.gradle(Android - Gradle Script - build.gradle)에서 플러그인 사용을 위해 소스를 추가해줍니다.

파이어베이스에서 버전 업이나 플러그인 변경으로 아래 추가해야하는 소스가 달라질 수도 있습니다. 

build.gradle(Project)에 추가할 소스코드 : classpath 'com.google.gms:google-services:3.0.0' 

build.gradle(Module.app)에 추가할 소스코드 : apply plugin'com.google.gms.google-services'





4) 파이어베이스의 데이터베이스 규칙 변경


데이터베이스 - 데이터에서 '문서보기'를 새탭으로 엽니다.




읽기와 쓰기를 공개적으로 허용하기 위해 다음과 같은 소스코드를 [데이터베이스 - 규칙]에 작성해줍니다.




3. 본 예제의 목표


예제는 채팅방 입장 페이지채팅 페이지로 두 가지 화면을 구성했습니다. 채팅방 입장 페이지에서는 원하는 유저 이름으로 이미 존재하는 채팅방에 들어갈 수도 있고, 자신이 채팅방 이름을 정해 채팅방을 만들 수도 있게했습니다. 또한 리스트뷰로 채팅방 목록 View를 만들었습니다. 채팅방 페이지는 서로 다른 사용자가 실시간으로 채팅을 할 수 있는 공간입니다. 아래는 채팅방 등록과 채팅, 그리고 파이어베이스 데이터베이스에 저장된 데이터를 확인하는 사진입니다. 너무 디테일하고 페이지 구성이 많으면 포스팅하는데, 그리고 보는 입장에서 힘들 것 같아 최대한 적은 규모로 구현했습니다. 추가 기능 구현에 대해 궁금한 점을 댓글로 남겨주시면 답변이나 새 포스팅을 하겠습니다.











4. 안드로이드에서 파이어베이스 리얼타임 데이터베이스 사용하기


파이어베이스의 데이터베이스 구조는 마치 부모과 자식 간의 계속된 관계와 같습니다. 예를들면 HTML에서 리스트 사용 시<ul>안에 <li>를 넣고 그안에 또 다른 자식 리스트 <ul>을 넣을 수 있는 것과 같습니다.



데이터 전송


우선 데이터베이스 사용에 관한 dependency를 추가해줘야합니다. 안드로이드 프로젝트의 app-level build.gradle 파일에 추가해줍니다.

compile 'com.google.firebase:firebase-database:11.0.2'


다음으로 내가 사용할 데이터베이스의 인스턴스를 불러와야합니다.

FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference databaseReference = database.getReference("message");


그리고 아래와 같이 DatabaseReference로 간단하게 데이터를 넣을 수 있습니다.

하지만 저희는 '채팅방 이름' 넣어야하고 'Data Set'도 넣어야하므로 단순히 이렇게 쓰지는 않습니다.

databaseReference.setValue("Hello, World!");


Data Set은 아래와 같습니다. (안드로이드 프로젝트에서 ChatDTO.class로 만들었습니다. 최 하단 소스코드 참고)



채팅방에서 메시지 입력 후 전송 버튼을 누르면 아래와 같은 소스가 실행됩니다.

ChatDTO chat = new ChatDTO(USER_NAME, chat_edit.getText().toString()); //ChatDTO를 이용하여 데이터를 묶는다.
databaseReference.child("chat").child(CHAT_NAME).push().setValue(chat); //databaseReference를 이용해 데이터 푸쉬
chat_edit.setText(""); //입력창 초기화


여기까지는 데이터를 리얼타임 데이터베이스에 전송하는 작업이었습니다. FirebaseDatabaseDatabaseReference 두 가지만로 정말 간단하게 데이터 푸쉬가 가능하다는 것을 느낄 수 있습니다. 다음은 파이어베이스에서 리얼타임으로 데이터를 가져오는 방법입니다.



데이터 수신


데이터는 리스너를 사용해 수신할 수있다. 리스너는 두 가지가 있으며, 본 예제에서는 부모-자식 관계를 사용하고 데이터 저장과 삭제에 대한 디테일한 기능을 구현을 위해 ChildEventListener를 사용합니다.


리스너이벤트 콜백일반적인 용도
ValueEventListeneronDataChange()경로의 전체 내용에 대한 변경을 읽고 수신 대기합니다.

onCancelled()

읽기가 취소된 경우에 호출됩니다. 또한  이 메소드에는 실패 이유를 나타내는 DatabaseError 개체가 전달됩니다.

ChildEventListeneronChildAdded()항목 목록을 검색하거나 항목 목록에 대한 추가를 수신 대기합니다. onChildChanged()  onChildRemoved()와 함께 사용하여 목록의 변경을 모니터링할 수 있습니다.
onChildChanged()목록의 항목에 대한 변경을 수신 대기합니다. onChildAdded() 및 onChildRemoved()와 함께 사용하여 목록의 변경을 모니터링할 수 있습니다.
onChildRemoved()목록의 항목 삭제를 수신 대기합니다. onChildAdded() 및 onChildChanged()와 함께 사용하여 목록의 변경을 모니터링할 수 있습니다.
onChildMoved()순서 있는 목록의 항목 순서 변경을 수신 대기합니다. 현재의 정렬 기준에 따라 항목 순서 변경의 원인이 된 onChildMoved() 이벤트가 onChildChanged() 이벤트를 항상 뒤따릅니다.


아래는 ChildEventListener를 사용해 데이터를 수신하는 코드입니다. 이 리스너에서 Override하는 메서드들은 리얼타임의 기능을 제공합니다. 아래 두 메서드는 본 예제에서 필요한 기능을 만들기 위해 구현한 부분입니다.

- onChildAdded()는 리스너를 처음 사용하기 시작할 때, 파이어베이스 데이터베이스의 데이터를 받아옵니다. (채팅방 입장 시 메시지 내역 수신)

onChildRemoved()는 사용해 데이터베이스의 데이터 삭제가 이뤄지면 자동으로 호출됩니다. (채팅방 메시지 삭제 시 데이터 재 수신)


// 메시지를 보여주기 위한 리스트 어댑터 생성 및 세팅
final ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, android.R.id.text1);
chat_view.setAdapter(adapter);


/* 데이터 받아오기 및 어댑터 데이터 추가 및 삭제 등.. 리스너 관리

* child("chat")은 파이어베이스 데이터베이스의 루트 바로 밑에있는 'chat'를 선택한다는 뜻 (없으면 자동으로 생성한다.)

* child(chatName)은 생성 or 입장할 '채팅 방 이름'이다. */

databaseReference.child("chat").child(chatName).addChildEventListener(new ChildEventListener() {
@Override
public void onChildAdded(DataSnapshot dataSnapshot, String s) {
addMessage(dataSnapshot, adapter);
Log.e("LOG", "s:"+s);
}

@Override
public void onChildChanged(DataSnapshot dataSnapshot, String s) {

}

@Override
public void onChildRemoved(DataSnapshot dataSnapshot) {
removeMessage(dataSnapshot, adapter);
}

@Override
public void onChildMoved(DataSnapshot dataSnapshot, String s) {

}

@Override
public void onCancelled(DatabaseError databaseError) {

}
});




5. 소스코드 


프로젝트 구성



소스코드


ChatDTO.class

public class ChatDTO {

private String userName;
private String message;

public ChatDTO() {}
public ChatDTO(String userName, String message) {
this.userName = userName;
this.message = message;
}

public void setUserName(String userName) {
this.userName = userName;
}

public void setMessage(String message) {
this.message = message;
}

public String getUserName() {
return userName;
}

public String getMessage() {
return message;
}
}


StartActivity.class

import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;

import com.google.firebase.database.ChildEventListener;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;

public class StartActivity extends AppCompatActivity {

private EditText user_chat, user_edit;
private Button user_next;
private ListView chat_list;

private FirebaseDatabase firebaseDatabase = FirebaseDatabase.getInstance();
private DatabaseReference databaseReference = firebaseDatabase.getReference();

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_start);

user_chat = (EditText) findViewById(R.id.user_chat);
user_edit = (EditText) findViewById(R.id.user_edit);
user_next = (Button) findViewById(R.id.user_next);
chat_list = (ListView) findViewById(R.id.chat_list);

user_next.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (user_edit.getText().toString().equals("") || user_chat.getText().toString().equals(""))
return;

Intent intent = new Intent(StartActivity.this, ChatActivity.class);
intent.putExtra("chatName", user_chat.getText().toString());
intent.putExtra("userName", user_edit.getText().toString());
startActivity(intent);
}
});

showChatList();

}

private void showChatList() {
// 리스트 어댑터 생성 및 세팅
final ArrayAdapter<String> adapter

= new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, android.R.id.text1);
chat_list.setAdapter(adapter);

// 데이터 받아오기 및 어댑터 데이터 추가 및 삭제 등..리스너 관리
databaseReference.child("chat").addChildEventListener(new ChildEventListener() {
@Override
public void onChildAdded(DataSnapshot dataSnapshot, String s) {
Log.e("LOG", "dataSnapshot.getKey() : " + dataSnapshot.getKey());
adapter.add(dataSnapshot.getKey());
}

@Override
public void onChildChanged(DataSnapshot dataSnapshot, String s) {

}

@Override
public void onChildRemoved(DataSnapshot dataSnapshot) {

}

@Override
public void onChildMoved(DataSnapshot dataSnapshot, String s) {

}

@Override
public void onCancelled(DatabaseError databaseError) {

}
});

}
}


ChatActivity.class

import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;

import com.google.firebase.database.ChildEventListener;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;

public class ChatActivity extends AppCompatActivity {

private String CHAT_NAME;
private String USER_NAME;

private ListView chat_view;
private EditText chat_edit;
private Button chat_send;

private FirebaseDatabase firebaseDatabase = FirebaseDatabase.getInstance();
private DatabaseReference databaseReference = firebaseDatabase.getReference();

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_chat);

// 위젯 ID 참조
chat_view = (ListView) findViewById(R.id.chat_view);
chat_edit = (EditText) findViewById(R.id.chat_edit);
chat_send = (Button) findViewById(R.id.chat_sent);

// 로그인 화면에서 받아온 채팅방 이름, 유저 이름 저장
Intent intent = getIntent();
CHAT_NAME = intent.getStringExtra("chatName");
USER_NAME = intent.getStringExtra("userName");

// 채팅 방 입장
openChat(CHAT_NAME);

// 메시지 전송 버튼에 대한 클릭 리스너 지정
chat_send.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (chat_edit.getText().toString().equals(""))
return;

ChatDTO chat = new ChatDTO(USER_NAME, chat_edit.getText().toString()); //ChatDTO를 이용하여 데이터를 묶는다.
databaseReference.child("chat").child(CHAT_NAME).push().setValue(chat); // 데이터 푸쉬
chat_edit.setText(""); //입력창 초기화

}
});
}

private void addMessage(DataSnapshot dataSnapshot, ArrayAdapter<String> adapter) {
ChatDTO chatDTO = dataSnapshot.getValue(ChatDTO.class);
adapter.add(chatDTO.getUserName() + " : " + chatDTO.getMessage());
}

private void removeMessage(DataSnapshot dataSnapshot, ArrayAdapter<String> adapter) {
ChatDTO chatDTO = dataSnapshot.getValue(ChatDTO.class);
adapter.remove(chatDTO.getUserName() + " : " + chatDTO.getMessage());
}

private void openChat(String chatName) {
// 리스트 어댑터 생성 및 세팅
final ArrayAdapter<String> adapter

= new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, android.R.id.text1);
chat_view.setAdapter(adapter);

// 데이터 받아오기 및 어댑터 데이터 추가 및 삭제 등..리스너 관리
databaseReference.child("chat").child(chatName).addChildEventListener(new ChildEventListener() {
@Override
public void onChildAdded(DataSnapshot dataSnapshot, String s) {
addMessage(dataSnapshot, adapter);
Log.e("LOG", "s:"+s);
}

@Override
public void onChildChanged(DataSnapshot dataSnapshot, String s) {

}

@Override
public void onChildRemoved(DataSnapshot dataSnapshot) {
removeMessage(dataSnapshot, adapter);
}

@Override
public void onChildMoved(DataSnapshot dataSnapshot, String s) {

}

@Override
public void onCancelled(DatabaseError databaseError) {

}
});
}
}


activity_start.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.weet.weet.StartActivity"
tools:layout_editor_absoluteY="81dp"
tools:layout_editor_absoluteX="0dp">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<LinearLayout
android:layout_weight="1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true">

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20dp"
android:text="채팅방 입장 또는 새로 만들기"
android:layout_marginBottom="20dp"
android:gravity="center"/>
<EditText
android:id="@+id/user_chat"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:inputType="textPersonName"
android:hint="CHAT NAME"
android:layout_gravity="left"
android:gravity="center"
android:layout_marginBottom="20dp"/>

<EditText
android:id="@+id/user_edit"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:inputType="textPersonName"
android:hint="USER NAME"
android:layout_gravity="right"
android:gravity="center"
android:layout_marginBottom="20dp"/>

<Button
android:id="@+id/user_next"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:text="GO"/>

</LinearLayout>

<LinearLayout
android:layout_weight="2"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20dp" android:text="현재 개설된 채팅방"
android:layout_marginBottom="20dp"
android:gravity="center"/>
<ListView
android:id="@+id/chat_list"
android:layout_width="match_parent"
android:layout_height="match_parent">

</ListView>
</LinearLayout>
</LinearLayout>


</RelativeLayout>


activity_chat.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.weet.weet.ChatActivity">


<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<LinearLayout
android:layout_weight="1"
android:layout_width="match_parent"
android:layout_height="match_parent">

<ListView
android:id="@+id/chat_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">

<Button
android:id="@+id/chat_sent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="send" />

<EditText
android:id="@+id/chat_edit"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>

</LinearLayout>

</RelativeLayout>





구현 화면은 3. 본 예제의 목표에 있습니다. 감사합니다.