์บก์คํค 2: ํ ์ผ ์ฑ ํ๋๋ก ๋์๋ณธ Compose ํ์ต ์ฌ์ (์ ์ฒด์ฝ๋)
์ด ์๋ฆฌ์ฆ๋ฅผ 1๋ฒ ๊ธ๋ถํฐ ์ฌ๊ธฐ๊น์ง ์๋ค. @Composable ํ๋ ์ฐ์ด๋ณด๋ ๊ฑธ๋ก ์์ํด์, ์ง๊ธ์ Room์ ์ ์ฅํ๊ณ ๋คํธ์ํฌ๋ก ๊ฐ์ ธ์ค๊ณ ์ ๋๋ฉ์ด์
๋ถ๊ณ ํฐ/ํ๋ธ๋ฆฟ ๋ค ๋์ํ๊ณ ํ
์คํธ๊น์ง ์ง๊ณ Play ์คํ ์ด์ ์ฌ๋ฆด ์ค๋น๊น์ง ๋ ํ ์ผ ์ฑ์ด ๋๋ค.
๋ง์ง๋ง ๊ธ์ด๋๊น ์ฝ๋๋ฅผ ๋ ์ถ๊ฐํ์ง ์๋๋ค. ๋์ ์ด ์ฑ์ ๊ตฌ์กฐ๊ฐ ์ ์ด๋ ๊ฒ ๋ฐ๋์ด์๋์ง๋ฅผ ์ฒ์๋ถํฐ ๋๊น์ง ํ ๋ฒ ์ญ ๋ฐ๋ผ๊ฐ๋ณธ๋ค. ๊ทธ๋ฆฌ๊ณ ์ ์ฒด ์ฝ๋๋ฅผ ํ ๋ฒ์ ๋ชจ์๋๋ค.
์ ์ฒ์๋ถํฐ ์ด ๊ตฌ์กฐ๋ก ์ ์งฐ์๊น
์ฌ์ค ์ง๊ธ ๊ตฌ์กฐ๋ฅผ ์๋ค๋ฉด, ์ฒ์๋ถํฐ ViewModel + Room + Hilt๋ก ๋ค ์งค ์๋ ์์๋ค. ๊ทผ๋ฐ ๊ทธ๋ ๊ฒ ํ์ผ๋ฉด "์ ์ด๊ฒ ํ์ํ์ง"๋ฅผ ๋ชธ์ผ๋ก ๋ชป ๋๊ผ์ ๊ฑฐ๋ค. ์ด ์๋ฆฌ์ฆ๋ ์ผ๋ถ๋ฌ ๋จผ์ ๋ฌธ์ ๋ฅผ ๊ฒช๊ณ , ๊ทธ๋ค์์ ํด๊ฒฐ ๋๊ตฌ๋ฅผ ๋ฐฐ์ฐ๋ ์์๋ก ์งฐ๋ค. ๊ทธ ํ๋ฆ์ ๋ค์ ์ง์ด๋ณด์.
1๋จ๊ณ โ 2๋จ๊ณ: ํ๋ฉด ํ๋์์ ์ฌ๋ฌ ํ๋ฉด์ผ๋ก
1๋ฒ ๊ธ์ @Composable์ ๊ทธ๋ฅ ํจ์๋ค. ํจ์์ธ๋ฐ UI๋ฅผ ๊ทธ๋ฆฐ๋ค๋ ๊ฒ ๋ฏ์ค์์ ๊ฑฐ๋ค. 2๋ฒ ๊ธ์ remember { mutableStateOf(...) }๋ก "์ด ๊ฐ์ด ๋ฐ๋๋ฉด ๋ค์ ๊ทธ๋ ค์ง๋ค"๋ ๋ฆฌ์ปดํฌ์ง์
์ ๊ธฐ๋ณธ์ ๋ฐฐ์ ๊ณ , 3๋ฒ ๊ธ์ rememberSaveable๋ก "ํ๋ฉด ํ์ ํ๋ฉด ์ ๊ฐ์ด ๋ ์๊ฐ๋์ง"๋ฅผ ๊ฒช์๋ค.
5๋ฒ ๊ธ์์ LazyColumn์ key๋ฅผ ์ ์ฐ๊ณ ํญ๋ชฉ์ ์ง์ ์ ๋ ์ฒดํฌ ์ํ๊ฐ ์๋ฑํ ํญ๋ชฉ์ ๋ถ๋ ๋ฒ๊ทธ๋ฅผ ์ง์ ๋ดค๋ค. ์ด๊ฒ 1๋ฒ ๊ธ์ ๋ฆฌ์ปดํฌ์ง์
์๋ฆฌ์ ๋ฐ๋ก ์ฐ๊ฒฐ๋๋ ์ง์ ์ด์๋ค โ Compose๋ "๋ช ๋ฒ์งธ ์ค"์ด ์๋๋ผ "์ด๊ฒ ๊ทธ ํญ๋ชฉ์ธ์ง"๋ฅผ ์์์ผ ํ๋ค๋ ๊ฒ.
6๋ฒ ๊ธ์ Navigation์์ "ํ๋ฉด ๊ฐ์ ๊ฐ์ฒด๋ฅผ ํต์งธ๋ก ๋๊ธฐ์ง ์๊ณ id๋ง ๋๊ธด๋ค"๋ ๊ท์น์ ๋ฐฐ์ ๋๋ฐ, ์ด ๊ท์น์ด ๋์ค์ Room์์ ๊ฑฐ๊พธ๋ก ์ค์ํด์ง๋ค(๋ค์์ ๋ค์ ๋์จ๋ค).
3๋จ๊ณ: ์ํ๋ฅผ ํ ํ๋ฉด ์์ ๋๊ณ ์ง๋ด๋ณธ ๊ฒฐ๊ณผ
7๋ฒ ๊ธ(์บก์คํค 1)์์ ์ผ๋ถ๋ฌ ์ํ๋ฅผ ํ๋ฉด ์ปดํฌ์ ๋ธ ์์ ๋๋ค. TodoApp ํ๋๊ฐ todoList, ๋ค๋น๊ฒ์ด์
, ์
๋ ฅ๊ฐ๊น์ง ๋ค ๋ค๊ณ ์๋ ๊ตฌ์กฐ์๋ค. ๋์๊ฐ๊ธด ํ์ง๋ง ๋ ๊ฐ์ง๊ฐ ๋ถํธํ๋ค.
- ํ๋ฉด ํ์ ํ๋ฉด (
rememberSaveable์ ์ ์ด ๊ฐ์) ๋ ์๊ฐ๋ค - ๋ก์ง ํ ์คํธ๋ฅผ ํ๋ ค๋ฉด ํ๋ฉด์ ๋์์ผ ํ๋ค
์ด ๋ถํธํจ์ ๊ฒช๊ณ ๋์์ผ 8๋ฒ ๊ธ ์ํ ํธ์ด์คํ , 9๋ฒ ๊ธ ViewModel์ด "์ ํ์ํ์ง"๊ฐ ๋ฉ๋์ด ๋๋ค. ์์๋ฅผ ๋ฐ๊ฟ์ ViewModel์ ๋จผ์ ๋ฐฐ์ ๋ค๋ฉด "๊ทธ๋ฅ ์ด๋ ๊ฒ ์ฐ๋ผ๋๊น ์ฐ๋" ์๊ธฐ๊ฐ ๋์ ๊ฑฐ๋ค.
4๋จ๊ณ: ์ํ ๊ด๋ฆฌ์ ๊ตฌ์กฐ
8๋ฒ ๊ธ์์ TodoListScreen์ value + onValueChange(์ฌ๊ธฐ์๋ todoList + onAdd/onToggleDone/onDelete) ํจํด์ผ๋ก statelessํ๊ฒ ๋ง๋ค์๋ค. ์ด๊ฒ ๋์ค์ 20๋ฒ ๊ธ UI ํ
์คํธ์์ "ViewModel ์์ด ํ๋ฉด๋ง ํ
์คํธ"๊ฐ ๊ฐ๋ฅํ๋ ์ด์ ๋ค. ๊ทธ๋๋ ํ
์คํธ๋ฅผ ์ผ๋์ ๋๊ณ ์ง ๊ฒ ์๋๋ผ "ํ๋ฉด๊ณผ ๋ก์ง์ ๋ถ๋ฆฌํ๋ฉด ์ฌ์ฌ์ฉํ๊ธฐ ์ข๋ค"๋ ์ด์ ์๋๋ฐ, ๊ฒฐ๊ตญ ํ
์คํธํ๊ธฐ ์ข์ ๊ตฌ์กฐ์ ๊ฐ์ ๊ตฌ์กฐ์๋ค.
9๋ฒ ๊ธ ViewModel, 10๋ฒ ๊ธ StateFlow/UiState๋ฅผ ๊ฑฐ์ณ 11๋ฒ ๊ธ์์ ์บก์คํค 1์ ๋ฆฌํฉํ ๋งํ๋ค. data class TodoUiState(todoList, isLoading, errorMessage) ํ๋๋ก ํ๋ฉด์ ๋ชจ๋ ์ํ๋ฅผ ํํํ๋ ํจํด์ด ์ฌ๊ธฐ์ ์๋ฆฌ์ก์๊ณ , ์ดํ 12~15๋ฒ ๊ธ ๋ด๋ด ๊ทธ๋๋ก ์ ์ง๋๋ค.
5๋จ๊ณ: ๋ฐ์ดํฐ โ ์ ์ฅ ๋ฐฉ์์ด ๋์ด๋๋ UiState๋ ๊ทธ๋๋ก
12๋ฒ ๊ธ Room์ ๋ถ์ด๋ฉด์ Todo๊ฐ @Entity๊ฐ ๋๋ค. ์ฌ๊ธฐ์ 6๋ฒ ๊ธ์ "id๋ง ๋๊ธด๋ค"๋ ๊ท์น์ด ๋ค์ ๋์จ๋ค โ Room์ @Update/@Delete๋ primary key๋ก ํ์ ์ฐพ๊ธฐ ๋๋ฌธ์, ์ด๋ฒ์ ๋ฐ๋๋ก ๊ฐ์ฒด ์ ์ฒด(๋๋ ์ต์ํ id๊ฐ ํฌํจ๋ ์ ์ฒด)๋ฅผ ๋๊ฒจ์ผ ์ ๋๋ก ์์ /์ญ์ ๊ฐ ๋๋ค๋ ๊ฑธ ๋ฐฐ์ ๋ค. 6๋ฒ ๊ธ๊ณผ ์ ๋ฐ๋ ๊ฒฐ๋ก ์ฒ๋ผ ๋ณด์ด์ง๋ง, ์ค์ ๋ก๋ "๊ฐ์ ์ง๋ฌธ(๋ญ ๊ธฐ์ค์ผ๋ก ๊ทธ ํญ๋ชฉ์ ์ฐพ๋๊ฐ)์ ์ํฉ๋ง๋ค ๋ค๋ฅธ ๋ต"์ด๋ผ๋ ๊ฑธ ๋ณด์ฌ์ฃผ๋ ์ข์ ๋์กฐ์๋ค.
13๋ฒ ๊ธ DataStore๋ก ๋คํฌ ๋ชจ๋ ์ค์ ์ ์ ์ฅํ๊ณ , 14๋ฒ ๊ธ Retrofit์ผ๋ก ์๋ฒ์์ ํ ์ผ์ ๊ฐ์ ธ์ค๋ ๊ธฐ๋ฅ์ ๋ถ์๋ค. ์ด๋ UiState์ isLoading/errorMessage๊ฐ ์ ๋ฏธ๋ฆฌ ๋ค์ด์์๋์ง(10๋ฒ ๊ธ์์ ๋ง๋ค์์ ๋๋ ์ธ ๋ฐ๊ฐ ์์๋ค)๊ฐ ๋๋์ด ์ฐ์ธ๋ค.
15๋ฒ ๊ธ Hilt๋ก TodoRepository(Room + Retrofit์ ๊ฐ์ผ)๋ฅผ ViewModel์ ์ฃผ์
ํ๋ค. @HiltViewModel + hiltViewModel()๋ก, ์ด์ ViewModel์ด Repository๋ฅผ ์ง์ newํ์ง ์๋๋ค. ์ด ๊ตฌ์กฐ๊ฐ 20๋ฒ ๊ธ์์ Repository๋ฅผ ์ธํฐํ์ด์ค๋ก ๋ฝ์ Fake๋ก ๋ฐ๊ฟ ๋ผ์ธ ์ ์์๋ ์ ์ ์กฐ๊ฑด์ด์๋ค.
6๋จ๊ณ: ์ฌํ โ ์๋ ๊ตฌ์กฐ๋ฅผ ๊ฑด๋๋ฆฌ์ง ์๊ณ ๋ค๋ฌ๊ธฐ
16๋ฒ ๊ธ LaunchedEffect/DisposableEffect, 17๋ฒ ๊ธ ์ ๋๋ฉ์ด์
, 18๋ฒ ๊ธ ์ฑ๋ฅ ์ต์ ํ(derivedStateOf) ๋ชจ๋ ์ง๊ธ๊น์ง์ ๊ตฌ์กฐ(ViewModel + UiState + stateless ํ๋ฉด)๋ฅผ ๋ฐ๊พธ์ง ์๊ณ ๊ทธ ์์ ์น์๋ค. ๊ตฌ์กฐ๋ฅผ ํ ๋ฒ ์ ๋๋ก ์ก์๋๋ฉด, ๊ทธ ๋ค๋ก๋ ๊ธฐ๋ฅ์ "์ถ๊ฐ"ํ๋ ์์
์ด "๋ค์ ์ค๊ณ"ํ๋ ์์
์ด ์ ๋๋ค๋ ๊ฑธ ๋ณด์ฌ์ฃผ๋ ๊ตฌ๊ฐ์ด์๋ค.
19๋ฒ ๊ธ WindowSizeClass๋ก PhoneLayout/TabletLayout์ ๋๋ ๋๋ ๋ง์ฐฌ๊ฐ์ง์๋ค. TodoListScreen, TodoDetailScreen์ด stateless๋ผ์, ๋ ๋ ์ด์์์ด ๊ทธ ํ๋ฉด๋ค์ ๊ทธ๋ฅ ์ฌ์ฌ์ฉ๋ง ํ๋ฉด ๋๋ค.
7๋จ๊ณ: ํ ์คํธ์ ๋ฐฐํฌ โ ๊ตฌ์กฐ๊ฐ ์ข์๋์ง ํ์ธ๋ฐ๋ ์๊ฐ
20๋ฒ ๊ธ์์ ํ ์คํธ๋ฅผ ์ง๋ณด๋ฉด์, ์ง๊ธ๊น์ง์ ์ค๊ณ๊ฐ "๋ง๋ก๋ ์ข๋ค"๋ ๊ฒ ์๋๋ผ ์ค์ ๋ก ํ ์คํธํ๊ธฐ ์ฌ์ด ๊ตฌ์กฐ์๋์ง ํ์ธ๋ฐ์๋ค. stateless ํ๋ฉด + ์ธํฐํ์ด์ค๋ก ๋ถ๋ฆฌ๋ Repository ๋๋ถ์ ViewModel ํ ์คํธ์ UI ํ ์คํธ๊ฐ ๋ ๋ค ์ด๋ ต์ง ์๊ฒ ๋๋ค.
21๋ฒ ๊ธ์์ ์๋ช ํค๋ฅผ ๋ง๋ค๊ณ AAB๋ฅผ ๋น๋ํ๋ ๋ฒ์ ๋ดค๋ค. ์ด๊ฑด ์ฝ๋ ๊ตฌ์กฐ์๋ ๋ณ๊ฐ๋ก, "๋ง๋ ๊ฑธ ๋จ์๊ฒ ์ ๋ฌํ๋ ์ ์ฐจ"๋ฅผ ์์์ผ ํ๋ค๋ ์ค์ฉ์ ์ธ ๊ธ์ด์๋ค.
์ ์ฒด์ฝ๋: todo_project ์ต์ข ๊ตฌ์กฐ
์ง๊ธ๊น์ง ํฉ์ด์ ธ ์๋ ์ฝ๋๋ฅผ ํ ๋ฐ ๋ชจ์ผ๋ฉด ๋๋ต ์ด๋ฐ ๋ชจ์ต์ด๋ค. (๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ฒ์ , Hilt ๋ชจ๋ ๋ฑ ์ธ๋ถ๋ ๊ฐ ๊ธ์ ์ฐธ๊ณ . ์ฌ๊ธฐ์๋ ๊ตฌ์กฐ์ ํ๋ฆ๋ง ๋ณด๊ธฐ ์ํ ์์ฝ์ด๋ค.)
// data/Todo.kt
@Entity
data class Todo(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val title: String,
val isDone: Boolean = false
)
// data/TodoDao.kt
@Dao
interface TodoDao {
@Query("SELECT * FROM Todo")
fun getAll(): Flow<List<Todo>>
@Insert suspend fun insert(todo: Todo)
@Update suspend fun update(todo: Todo)
@Delete suspend fun delete(todo: Todo)
}
// data/TodoDatabase.kt
@Database(entities = [Todo::class], version = 1)
abstract class TodoDatabase : RoomDatabase() {
abstract fun todoDao(): TodoDao
}
// data/TodoRepository.kt โ ์ธํฐํ์ด์ค๋ก ๋ฝ์์ 20๋ฒ ๊ธ์์ Fake๋ก ๊ต์ฒด ๊ฐ๋ฅํ๊ฒ ํจ
interface TodoRepository {
val todoList: Flow<List<Todo>>
suspend fun addTodo(title: String)
suspend fun toggleDone(todo: Todo)
suspend fun deleteTodo(todo: Todo)
suspend fun importFromServer()
}
class TodoRepositoryImpl @Inject constructor(
private val dao: TodoDao,
private val apiService: TodoApiService
) : TodoRepository {
override val todoList: Flow<List<Todo>> = dao.getAll()
override suspend fun addTodo(title: String) = dao.insert(Todo(title = title))
override suspend fun toggleDone(todo: Todo) = dao.update(todo.copy(isDone = !todo.isDone))
override suspend fun deleteTodo(todo: Todo) = dao.delete(todo)
override suspend fun importFromServer() {
val remoteTodos = apiService.getTodos()
remoteTodos.forEach { dao.insert(Todo(title = it.title)) }
}
}
// ui/TodoUiState.kt
data class TodoUiState(
val todoList: List<Todo> = emptyList(),
val isLoading: Boolean = false,
val errorMessage: String? = null
)
// ui/TodoViewModel.kt
@HiltViewModel
class TodoViewModel @Inject constructor(
private val repository: TodoRepository
) : ViewModel() {
val uiState: StateFlow<TodoUiState> = repository.todoList
.map { TodoUiState(todoList = it) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), TodoUiState(isLoading = true))
fun addTodo(title: String) = viewModelScope.launch { repository.addTodo(title) }
fun toggleDone(todo: Todo) = viewModelScope.launch { repository.toggleDone(todo) }
fun deleteTodo(todo: Todo) = viewModelScope.launch { repository.deleteTodo(todo) }
fun importFromServer() = viewModelScope.launch { repository.importFromServer() }
}
// ui/TodoApp.kt โ ํฐ/ํ๋ธ๋ฆฟ ๋ถ๊ธฐ + ๋ค๋น๊ฒ์ด์
@Composable
fun TodoApp(viewModel: TodoViewModel = hiltViewModel(), windowSizeClass: WindowSizeClass) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
if (windowSizeClass.isWidthAtLeastBreakpoint(WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND)) {
TabletLayout(uiState, viewModel::addTodo, viewModel::toggleDone, viewModel::deleteTodo)
} else {
PhoneLayout(uiState, viewModel::addTodo, viewModel::toggleDone, viewModel::deleteTodo)
}
}
// ui/TodoListScreen.kt โ stateless, ํ
์คํธ์ ์ฌ์ฌ์ฉ์ด ์ฌ์ด ํต์ฌ ํ๋ฉด
@Composable
fun TodoListScreen(
todoList: List<Todo>,
onAdd: (String) -> Unit,
onToggleDone: (Todo) -> Unit,
onDelete: (Todo) -> Unit,
onTodoClick: (Todo) -> Unit
) {
// ... (5๋ฒ, 8๋ฒ ๊ธ์์ ๋ค๋ฃฌ LazyColumn + key, ์ํ ํธ์ด์คํ
)
}์ด ์ฑ์ด ์์ผ๋ก ๋ ์ปค์ง๋ค๋ฉด
์ด ๊ตฌ์กฐ๋ ์์ ํ ์ผ ์ฑ ํ๋์๋ ๋๋ํ๋ค. ๋ ์ปค์ง๋ค๋ฉด ๋ณดํต ์ด๋ฐ ๋ฐฉํฅ์ผ๋ก ๊ฐ๋ค.
- ํ๋ฉด/๊ธฐ๋ฅ์ด ๋ง์์ง๋ฉด ๋ชจ๋์ ์ชผ๊ฐ ๋ค (
:feature:list,:feature:detail๊ฐ์ ๋ฉํฐ ๋ชจ๋) - ์๋ฒ์ ๋ก์ปฌ DB๋ฅผ ๋๊ธฐํํ๋ ๋ก์ง์ด ๋ณต์กํด์ง๋ฉด Repository์ "์คํ๋ผ์ธ ์ฐ์ " ์ ๋ต์ ์ถ๊ฐํ๋ค
- ํ ์คํธ๊ฐ ๋์ด๋๋ฉด Fake๋ฅผ ๋งค๋ฒ ์์ผ๋ก ๋ง๋ค์ง ์๊ณ ํ ์คํธ ์ ์ฉ ๋ผ์ด๋ธ๋ฌ๋ฆฌ(MockK ๋ฑ)๋ฅผ ๋์ ํ๋ค
์ง๊ธ ๋น์ฅ ํ ์ผ์ ์๋๋ค. ์ง๊ธ ๊ตฌ์กฐ์์ ์์ฐ์ค๋ฝ๊ฒ ํ์ฅ๋๋ ๋ฐฉํฅ์ด๋ผ๋ ๊ฒ๋ง ์์๋๋ฉด ๋๋ค.
์ ๋ฆฌ
- ์ด ์๋ฆฌ์ฆ๋ "๋ฌธ์ ๋ฅผ ๋จผ์ ๊ฒช๊ณ ๋๊ตฌ๋ฅผ ๋์ค์ ๋ฐฐ์ฐ๋" ์์๋ก ์งฐ๋ค. 1~7๋ฒ์์ ๋ถํธํจ์ ๊ฒช์๊ณ , 8๋ฒ๋ถํฐ ๊ทธ ๋ถํธํจ์ ํ๋์ฉ ํด๊ฒฐํ๋ ๋๊ตฌ๋ค์ด ๋์๋ค.
TodoUiStateํ๋๋ก ๋ก๋ฉ/์๋ฌ/๋ฐ์ดํฐ๋ฅผ ํํํ๋ ํจํด์ 10๋ฒ ๊ธ์์ ๋ง๋ค์ด์ 15๋ฒ ๊ธ๊น์ง ๊ทธ๋๋ก ์ผ๋ค.- stateless ํ๋ฉด + ์ธํฐํ์ด์ค Repository๋ผ๋ ๋ ๊ฐ์ง ์ ํ์ด, ๋์ค์(20๋ฒ ๊ธ) ํ ์คํธ๋ฅผ ์ฝ๊ฒ ๋ง๋ค์ด์ค ํต์ฌ์ด์๋ค.
- ๊ตฌ์กฐ๋ฅผ ํ ๋ฒ ์ ๋๋ก ์ก์ผ๋ฉด ์ดํ ๊ธฐ๋ฅ ์ถ๊ฐ(์ ๋๋ฉ์ด์ , ์ ์ํ UI)๋ ๊ตฌ์กฐ๋ฅผ ์ ๊ฑด๋๋ฆฌ๊ณ ์น์ ์ ์๋ค.
- ์ฝ๋์ ๊ตฌ์กฐ๊ฐ ๋ค ๋ผ๋ ์๋ช ํค, AAB, Play Console ๋ฑ๋ก ๊ฐ์ "์ ๋ฌํ๋ ์ ์ฐจ"๊ฐ ๋ฐ๋ก ์๋ค๋ ๊ฒ๋ 21๋ฒ ๊ธ์์ ๋ดค๋ค.
์ฌ๊ธฐ๊น์ง๊ฐ 22ํธ์ง๋ฆฌ Compose ์๋ฆฌ์ฆ ์ ์ฒด๋ค. ๋ค์์ ๋ ๋ค๋ฅธ ์ฃผ์ ๋ก ์ ์๋ฆฌ์ฆ๋ฅผ ์์ํ ์ฐจ๋ก๋ค.
