개발/안드로이드

채팅 UI 화면 구현하기

세크레투스 2025. 4. 20. 20:44
반응형
SMALL

1. 앱의 시작점 : MainActivity.kt

Jetpack Compose 기반으로 채팅 UI를 구성하면서 가장 먼저 세팅해야 할 부분은

앱의 진입점, 즉 MainActivity.kt이다.

이 파일에서는 NavHost를 통해 화면 간의 이동을 구성하고 있다.

package com.housweet.presentation

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.housweet.presentation.ui.chat.ChatScreen
import com.housweet.presentation.ui.chatlist.ChatListScreen

/**
 * 앱 진입 지점 - Compose 기반의 Navigation 구성
 */
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            val navController = rememberNavController()
            NavHost(
                navController = navController,
                startDestination = "chat_list" // 시작 화면은 채팅 목록
            ) {
                composable("chat_list") {
                    ChatListScreen(navController) // 채팅 목록 화면
                }
                composable("chat/{chatName}") { backStackEntry ->
                    val chatName = backStackEntry.arguments?.getString("chatName") ?: "Unknown"
                    ChatScreen(chatName, navController) // 채팅 상세 화면
                }
            }
        }
    }
}

NavHost를 통해 "chat_list"에서 채팅 목록 화면이 시작되고,

채팅 항목을 클릭하면 "chat/{chatName}"으로 이동해 해당 채팅방으로 들어가게 된다.

 

2. 채팅 목록의 데이터 모델 : ChatPreview.kt

채팅 리스트 화면에서는 여러 사람과의 최근 대화를 미리 볼 수 있어야 한다.

이를 위해 간단한 데이터 모델인 ChatPreview를 만들어 사용했다.

package com.housweet.domain.model

// 채팅 리스트에 표시될 항목 모델 정의
data class ChatPreview (
    val name: String,          // 채팅 상대 이름
    val lastMessage: String,   // 마지막 메시지 내용
    val time: String,          // 메시지 시간 (ex. 오전 11:30)
    val unread: Boolean        // 읽지 않은 메시지가 있는지 여부
)

// 더미 데이터를 미리 구성해놓은 리스트 (미리보기용)
val dummyChatList = listOf(
    ChatPreview("김지안", "집을 문의하고 싶어서 연락드렸어요. 지금...", "오전 11:30", true),
    ChatPreview("김지안", "안녕하세요", "오전 11:30", false),
    ChatPreview("김지안", "안녕하세요", "오전 11:30", false)
)

이 데이터 클래스는 채팅 상대의 이름, 마지막 메시지, 시간, 읽지 않은 메시지 여부를 담고 있다.

현재는 더미 데이터를 사용해서 테스트하고 있다.

 

3. 채팅 항목의 UI : ChatListItem.kt

채팅 상대 한 명에 대한 정보를 한 줄로 보여주는 UI 컴포넌트이다.

Row를 사용해 프로필 이미지(임시로 회색 원), 이름, 마지막 메시지, 시간을 나란히 배치했다.

package com.housweet.presentation.ui.chatlist

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.housweet.domain.model.ChatPreview

/**
 * 채팅 목록에서 하나의 채팅 항목을 나타내는 컴포저블
 * @param chat 표시할 채팅 정보
 * @param onClick 항목 클릭 시 실행할 동작 (ex. 채팅방으로 이동)
 */
@Composable
fun ChatListItem(chat: ChatPreview, onClick: () -> Unit) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .clickable { onClick() } // 클릭 시 onClick 콜백 실행
            .padding(16.dp)
    ) {
        // 프로필 이미지 영역 (현재는 회색 원)
        Box(
            modifier = Modifier
                .size(48.dp)
                .background(Color.Gray, CircleShape)
        )

        Spacer(modifier = Modifier.width(12.dp))

        // 이름과 마지막 메시지를 세로 정렬로 표시
        Column(modifier = Modifier.weight(1f)) {
            Text(chat.name, fontWeight = FontWeight.Bold) // 이름
            Text(chat.lastMessage, color = Color.Gray)     // 마지막 메시지
        }

        // 메시지 시간
        Text(chat.time, fontSize = 12.sp, color = Color.Gray)
    }
}

Modifier.clickable을 통해 항목을 탭하면 상세 채팅 화면으로 이동할 수 있도록 구성했다.

 

4. 채팅 목록 화면 : ChatListScreen.kt

ChatListItem을 여러 개 보여주는 리스트 화면이다.

상단에는 TopAppBar로 "채팅"이라는 타이틀을 보여주고,

본문에는 LazyColumn으로 채팅 항목들을 랜더링한다.

package com.housweet.presentation.ui.chatlist

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.runtime.Composable
import androidx.navigation.NavController
import com.housweet.domain.model.dummyChatList

/**
 * 채팅 목록 전체 화면 컴포저블
 * @param navController Navigation을 위한 NavController
 */
@Composable
fun ChatListScreen(navController: NavController) {
    Column {
        // 상단 앱바
        TopAppBar(title = { Text("채팅") })

        // 채팅 리스트를 스크롤 가능한 형태로 렌더링
        LazyColumn {
            itemsIndexed(dummyChatList) { _, chat ->
                ChatListItem(chat = chat, onClick = {
                    navController.navigate("chat/${chat.name}")
                })
            }
        }
    }
}

NavController를 이용해서 채팅방으로 자연스럽게 이동하도록 연결했다.

 

5. 채팅 상세 화면 : ChatScreen.kt

채팅방에 들어왔을 때 보여지는 전체 레이아웃이다. 구성은 다음과 같다:

  • 상단: 상대 이름을 보여주는 TopAppBar
  • 안내 문구: 안전한 거래를 위한 가이드 메시지
  • 날짜 정보
  • 메시지 목록 (ChatBubble로 구성)
  • 입력창 (ChatInput)

말풍선과 입력창은 각각 별도의 컴포저블로 분리해서 재사용성과 가독성을 높였다.

package com.housweet.presentation.ui.chat

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.foundation.lazy.items

/**
 * 채팅 상세 화면 (채팅방)
 * @param chatName 채팅 상대 이름 (타이틀)
 */
@Composable
fun ChatScreen(chatName: String, navController: NavController) {
    // 채팅 메시지 리스트 상태 (초기 더미 메시지 3개)
    var messages by remember {
        mutableStateOf(
            listOf<Pair<String, Boolean>>(
                "안녕하세요" to false,
                "안녕하세요" to true,
                "집 문의하고 싶어서 연락드렸어요. 지금도 메이트 구하시나요?" to false
            )
        )
    }
    
    // 입력창 텍스트 상태
    var inputText by remember { mutableStateOf("") }

    Column(modifier = Modifier.fillMaxSize().background(Color.White)) {

        // 상단 AppBar에 채팅 상대 이름 표시 + 뒤로가기
        TopAppBar(
            title = { Text(chatName) }, //채팅 상대 이름 표시
            backgroundColor = Color.White,
            elevation = 0.dp,
            navigationIcon = {
                Icon(
                    imageVector = Icons.Default.ArrowBack,
                    contentDescription = "뒤로가기",
                    modifier = Modifier
                        .padding(start = 16.dp)
                        .clickable { navController.popBackStack() } // 이전 화면으로 이동
                )
            }
        )

        // 안내 문구 (보안 관련 메시지)
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .background(Color(0xFFF9F9F9))
                .padding(16.dp)
        ) {
            Text("연락처, 주소 등 민감한 개인정보는 채팅을 통해 공유하지 마세요.", color = Color.Gray, fontSize = 12.sp)
            Text("직접 만날 경우, 안전한 공공장소에서 만나시기 바랍니다.", color = Color.Gray, fontSize = 12.sp)
        }

        // 날짜 정보
        Text(
            "3월 8일",
            modifier = Modifier.padding(top = 12.dp).align(Alignment.CenterHorizontally),
            fontSize = 12.sp,
            color = Color.Gray
        )

        // 채팅 메시지 리스트
        LazyColumn(
            modifier = Modifier
                .weight(1f) // 화면에서 가능한 공간을 모두 차지
                .padding(horizontal = 12.dp),
            verticalArrangement = Arrangement.spacedBy(8.dp) // 메시지 사이 간격
        ) {
            // 리스트를 순회하며 ChatBubble을 하나씩 그려줌
            items(items = messages) { (msg, isMine) ->
                ChatBubble(message = msg, isMine = isMine)
            }
        }

        // 메시지 입력창
        ChatInput(
            inputText = inputText,                    // 현재 입력값
            onTextChange = { inputText = it },        // 입력값 변경 시 상태 업데이트
            onSend = {
                if (inputText.isNotBlank()) {
                    messages = messages + Pair(inputText, true) // 메시지 추가
                    inputText = "" // 입력값 초기화
                }
            }
        )
    }
}

 

 

6. 채팅 말풍선 UI : ChatBubble.kt

하단 대화창에 들어가는 말풍선 UI이다.

isMine 플래그를 이용해 왼쪽(상대방), 오른쪽(나)으로 구분해서 정렬되도록 했다.

package com.housweet.presentation.ui.chat

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

/**
 * 하나의 채팅 말풍선을 그리는 컴포저블
 * @param message 메시지 내용
 * @param isMine 내가 보낸 메시지인지 여부
 */
@Composable
fun ChatBubble(message: String, isMine: Boolean) {
    Row(
        modifier = Modifier.fillMaxWidth(),
        horizontalArrangement = if (isMine) Arrangement.End else Arrangement.Start
    ) {
        Box(
            modifier = Modifier
                .background(
                    color = if (isMine) Color(0xFF6F3DD2) else Color(0xFFF2F2F2),
                    shape = RoundedCornerShape(16.dp)
                )
                .padding(horizontal = 14.dp, vertical = 10.dp)
        ) {
            Text(
                text = message,
                color = if (isMine) Color.White else Color.Black,
                fontSize = 14.sp
            )
        }
    }
}
ChatBubble("안녕하세요", isMine = false) // 상대방 메시지
ChatBubble("반가워요", isMine = true)   // 내 메시지

색상도 회색/보라색으로 차별화해서 직관적으로 인식할 수 있게 했다.

 

7. 채팅 입력창 : ChatInput.kt

화면 맨 아래에 위치한 입력창이다.

TextField로 메시지를 입력할 수 있고, 양옆에는 파일 첨부 버튼과 전송 버튼을 배치했다.

현재는 전송 기능은 아직 구현되지 않았지만, UI 구성은 완성된 상태이다.

package com.housweet.presentation.ui.chat

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Send
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

/**
 * 채팅 입력창 컴포저블
 * @param inputText 현재 입력 텍스트
 * @param onTextChange 입력값 변경 콜백
 * @param onSend 전송 버튼 클릭 시 콜백
 */
@Composable
fun ChatInput(
    inputText: String,
    onTextChange: (String) -> Unit,
    onSend: () -> Unit
) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .background(Color.White)
            .padding(8.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {
        // 왼쪽 추가 버튼 (현재는 기능 없음)
        Icon(
            imageVector = Icons.Default.Add,
            contentDescription = "추가",
            modifier = Modifier
                .padding(8.dp)
                .size(24.dp),
            tint = Color.Gray
        )

        // 가운데 입력창
        TextField(
            value = inputText,
            onValueChange = onTextChange,
            placeholder = { Text("메세지 입력") },
            modifier = Modifier
                .weight(1f)
                .padding(horizontal = 8.dp),
            colors = TextFieldDefaults.textFieldColors(
                backgroundColor = Color(0xFFF2F2F2),
                focusedIndicatorColor = Color.Transparent,
                unfocusedIndicatorColor = Color.Transparent
            ),
            shape = RoundedCornerShape(20.dp),
            singleLine = true // 한 줄 입력만 가능
        )

        // 오른쪽 전송 버튼
        Icon(
            imageVector = Icons.Default.Send,
            contentDescription = "보내기",
            modifier = Modifier
                .padding(8.dp)
                .size(24.dp)
                .clickable { onSend() }, // 클릭 시 메시지 전송
            tint = Color.Gray
        )
    }
}

 

마무리

이번 프로젝트는 Jetpack Compose로 구성한 첫 채팅 UI였고, 기능보다는 디자인과 화면 흐름을 위주로 구현해봤다.

추후에는 Socket 통신, 채팅 DB 저장 기능, Firebase 연동 등을 통해 실제 메시지를 주고받을 수 있는 구조로 확장해나갈 예정이다.

반응형
LIST