22 minute read

๐ŸŽฏ Best Practices ์š”์•ฝ

๐Ÿ’ก ์„ฑ๋Šฅ ์ตœ์ ํ™”

  • remember ์‚ฌ์šฉ์œผ๋กœ ๋ถˆํ•„์š”ํ•œ ์žฌ๊ณ„์‚ฐ ๋ฐฉ์ง€
  • LazyColumn/LazyRow ์‚ฌ์šฉ์œผ๋กœ ๋Œ€์šฉ๋Ÿ‰ ๋ฆฌ์ŠคํŠธ ์ตœ์ ํ™”
  • key ํŒŒ๋ผ๋ฏธํ„ฐ ์‚ฌ์šฉ์œผ๋กœ recomposition ์ตœ์ ํ™”

๐Ÿ”ง ์ฝ”๋“œ ํ’ˆ์งˆ

  • Composable ํ•จ์ˆ˜ ๋ถ„๋ฆฌ๋กœ ์žฌ์‚ฌ์šฉ์„ฑ ํ–ฅ์ƒ
  • State hoisting์œผ๋กœ ์ƒํƒœ ๊ด€๋ฆฌ ์ตœ์ ํ™”
  • testTag ์‚ฌ์šฉ์œผ๋กœ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•œ UI ๊ตฌ์„ฑ

๐ŸŒŸ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜

  • Loading states๋กœ ์‚ฌ์šฉ์ž ํ”ผ๋“œ๋ฐฑ ์ œ๊ณต
  • Error handling์œผ๋กœ ์˜ค๋ฅ˜ ์ƒํ™ฉ ๋Œ€์‘
  • Accessibility๋ฅผ ๊ณ ๋ คํ•œ UI ์„ค๊ณ„

๋ชฉ์ฐจ

๐ŸŒ ๋„คํŠธ์›Œํฌ ํ†ต์‹ ๊ณผ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ

๐ŸŽจ ํ…Œ๋งˆ์™€ ์Šคํƒ€์ผ๋ง

๐Ÿš€ ์„ฑ๋Šฅ Recomposition ์ตœ์ ํ™”

๐Ÿงช ํ…Œ์ŠคํŠธ

๐Ÿ’ก ์ฝ”๋“œ ๊ตฌ์กฐํ™”

๐ŸŒ ๋„คํŠธ์›Œํฌ ํ†ต์‹ ๊ณผ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ

๐Ÿ”— HTTP ํ†ต์‹ 

HTTP ํ†ต์‹ ์€ ์„œ๋ฒ„์™€ ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ณ ๋ฐ›๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค. Axios๋‚˜ Fetch API์™€ ์œ ์‚ฌํ•œ ๊ฐœ๋…์ž…๋‹ˆ๋‹ค.

import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import java.net.HttpURLConnection
import java.net.URL

/**
 * ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค
 */
data class User(
    val id: Int,
    val name: String,
    val email: String,
    val phone: String
)

/**
 * UI ์ƒํƒœ ๊ด€๋ฆฌ
 * Redux์˜ State์™€ ์œ ์‚ฌํ•œ ๊ฐœ๋…
 */
sealed class UiState {
    object Loading : UiState()
    data class Success(val users: List<User>) : UiState()
    data class Error(val message: String) : UiState()
}

/**
 * ๋„คํŠธ์›Œํฌ ํ†ต์‹  ViewModel
 * React์˜ Custom Hook์ด๋‚˜ Redux Thunk์™€ ์œ ์‚ฌ
 */
class NetworkViewModel : ViewModel() {
    // StateFlow๋Š” React์˜ state์™€ ์œ ์‚ฌ
    private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
    val uiState: StateFlow<UiState> = _uiState.asStateFlow()
    
    init {
        // ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ ๋กœ๋“œ
        loadUsers()
    }
    
    /**
     * ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ๋กœ๋“œ
     * Axios๋‚˜ Fetch API์™€ ์œ ์‚ฌํ•œ ๊ธฐ๋Šฅ
     */
    fun loadUsers() {
        viewModelScope.launch {
            _uiState.value = UiState.Loading
            
            try {
                // ์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” Retrofit์ด๋‚˜ Ktor ์‚ฌ์šฉ
                val users = fetchUsersFromApi()
                _uiState.value = UiState.Success(users)
            } catch (e: Exception) {
                _uiState.value = UiState.Error("๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹คํŒจ: ${e.message}")
            }
        }
    }
    
    /**
     * ๋ชจ์˜ API ํ˜ธ์ถœ
     * ์‹ค์ œ๋กœ๋Š” Retrofit์ด๋‚˜ Ktor ์‚ฌ์šฉ
     */
    private suspend fun fetchUsersFromApi(): List<User> {
        // ๋„คํŠธ์›Œํฌ ์ง€์—ฐ ์‹œ๋ฎฌ๋ ˆ์ด์…˜
        kotlinx.coroutines.delay(2000)
        
        // ๋ชจ์˜ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜
        return listOf(
            User(1, "ํ™๊ธธ๋™", "hong@example.com", "010-1234-5678"),
            User(2, "๊น€์ฒ ์ˆ˜", "kim@example.com", "010-2345-6789"),
            User(3, "์ด์˜ํฌ", "lee@example.com", "010-3456-7890"),
            User(4, "๋ฐ•์ง€๋ฏผ", "park@example.com", "010-4567-8901"),
            User(5, "์ •์ˆ˜๋นˆ", "jung@example.com", "010-5678-9012")
        )
    }
}

/**
 * ๋„คํŠธ์›Œํฌ ํ†ต์‹  ํ™”๋ฉด
 * React์˜ App Component์™€ ์œ ์‚ฌ
 */
@Composable
fun NetworkScreen() {
    val viewModel: NetworkViewModel = viewModel()
    val uiState by viewModel.uiState.collectAsState()
    
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        // ํ—ค๋”
        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically
        ) {
            Text(
                text = "๐Ÿ‘ฅ ์‚ฌ์šฉ์ž ๋ชฉ๋ก",
                style = MaterialTheme.typography.headlineLarge
            )
            
            IconButton(
                onClick = { viewModel.loadUsers() }
            ) {
                Icon(
                    Icons.Default.Refresh,
                    contentDescription = "์ƒˆ๋กœ๊ณ ์นจ"
                )
            }
        }
        
        Spacer(modifier = Modifier.height(16.dp))
        
        // UI ์ƒํƒœ์— ๋”ฐ๋ฅธ ํ™”๋ฉด ํ‘œ์‹œ
        // React์˜ conditional rendering๊ณผ ์œ ์‚ฌ
        when (uiState) {
            is UiState.Loading -> {
                // ๋กœ๋”ฉ ์ƒํƒœ
                Box(
                    modifier = Modifier.fillMaxSize(),
                    contentAlignment = Alignment.Center
                ) {
                    Column(
                        horizontalAlignment = Alignment.CenterHorizontally
                    ) {
                        CircularProgressIndicator()
                        Spacer(modifier = Modifier.height(16.dp))
                        Text("๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘...")
                    }
                }
            }
            
            is UiState.Success -> {
                // ์„ฑ๊ณต ์ƒํƒœ
                LazyColumn(
                    verticalArrangement = Arrangement.spacedBy(8.dp)
                ) {
                    items(uiState.users) { user ->
                        UserCard(user = user)
                    }
                }
            }
            
            is UiState.Error -> {
                // ์—๋Ÿฌ ์ƒํƒœ
                Card(
                    modifier = Modifier.fillMaxWidth(),
                    colors = CardDefaults.cardColors(
                        containerColor = MaterialTheme.colorScheme.errorContainer
                    )
                ) {
                    Column(
                        modifier = Modifier.padding(16.dp)
                    ) {
                        Text(
                            text = "โŒ ์˜ค๋ฅ˜ ๋ฐœ์ƒ",
                            style = MaterialTheme.typography.headlineMedium,
                            color = MaterialTheme.colorScheme.onErrorContainer
                        )
                        
                        Spacer(modifier = Modifier.height(8.dp))
                        
                        Text(
                            text = uiState.message,
                            color = MaterialTheme.colorScheme.onErrorContainer
                        )
                        
                        Spacer(modifier = Modifier.height(16.dp))
                        
                        Button(
                            onClick = { viewModel.loadUsers() }
                        ) {
                            Text("๋‹ค์‹œ ์‹œ๋„")
                        }
                    }
                }
            }
        }
    }
}

/**
 * ์‚ฌ์šฉ์ž ์นด๋“œ ์ปดํฌ๋„ŒํŠธ
 * React์˜ UserCard Component์™€ ์œ ์‚ฌ
 */
@Composable
fun UserCard(user: User) {
    Card(
        modifier = Modifier.fillMaxWidth(),
        elevation = CardDefaults.cardElevation(4.dp)
    ) {
        Column(
            modifier = Modifier.padding(16.dp)
        ) {
            Text(
                text = user.name,
                style = MaterialTheme.typography.headlineSmall
            )
            
            Spacer(modifier = Modifier.height(4.dp))
            
            Text(
                text = "๐Ÿ“ง ${user.email}",
                color = MaterialTheme.colorScheme.onSurfaceVariant
            )
            
            Text(
                text = "๐Ÿ“ฑ ${user.phone}",
                color = MaterialTheme.colorScheme.onSurfaceVariant
            )
        }
    }
}

๐ŸŽจ ํ…Œ๋งˆ์™€ ์Šคํƒ€์ผ๋ง

๐ŸŽญ Material Design ํ…Œ๋งˆ

Material Design์€ Google์˜ design language์ž…๋‹ˆ๋‹ค. Bootstrap์ด๋‚˜ Ant Design๊ณผ ์œ ์‚ฌํ•œ design system์ž…๋‹ˆ๋‹ค.

import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

/**
 * ์ปค์Šคํ…€ ์ƒ‰์ƒ ํŒ”๋ ˆํŠธ
 * CSS Variables๋‚˜ SCSS Variables์™€ ์œ ์‚ฌ
 */
private val LightColorScheme = lightColorScheme(
    primary = Color(0xFF1976D2),
    onPrimary = Color.White,
    primaryContainer = Color(0xFFBBDEFB),
    onPrimaryContainer = Color(0xFF0D47A1),
    secondary = Color(0xFF03DAC6),
    onSecondary = Color.Black,
    tertiary = Color(0xFF018786),
    background = Color(0xFFFFFBFE),
    surface = Color(0xFFFFFBFE),
    onBackground = Color(0xFF1C1B1F),
    onSurface = Color(0xFF1C1B1F),
    error = Color(0xFFBA1A1A),
    onError = Color.White
)

private val DarkColorScheme = darkColorScheme(
    primary = Color(0xFF90CAF9),
    onPrimary = Color(0xFF0D47A1),
    primaryContainer = Color(0xFF1565C0),
    onPrimaryContainer = Color(0xFFE3F2FD),
    secondary = Color(0xFF03DAC6),
    onSecondary = Color.Black,
    tertiary = Color(0xFF66FFF9),
    background = Color(0xFF1C1B1F),
    surface = Color(0xFF1C1B1F),
    onBackground = Color(0xFFE6E1E5),
    onSurface = Color(0xFFE6E1E5),
    error = Color(0xFFFFB4AB),
    onError = Color(0xFF690005)
)

/**
 * ์ปค์Šคํ…€ ํƒ€์ดํฌ๊ทธ๋ž˜ํ”ผ
 * CSS Font Family์™€ ์œ ์‚ฌ
 */
private val CustomTypography = Typography(
    displayLarge = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Normal,
        fontSize = 57.sp,
        lineHeight = 64.sp,
        letterSpacing = (-0.25).sp
    ),
    headlineLarge = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Bold,
        fontSize = 32.sp,
        lineHeight = 40.sp,
        letterSpacing = 0.sp
    ),
    titleLarge = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Medium,
        fontSize = 22.sp,
        lineHeight = 28.sp,
        letterSpacing = 0.sp
    ),
    bodyLarge = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp,
        lineHeight = 24.sp,
        letterSpacing = 0.5.sp
    ),
    labelLarge = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Medium,
        fontSize = 14.sp,
        lineHeight = 20.sp,
        letterSpacing = 0.1.sp
    )
)

/**
 * ์ปค์Šคํ…€ Shapes
 * CSS border-radius์™€ ์œ ์‚ฌ
 */
private val CustomShapes = Shapes(
    small = RoundedCornerShape(4.dp),
    medium = RoundedCornerShape(8.dp),
    large = RoundedCornerShape(16.dp)
)

/**
 * ์•ฑ ํ…Œ๋งˆ ์„ค์ •
 * CSS Global Styles๋‚˜ styled-components์˜ ThemeProvider์™€ ์œ ์‚ฌ
 */
@Composable
fun CustomTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val colorScheme = if (darkTheme) {
        DarkColorScheme
    } else {
        LightColorScheme
    }
    
    MaterialTheme(
        colorScheme = colorScheme,
        typography = CustomTypography,
        shapes = CustomShapes,
        content = content
    )
}

/**
 * ํ…Œ๋งˆ ๋ฐ๋ชจ ํ™”๋ฉด
 * ๋‹ค์–‘ํ•œ Material Design ์ปดํฌ๋„ŒํŠธ ์Šคํƒ€์ผ๋ง ์˜ˆ์ œ
 */
@Composable
fun ThemeDemo() {
    var isDarkMode by remember { mutableStateOf(false) }
    
    CustomTheme(darkTheme = isDarkMode) {
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(16.dp),
            verticalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            // ํ…Œ๋งˆ ํ† ๊ธ€ ํ—ค๋”
            Row(
                modifier = Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.SpaceBetween,
                verticalAlignment = Alignment.CenterVertically
            ) {
                Text(
                    text = "๐ŸŽจ ํ…Œ๋งˆ ์˜ˆ์ œ",
                    style = MaterialTheme.typography.headlineLarge
                )
                
                Row(
                    verticalAlignment = Alignment.CenterVertically,
                    horizontalArrangement = Arrangement.spacedBy(8.dp)
                ) {
                    Text("๐ŸŒž")
                    Switch(
                        checked = isDarkMode,
                        onCheckedChange = { isDarkMode = it }
                    )
                    Text("๐ŸŒ™")
                }
            }
            
            // ์ƒ‰์ƒ ํŒ”๋ ˆํŠธ ํ‘œ์‹œ
            Card(
                modifier = Modifier.fillMaxWidth(),
                elevation = CardDefaults.cardElevation(4.dp)
            ) {
                Column(
                    modifier = Modifier.padding(16.dp)
                ) {
                    Text(
                        text = "์ƒ‰์ƒ ํŒ”๋ ˆํŠธ",
                        style = MaterialTheme.typography.titleLarge
                    )
                    
                    Spacer(modifier = Modifier.height(12.dp))
                    
                    // Primary ์ƒ‰์ƒ
                    ColorSwatch(
                        color = MaterialTheme.colorScheme.primary,
                        name = "Primary",
                        textColor = MaterialTheme.colorScheme.onPrimary
                    )
                    
                    Spacer(modifier = Modifier.height(8.dp))
                    
                    // Secondary ์ƒ‰์ƒ
                    ColorSwatch(
                        color = MaterialTheme.colorScheme.secondary,
                        name = "Secondary",
                        textColor = MaterialTheme.colorScheme.onSecondary
                    )
                    
                    Spacer(modifier = Modifier.height(8.dp))
                    
                    // Surface ์ƒ‰์ƒ
                    ColorSwatch(
                        color = MaterialTheme.colorScheme.surface,
                        name = "Surface",
                        textColor = MaterialTheme.colorScheme.onSurface
                    )
                }
            }
            
            // ํƒ€์ดํฌ๊ทธ๋ž˜ํ”ผ ์˜ˆ์ œ
            Card(
                modifier = Modifier.fillMaxWidth(),
                elevation = CardDefaults.cardElevation(4.dp)
            ) {
                Column(
                    modifier = Modifier.padding(16.dp)
                ) {
                    Text(
                        text = "ํƒ€์ดํฌ๊ทธ๋ž˜ํ”ผ",
                        style = MaterialTheme.typography.titleLarge
                    )
                    
                    Spacer(modifier = Modifier.height(12.dp))
                    
                    Text(
                        text = "Display Large",
                        style = MaterialTheme.typography.displayLarge
                    )
                    
                    Text(
                        text = "Headline Large",
                        style = MaterialTheme.typography.headlineLarge
                    )
                    
                    Text(
                        text = "Title Large",
                        style = MaterialTheme.typography.titleLarge
                    )
                    
                    Text(
                        text = "Body Large - ๋ณธ๋ฌธ ํ…์ŠคํŠธ์ž…๋‹ˆ๋‹ค.",
                        style = MaterialTheme.typography.bodyLarge
                    )
                    
                    Text(
                        text = "Label Large",
                        style = MaterialTheme.typography.labelLarge
                    )
                }
            }
            
            // ์ปดํฌ๋„ŒํŠธ ์Šคํƒ€์ผ๋ง ์˜ˆ์ œ
            Card(
                modifier = Modifier.fillMaxWidth(),
                elevation = CardDefaults.cardElevation(4.dp)
            ) {
                Column(
                    modifier = Modifier.padding(16.dp),
                    verticalArrangement = Arrangement.spacedBy(12.dp)
                ) {
                    Text(
                        text = "์ปดํฌ๋„ŒํŠธ ์Šคํƒ€์ผ๋ง",
                        style = MaterialTheme.typography.titleLarge
                    )
                    
                    // ๋‹ค์–‘ํ•œ ๋ฒ„ํŠผ ์Šคํƒ€์ผ
                    Row(
                        horizontalArrangement = Arrangement.spacedBy(8.dp)
                    ) {
                        Button(
                            onClick = { /* ์•ก์…˜ */ }
                        ) {
                            Text("Primary")
                        }
                        
                        OutlinedButton(
                            onClick = { /* ์•ก์…˜ */ }
                        ) {
                            Text("Outlined")
                        }
                        
                        TextButton(
                            onClick = { /* ์•ก์…˜ */ }
                        ) {
                            Text("Text")
                        }
                    }
                    
                    // ์ปค์Šคํ…€ ์Šคํƒ€์ผ ๋ฒ„ํŠผ
                    Button(
                        onClick = { /* ์•ก์…˜ */ },
                        colors = ButtonDefaults.buttonColors(
                            containerColor = MaterialTheme.colorScheme.tertiary,
                            contentColor = MaterialTheme.colorScheme.onTertiary
                        ),
                        shape = MaterialTheme.shapes.large
                    ) {
                        Text("์ปค์Šคํ…€ ๋ฒ„ํŠผ")
                    }
                    
                    // ํ…์ŠคํŠธ ํ•„๋“œ
                    var textValue by remember { mutableStateOf("") }
                    TextField(
                        value = textValue,
                        onValueChange = { textValue = it },
                        label = { Text("ํ…Œ๋งˆ ์ ์šฉ๋œ ํ…์ŠคํŠธ ํ•„๋“œ") },
                        modifier = Modifier.fillMaxWidth()
                    )
                }
            }
        }
    }
}

/**
 * ์ƒ‰์ƒ ๊ฒฌ๋ณธ ์ปดํฌ๋„ŒํŠธ
 * CSS Color Palette์™€ ์œ ์‚ฌ
 */
@Composable
fun ColorSwatch(
    color: Color,
    name: String,
    textColor: Color
) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .height(48.dp)
            .padding(horizontal = 4.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {
        Box(
            modifier = Modifier
                .size(40.dp)
                .background(color, MaterialTheme.shapes.small)
        )
        
        Spacer(modifier = Modifier.width(12.dp))
        
        Text(
            text = name,
            style = MaterialTheme.typography.bodyLarge,
            color = textColor
        )
    }
}

๐Ÿš€ ์„ฑ๋Šฅ ์ตœ์ ํ™”

โšก Recomposition ์ตœ์ ํ™”

Recomposition์€ ํ•„์š”ํ•œ ๋ถ€๋ถ„๋งŒ ๋‹ค์‹œ ๊ทธ๋ ค์•ผ ์„ฑ๋Šฅ์ด ์ข‹์Šต๋‹ˆ๋‹ค. React์˜ ๋ฆฌ๋ Œ๋”๋ง ์ตœ์ ํ™”์™€ ๋™์ผํ•œ ๊ฐœ๋…์ž…๋‹ˆ๋‹ค.

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

/**
 * ์„ฑ๋Šฅ ์ตœ์ ํ™” ์˜ˆ์ œ
 * React์˜ useMemo, useCallback, React.memo์™€ ์œ ์‚ฌํ•œ ์ตœ์ ํ™” ๊ธฐ๋ฒ•
 */
@Composable
fun PerformanceExample() {
    var counter by remember { mutableStateOf(0) }
    var expensiveValue by remember { mutableStateOf(0) }
    
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        Text(
            text = "โšก ์„ฑ๋Šฅ ์ตœ์ ํ™” ์˜ˆ์ œ",
            style = MaterialTheme.typography.headlineLarge
        )
        
        // 1. remember๋ฅผ ์‚ฌ์šฉํ•œ ๊ฐ’ ์บ์‹ฑ
        // React์˜ useMemo์™€ ๋™์ผํ•œ ๊ฐœ๋…
        val expensiveCalculation = remember(expensiveValue) {
            // expensiveValue๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งŒ ๊ณ„์‚ฐ
            println("๐Ÿ”„ ๋น„์šฉ์ด ๋งŽ์ด ๋“œ๋Š” ๊ณ„์‚ฐ ์‹คํ–‰")
            (1..expensiveValue).map { it * it }.sum()
        }
        
        Card(
            modifier = Modifier.fillMaxWidth(),
            elevation = CardDefaults.cardElevation(4.dp)
        ) {
            Column(
                modifier = Modifier.padding(16.dp)
            ) {
                Text(
                    text = "๐Ÿ’ฐ ๋น„์šฉ์ด ๋งŽ์ด ๋“œ๋Š” ๊ณ„์‚ฐ",
                    style = MaterialTheme.typography.titleLarge
                )
                
                Text("์ž…๋ ฅ๊ฐ’: $expensiveValue")
                Text("๊ณ„์‚ฐ ๊ฒฐ๊ณผ: $expensiveCalculation")
                
                Spacer(modifier = Modifier.height(8.dp))
                
                Button(
                    onClick = { expensiveValue++ }
                ) {
                    Text("๊ฐ’ ์ฆ๊ฐ€")
                }
            }
        }
        
        // 2. ๋ถ„๋ฆฌ๋œ ์ปดํฌ๋„ŒํŠธ๋กœ recomposition ์ตœ์ ํ™”
        // React์˜ ์ปดํฌ๋„ŒํŠธ ๋ถ„๋ฆฌ์™€ ๋™์ผํ•œ ๊ฐœ๋…
        CounterSection(
            counter = counter,
            onIncrement = { counter++ },
            onDecrement = { counter-- }
        )
        
        // 3. LazyColumn์œผ๋กœ ๋Œ€์šฉ๋Ÿ‰ ๋ฆฌ์ŠคํŠธ ์ตœ์ ํ™”
        // React์˜ Virtual List์™€ ๋™์ผํ•œ ๊ฐœ๋…
        LargeListSection()
    }
}

/**
 * ๋ถ„๋ฆฌ๋œ ์นด์šดํ„ฐ ์ปดํฌ๋„ŒํŠธ
 * React์˜ memo()์™€ ์œ ์‚ฌํ•œ ์ตœ์ ํ™” ํšจ๊ณผ
 */
@Composable
fun CounterSection(
    counter: Int,
    onIncrement: () -> Unit,
    onDecrement: () -> Unit
) {
    // ์ด ์ปดํฌ๋„ŒํŠธ๋Š” counter ๊ด€๋ จ ๊ฐ’์ด ๋ณ€๊ฒฝ๋  ๋•Œ๋งŒ recomposition
    Card(
        modifier = Modifier.fillMaxWidth(),
        elevation = CardDefaults.cardElevation(4.dp)
    ) {
        Column(
            modifier = Modifier.padding(16.dp),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text(
                text = "๐Ÿ“Š ์นด์šดํ„ฐ ์„น์…˜",
                style = MaterialTheme.typography.titleLarge
            )
            
            Text(
                text = "Count: $counter",
                style = MaterialTheme.typography.headlineMedium
            )
            
            Spacer(modifier = Modifier.height(16.dp))
            
            Row(
                horizontalArrangement = Arrangement.spacedBy(8.dp)
            ) {
                Button(onClick = onDecrement) {
                    Text("-")
                }
                
                Button(onClick = onIncrement) {
                    Text("+")
                }
            }
        }
    }
}

/**
 * ๋Œ€์šฉ๋Ÿ‰ ๋ฆฌ์ŠคํŠธ ์„น์…˜
 * React์˜ Virtual List์™€ ๋™์ผํ•œ ๊ฐœ๋…
 */
@Composable
fun LargeListSection() {
    var items by remember { mutableStateOf(generateLargeList()) }
    
    Card(
        modifier = Modifier.fillMaxWidth(),
        elevation = CardDefaults.cardElevation(4.dp)
    ) {
        Column(
            modifier = Modifier.padding(16.dp)
        ) {
            Text(
                text = "๐Ÿ“‹ ๋Œ€์šฉ๋Ÿ‰ ๋ฆฌ์ŠคํŠธ (${items.size}๊ฐœ)",
                style = MaterialTheme.typography.titleLarge
            )
            
            Spacer(modifier = Modifier.height(8.dp))
            
            Button(
                onClick = { items = generateLargeList() }
            ) {
                Text("๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ")
            }
            
            Spacer(modifier = Modifier.height(8.dp))
            
            // LazyColumn์€ ๋ณด์ด๋Š” ํ•ญ๋ชฉ๋งŒ ๋ Œ๋”๋ง
            // React์˜ react-window๋‚˜ react-virtualized์™€ ๋™์ผ
            LazyColumn(
                modifier = Modifier.height(200.dp),
                verticalArrangement = Arrangement.spacedBy(4.dp)
            ) {
                items(items) { item ->
                    // ๊ฐ ํ•ญ๋ชฉ์€ ๋…๋ฆฝ์ ์œผ๋กœ recomposition
                    ListItem(item = item)
                }
            }
        }
    }
}

/**
 * ๋ฆฌ์ŠคํŠธ ํ•ญ๋ชฉ ์ปดํฌ๋„ŒํŠธ
 * React์˜ memo()์™€ ์œ ์‚ฌํ•œ ์ตœ์ ํ™” ํšจ๊ณผ
 */
@Composable
fun ListItem(item: String) {
    var isSelected by remember { mutableStateOf(false) }
    
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .clickable { isSelected = !isSelected }
            .background(
                if (isSelected) Color.LightGray else Color.Transparent,
                MaterialTheme.shapes.small
            )
            .padding(8.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {
        Text(
            text = item,
            modifier = Modifier.weight(1f)
        )
        
        if (isSelected) {
            Text(
                text = "โœ“",
                color = MaterialTheme.colorScheme.primary
            )
        }
    }
}

/**
 * ๋Œ€์šฉ๋Ÿ‰ ๋ฆฌ์ŠคํŠธ ์ƒ์„ฑ ํ•จ์ˆ˜
 */
fun generateLargeList(): List<String> {
    return (1..1000).map { "Item $it" }
}

๐Ÿงช ํ…Œ์ŠคํŠธ

๐Ÿ” UI ํ…Œ์ŠคํŠธ

UI ํ…Œ์ŠคํŠธ๋Š” ์‚ฌ์šฉ์ž ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
React Testing Library๋‚˜ Cypress์™€ ์œ ์‚ฌํ•œ ๊ฐœ๋…์ž…๋‹ˆ๋‹ค.

import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp

/**
 * ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•œ ์ปดํฌ๋„ŒํŠธ ์˜ˆ์ œ
 * React Testing Library์˜ ํ…Œ์ŠคํŠธ ํŒจํ„ด๊ณผ ์œ ์‚ฌ
 */
@Composable
fun TestableComponent() {
    var count by remember { mutableStateOf(0) }
    var text by remember { mutableStateOf("") }
    
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
            .semantics { 
                contentDescription = "๋ฉ”์ธ ํ™”๋ฉด" // ์ ‘๊ทผ์„ฑ์„ ์œ„ํ•œ ์„ค๋ช…
            },
        verticalArrangement = Arrangement.spacedBy(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(
            text = "๐Ÿงช ํ…Œ์ŠคํŠธ ์˜ˆ์ œ",
            style = MaterialTheme.typography.headlineLarge
        )
        
        // ์นด์šดํ„ฐ ์„น์…˜
        Card(
            modifier = Modifier
                .fillMaxWidth()
                .testTag("counter-card"), // ํ…Œ์ŠคํŠธ ID
            elevation = CardDefaults.cardElevation(4.dp)
        ) {
            Column(
                modifier = Modifier.padding(16.dp),
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Text(
                    text = "Count: $count",
                    modifier = Modifier.testTag("counter-text"), // ํ…Œ์ŠคํŠธ ID
                    style = MaterialTheme.typography.headlineMedium
                )
                
                Spacer(modifier = Modifier.height(16.dp))
                
                Row(
                    horizontalArrangement = Arrangement.spacedBy(8.dp)
                ) {
                    Button(
                        onClick = { count-- },
                        modifier = Modifier.testTag("decrement-button") // ํ…Œ์ŠคํŠธ ID
                    ) {
                        Text("-")
                    }
                    
                    Button(
                        onClick = { count++ },
                        modifier = Modifier.testTag("increment-button") // ํ…Œ์ŠคํŠธ ID
                    ) {
                        Text("+")
                    }
                }
                
                Spacer(modifier = Modifier.height(16.dp))
                
                Button(
                    onClick = { count = 0 },
                    modifier = Modifier.testTag("reset-button") // ํ…Œ์ŠคํŠธ ID
                ) {
                    Text("Reset")
                }
            }
        }
        
        // ํ…์ŠคํŠธ ์ž…๋ ฅ ์„น์…˜
        Card(
            modifier = Modifier
                .fillMaxWidth()
                .testTag("text-input-card"), // ํ…Œ์ŠคํŠธ ID
            elevation = CardDefaults.cardElevation(4.dp)
        ) {
            Column(
                modifier = Modifier.padding(16.dp)
            ) {
                TextField(
                    value = text,
                    onValueChange = { text = it },
                    label = { Text("ํ…์ŠคํŠธ ์ž…๋ ฅ") },
                    modifier = Modifier
                        .fillMaxWidth()
                        .testTag("text-input"), // ํ…Œ์ŠคํŠธ ID
                    placeholder = { Text("์—ฌ๊ธฐ์— ์ž…๋ ฅํ•˜์„ธ์š”") }
                )
                
                Spacer(modifier = Modifier.height(16.dp))
                
                if (text.isNotBlank()) {
                    Text(
                        text = "์ž…๋ ฅ๋œ ํ…์ŠคํŠธ: $text",
                        modifier = Modifier.testTag("display-text"), // ํ…Œ์ŠคํŠธ ID
                        style = MaterialTheme.typography.bodyLarge
                    )
                }
                
                Spacer(modifier = Modifier.height(16.dp))
                
                Button(
                    onClick = { text = "" },
                    modifier = Modifier.testTag("clear-text-button"), // ํ…Œ์ŠคํŠธ ID
                    enabled = text.isNotBlank()
                ) {
                    Text("Clear")
                }
            }
        }
        
        // ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง ํ…Œ์ŠคํŠธ
        if (count > 5) {
            Card(
                modifier = Modifier
                    .fillMaxWidth()
                    .testTag("achievement-card"), // ํ…Œ์ŠคํŠธ ID
                colors = CardDefaults.cardColors(
                    containerColor = MaterialTheme.colorScheme.primaryContainer
                )
            ) {
                Text(
                    text = "๐ŸŽ‰ 5๋ฅผ ๋„˜์—ˆ์Šต๋‹ˆ๋‹ค!",
                    modifier = Modifier
                        .padding(16.dp)
                        .testTag("achievement-text"), // ํ…Œ์ŠคํŠธ ID
                    style = MaterialTheme.typography.titleLarge
                )
            }
        }
    }
}

/**
 * ํ…Œ์ŠคํŠธ ์˜ˆ์ œ (์‹ค์ œ ํ…Œ์ŠคํŠธ ํŒŒ์ผ์— ์ž‘์„ฑ)
 * React Testing Library์˜ ํ…Œ์ŠคํŠธ ํŒจํ„ด๊ณผ ์œ ์‚ฌ
 */

@Test
fun testCounterIncrement() {
    // React Testing Library์˜ render()์™€ ์œ ์‚ฌ
    composeTestRule.setContent {
        TestableComponent()
    }
    
    // ์ดˆ๊ธฐ ์ƒํƒœ ํ™•์ธ - getByTestId()์™€ ์œ ์‚ฌ
    composeTestRule
        .onNodeWithTag("counter-text")
        .assertTextEquals("Count: 0")
    
    // ๋ฒ„ํŠผ ํด๋ฆญ - fireEvent.click()์™€ ์œ ์‚ฌ
    composeTestRule
        .onNodeWithTag("increment-button")
        .performClick()
    
    // ์ƒํƒœ ๋ณ€๊ฒฝ ํ™•์ธ - expect()์™€ ์œ ์‚ฌ
    composeTestRule
        .onNodeWithTag("counter-text")
        .assertTextEquals("Count: 1")
}

@Test
fun testTextInput() {
    composeTestRule.setContent {
        TestableComponent()
    }
    
    // ํ…์ŠคํŠธ ์ž…๋ ฅ - fireEvent.change()์™€ ์œ ์‚ฌ
    composeTestRule
        .onNodeWithTag("text-input")
        .performTextInput("Hello, World!")
    
    // ์ž…๋ ฅ ๊ฒฐ๊ณผ ํ™•์ธ
    composeTestRule
        .onNodeWithTag("display-text")
        .assertTextEquals("์ž…๋ ฅ๋œ ํ…์ŠคํŠธ: Hello, World!")
    
    // ํด๋ฆฌ์–ด ๋ฒ„ํŠผ ํด๋ฆญ
    composeTestRule
        .onNodeWithTag("clear-text-button")
        .performClick()
    
    // ํ…์ŠคํŠธ๊ฐ€ ์ง€์›Œ์กŒ๋Š”์ง€ ํ™•์ธ
    composeTestRule
        .onNodeWithTag("display-text")
        .assertDoesNotExist()
}

@Test
fun testConditionalRendering() {
    composeTestRule.setContent {
        TestableComponent()
    }
    
    // ์ดˆ๊ธฐ์—๋Š” achievement-card๊ฐ€ ์—†์Œ
    composeTestRule
        .onNodeWithTag("achievement-card")
        .assertDoesNotExist()
    
    // 6๋ฒˆ ํด๋ฆญํ•˜์—ฌ 5๋ฅผ ๋„˜๊น€
    repeat(6) {
        composeTestRule
            .onNodeWithTag("increment-button")
            .performClick()
    }
    
    // achievement-card๊ฐ€ ๋‚˜ํƒ€๋‚ฌ๋Š”์ง€ ํ™•์ธ
    composeTestRule
        .onNodeWithTag("achievement-card")
        .assertExists()
    
    composeTestRule
        .onNodeWithTag("achievement-text")
        .assertTextEquals("๐ŸŽ‰ 5๋ฅผ ๋„˜์—ˆ์Šต๋‹ˆ๋‹ค!")
}

๐Ÿ”ง ์‹ค๋ฌด Best Practices

๐Ÿ’ก ์ฝ”๋“œ ๊ตฌ์กฐํ™”

์ฝ”๋“œ ๊ตฌ์กฐํ™”๋Š” ์œ ์ง€๋ณด์ˆ˜์„ฑ๊ณผ ํ™•์žฅ์„ฑ์„ ๋†’์ด๋Š” ํ•ต์‹ฌ์ž…๋‹ˆ๋‹ค.

/**
 * ์‹ค๋ฌด ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ ์˜ˆ์ œ
 * 
 * ๐Ÿ“ app/
 * โ”œโ”€โ”€ ๐Ÿ“ src/main/java/com/example/app/
 * โ”‚   โ”œโ”€โ”€ ๐Ÿ“ ui/
 * โ”‚   โ”‚   โ”œโ”€โ”€ ๐Ÿ“ components/     # ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ปดํฌ๋„ŒํŠธ
 * โ”‚   โ”‚   โ”œโ”€โ”€ ๐Ÿ“ screens/        # ํ™”๋ฉด๋ณ„ ์ปดํฌ๋„ŒํŠธ
 * โ”‚   โ”‚   โ”œโ”€โ”€ ๐Ÿ“ theme/          # ํ…Œ๋งˆ ๊ด€๋ จ ํŒŒ์ผ
 * โ”‚   โ”‚   โ””โ”€โ”€ ๐Ÿ“ navigation/     # ๋„ค๋น„๊ฒŒ์ด์…˜ ๊ด€๋ จ
 * โ”‚   โ”œโ”€โ”€ ๐Ÿ“ data/
 * โ”‚   โ”‚   โ”œโ”€โ”€ ๐Ÿ“ repository/     # ๋ฐ์ดํ„ฐ ์ €์žฅ์†Œ
 * โ”‚   โ”‚   โ”œโ”€โ”€ ๐Ÿ“ network/        # ๋„คํŠธ์›Œํฌ ๊ด€๋ จ
 * โ”‚   โ”‚   โ””โ”€โ”€ ๐Ÿ“ local/          # ๋กœ์ปฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค
 * โ”‚   โ”œโ”€โ”€ ๐Ÿ“ domain/
 * โ”‚   โ”‚   โ”œโ”€โ”€ ๐Ÿ“ model/          # ๋ฐ์ดํ„ฐ ๋ชจ๋ธ
 * โ”‚   โ”‚   โ”œโ”€โ”€ ๐Ÿ“ usecase/        # ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง
 * โ”‚   โ”‚   โ””โ”€โ”€ ๐Ÿ“ repository/     # ์ €์žฅ์†Œ ์ธํ„ฐํŽ˜์ด์Šค
 * โ”‚   โ””โ”€โ”€ ๐Ÿ“ presentation/
 * โ”‚       โ”œโ”€โ”€ ๐Ÿ“ viewmodel/      # ViewModel
 * โ”‚       โ””โ”€โ”€ ๐Ÿ“ state/          # UI State
 */

/**
 * ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ปดํฌ๋„ŒํŠธ ์˜ˆ์ œ
 * React์˜ ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ์™€ ๋™์ผํ•œ ๊ฐœ๋…
 */
@Composable
fun CommonButton(
    text: String,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    variant: ButtonVariant = ButtonVariant.Primary
) {
    val colors = when (variant) {
        ButtonVariant.Primary -> ButtonDefaults.buttonColors()
        ButtonVariant.Secondary -> ButtonDefaults.outlinedButtonColors()
        ButtonVariant.Danger -> ButtonDefaults.buttonColors(
            containerColor = MaterialTheme.colorScheme.error
        )
    }
    
    Button(
        onClick = onClick,
        modifier = modifier,
        enabled = enabled,
        colors = colors
    ) {
        Text(text)
    }
}

enum class ButtonVariant {
    Primary, Secondary, Danger
}

/**
 * ๊ณตํ†ต ๋กœ๋”ฉ ์ปดํฌ๋„ŒํŠธ
 * React์˜ Loading Component์™€ ๋™์ผ
 */
@Composable
fun LoadingSpinner(
    modifier: Modifier = Modifier,
    message: String = "๋กœ๋”ฉ ์ค‘..."
) {
    Box(
        modifier = modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Column(
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            CircularProgressIndicator(
                color = MaterialTheme.colorScheme.primary
            )
            
            Spacer(modifier = Modifier.height(16.dp))
            
            Text(
                text = message,
                style = MaterialTheme.typography.bodyLarge,
                color = MaterialTheme.colorScheme.onSurface
            )
        }
    }
}

/**
 * ์—๋Ÿฌ ํ‘œ์‹œ ์ปดํฌ๋„ŒํŠธ
 * React์˜ Error Component์™€ ๋™์ผ
 */
@Composable
fun ErrorDisplay(
    message: String,
    onRetry: () -> Unit,
    modifier: Modifier = Modifier
) {
    Card(
        modifier = modifier.fillMaxWidth(),
        colors = CardDefaults.cardColors(
            containerColor = MaterialTheme.colorScheme.errorContainer
        )
    ) {
        Column(
            modifier = Modifier.padding(16.dp),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text(
                text = "โŒ ์˜ค๋ฅ˜ ๋ฐœ์ƒ",
                style = MaterialTheme.typography.headlineSmall,
                color = MaterialTheme.colorScheme.onErrorContainer
            )
            
            Spacer(modifier = Modifier.height(8.dp))
            
            Text(
                text = message,
                style = MaterialTheme.typography.bodyMedium,
                color = MaterialTheme.colorScheme.onErrorContainer
            )
            
            Spacer(modifier = Modifier.height(16.dp))
            
            CommonButton(
                text = "๋‹ค์‹œ ์‹œ๋„",
                onClick = onRetry,
                variant = ButtonVariant.Primary
            )
        }
    }
}

๐Ÿ“ฑ ํ”„๋กœ์ ํŠธ ์˜ˆ์ œ

๐Ÿ›’ ์‡ผํ•‘๋ชฐ ์•ฑ ์˜ˆ์ œ

/**
 * ์™„์ „ํ•œ ์‡ผํ•‘๋ชฐ ์•ฑ ์˜ˆ์ œ
 * ์‹ค๋ฌด์—์„œ ์‚ฌ์šฉ๋˜๋Š” ๋ชจ๋“  ํŒจํ„ด์„ ํฌํ•จ
 */

// ๋ฐ์ดํ„ฐ ๋ชจ๋ธ - TypeScript์˜ interface์™€ ์œ ์‚ฌ
data class Product(
    val id: Int,
    val name: String,
    val price: Double,
    val imageUrl: String,
    val description: String,
    val category: String,
    val rating: Float = 0f,
    val reviewCount: Int = 0
)

data class CartItem(
    val product: Product,
    val quantity: Int
)

// UI ์ƒํƒœ ๊ด€๋ฆฌ - Redux์˜ State์™€ ์œ ์‚ฌ
sealed class ShoppingUiState {
    object Loading : ShoppingUiState()
    data class Success(
        val products: List<Product>,
        val cartItems: List<CartItem> = emptyList(),
        val selectedCategory: String = "All"
    ) : ShoppingUiState()
    data class Error(val message: String) : ShoppingUiState()
}

// ViewModel - React์˜ Custom Hook์ด๋‚˜ Redux Store์™€ ์œ ์‚ฌ
class ShoppingViewModel : ViewModel() {
    // StateFlow๋Š” React์˜ state์™€ ์œ ์‚ฌ
    private val _uiState = MutableStateFlow<ShoppingUiState>(ShoppingUiState.Loading)
    val uiState: StateFlow<ShoppingUiState> = _uiState.asStateFlow()
    
    init {
        loadProducts() // ์ปดํฌ๋„ŒํŠธ ๋งˆ์šดํŠธ ์‹œ ์ž๋™ ์‹คํ–‰
    }
    
    // ์ƒํ’ˆ ๋กœ๋“œ - async/await ํŒจํ„ด๊ณผ ์œ ์‚ฌ
    private fun loadProducts() {
        viewModelScope.launch {
            _uiState.value = ShoppingUiState.Loading
            
            try {
                delay(1000) // ๋„คํŠธ์›Œํฌ ์ง€์—ฐ ์‹œ๋ฎฌ๋ ˆ์ด์…˜
                
                val products = generateSampleProducts()
                _uiState.value = ShoppingUiState.Success(products)
            } catch (e: Exception) {
                _uiState.value = ShoppingUiState.Error("์ƒํ’ˆ์„ ๋ถˆ๋Ÿฌ์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค")
            }
        }
    }
    
    // ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ถ”๊ฐ€ - Redux์˜ action๊ณผ ์œ ์‚ฌ
    fun addToCart(product: Product) {
        val currentState = _uiState.value
        if (currentState is ShoppingUiState.Success) {
            val existingItem = currentState.cartItems.find { it.product.id == product.id }
            val updatedCart = if (existingItem != null) {
                // ๊ธฐ์กด ์•„์ดํ…œ ์ˆ˜๋Ÿ‰ ์ฆ๊ฐ€
                currentState.cartItems.map {
                    if (it.product.id == product.id) {
                        it.copy(quantity = it.quantity + 1)
                    } else it
                }
            } else {
                // ์ƒˆ ์•„์ดํ…œ ์ถ”๊ฐ€
                currentState.cartItems + CartItem(product, 1)
            }
            
            _uiState.value = currentState.copy(cartItems = updatedCart)
        }
    }
    
    // ์นดํ…Œ๊ณ ๋ฆฌ ํ•„ํ„ฐ - Redux์˜ action๊ณผ ์œ ์‚ฌ
    fun filterByCategory(category: String) {
        val currentState = _uiState.value
        if (currentState is ShoppingUiState.Success) {
            _uiState.value = currentState.copy(selectedCategory = category)
        }
    }
    
    // ๋ชจ์˜ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ - ์‹ค์ œ๋กœ๋Š” API ํ˜ธ์ถœ
    private fun generateSampleProducts(): List<Product> {
        return listOf(
            Product(1, "์Šค๋งˆํŠธํฐ", 899.99, "", "์ตœ์‹  ์Šค๋งˆํŠธํฐ", "Electronics", 4.5f, 123),
            Product(2, "๋…ธํŠธ๋ถ", 1299.99, "", "๊ณ ์„ฑ๋Šฅ ๋…ธํŠธ๋ถ", "Electronics", 4.3f, 89),
            Product(3, "์šด๋™ํ™”", 129.99, "", "ํŽธ์•ˆํ•œ ์šด๋™ํ™”", "Fashion", 4.7f, 234),
            Product(4, "์ฑ…", 19.99, "", "๋ฒ ์ŠคํŠธ์…€๋Ÿฌ ๋„์„œ", "Books", 4.2f, 45),
            Product(5, "์ปคํ”ผ", 12.99, "", "์›๋‘ ์ปคํ”ผ", "Food", 4.6f, 167)
        )
    }
}

// ๋ฉ”์ธ ์•ฑ ์ปดํฌ๋„ŒํŠธ
@Composable
fun ShoppingApp() {
    val viewModel: ShoppingViewModel = viewModel() // React์˜ useContext์™€ ์œ ์‚ฌ
    val uiState by viewModel.uiState.collectAsState() // React์˜ useSelector์™€ ์œ ์‚ฌ
    
    Box(
        modifier = Modifier.fillMaxSize()
    ) {
        // ์ƒํƒœ์— ๋”ฐ๋ฅธ ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง - React์˜ conditional rendering๊ณผ ๋™์ผ
        when (uiState) {
            is ShoppingUiState.Loading -> {
                LoadingSpinner(message = "์ƒํ’ˆ์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘...")
            }
            
            is ShoppingUiState.Success -> {
                ShoppingContent(
                    products = uiState.products,
                    cartItems = uiState.cartItems,
                    selectedCategory = uiState.selectedCategory,
                    onAddToCart = viewModel::addToCart, // React์˜ callback props์™€ ์œ ์‚ฌ
                    onCategoryChange = viewModel::filterByCategory
                )
            }
            
            is ShoppingUiState.Error -> {
                ErrorDisplay(
                    message = uiState.message,
                    onRetry = { viewModel.loadProducts() }
                )
            }
        }
    }
}

// ๋ฉ”์ธ ์ฝ˜ํ…์ธ  ์ปดํฌ๋„ŒํŠธ
@Composable
fun ShoppingContent(
    products: List<Product>,
    cartItems: List<CartItem>,
    selectedCategory: String,
    onAddToCart: (Product) -> Unit,
    onCategoryChange: (String) -> Unit
) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        // ํ—ค๋” ์ปดํฌ๋„ŒํŠธ
        ShoppingHeader(
            cartItemCount = cartItems.sumOf { it.quantity }
        )
        
        Spacer(modifier = Modifier.height(16.dp))
        
        // ์นดํ…Œ๊ณ ๋ฆฌ ํ•„ํ„ฐ ์ปดํฌ๋„ŒํŠธ
        CategoryFilter(
            selectedCategory = selectedCategory,
            onCategoryChange = onCategoryChange
        )
        
        Spacer(modifier = Modifier.height(16.dp))
        
        // ์ƒํ’ˆ ๋ชฉ๋ก ์ปดํฌ๋„ŒํŠธ
        ProductList(
            products = products.filter { 
                selectedCategory == "All" || it.category == selectedCategory 
            },
            onAddToCart = onAddToCart
        )
    }
}

// ํ—ค๋” ์ปดํฌ๋„ŒํŠธ
@Composable
fun ShoppingHeader(cartItemCount: Int) {
    Row(
        modifier = Modifier.fillMaxWidth(),
        horizontalArrangement = Arrangement.SpaceBetween,
        verticalAlignment = Alignment.CenterVertically
    ) {
        Text(
            text = "๐Ÿ›๏ธ ์‡ผํ•‘๋ชฐ",
            style = MaterialTheme.typography.headlineLarge
        )
        
        // ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋ฐฐ์ง€ - Material UI์˜ Badge์™€ ์œ ์‚ฌ
        Badge(
            modifier = Modifier.testTag("cart-badge")
        ) {
            Text("$cartItemCount")
        }
    }
}

// ์นดํ…Œ๊ณ ๋ฆฌ ํ•„ํ„ฐ ์ปดํฌ๋„ŒํŠธ 
@Composable
fun CategoryFilter(
    selectedCategory: String,
    onCategoryChange: (String) -> Unit
) {
    val categories = listOf("All", "Electronics", "Fashion", "Books", "Food")
    
    // ๊ฐ€๋กœ ์Šคํฌ๋กค ๋ฆฌ์ŠคํŠธ - React์˜ horizontal scroll๊ณผ ์œ ์‚ฌ
    LazyRow(
        horizontalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        items(categories) { category ->
            FilterChip(
                selected = selectedCategory == category,
                onClick = { onCategoryChange(category) },
                label = { Text(category) }
            )
        }
    }
}

// ์ƒํ’ˆ ๋ชฉ๋ก ์ปดํฌ๋„ŒํŠธ
@Composable
fun ProductList(
    products: List<Product>,
    onAddToCart: (Product) -> Unit
) {
    // ์„ธ๋กœ ์Šคํฌ๋กค ๋ฆฌ์ŠคํŠธ - React์˜ VirtualizedList์™€ ์œ ์‚ฌ
    LazyColumn(
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        items(products) { product ->
            ProductCard(
                product = product,
                onAddToCart = { onAddToCart(product) }
            )
        }
    }
}

// ์ƒํ’ˆ ์นด๋“œ ์ปดํฌ๋„ŒํŠธ
@Composable
fun ProductCard(
    product: Product,
    onAddToCart: () -> Unit
) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .testTag("product-card-${product.id}"), // ํ…Œ์ŠคํŠธ์šฉ ID
        elevation = CardDefaults.cardElevation(4.dp)
    ) {
        Column(
            modifier = Modifier.padding(16.dp)
        ) {
            // ์ƒํ’ˆ๋ช…
            Text(
                text = product.name,
                style = MaterialTheme.typography.headlineSmall
            )
            
            // ์นดํ…Œ๊ณ ๋ฆฌ
            Text(
                text = product.category,
                style = MaterialTheme.typography.bodySmall,
                color = MaterialTheme.colorScheme.onSurfaceVariant
            )
            
            Spacer(modifier = Modifier.height(8.dp))
            
            // ๊ฐ€๊ฒฉ๊ณผ ํ‰์ 
            Row(
                modifier = Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.SpaceBetween,
                verticalAlignment = Alignment.CenterVertically
            ) {
                Text(
                    text = "${product.price}",
                    style = MaterialTheme.typography.titleLarge,
                    color = MaterialTheme.colorScheme.primary
                )
                
                Row(
                    verticalAlignment = Alignment.CenterVertically
                ) {
                    Text(
                        text = "โญ ${product.rating}",
                        style = MaterialTheme.typography.bodySmall
                    )
                    
                    Spacer(modifier = Modifier.width(4.dp))
                    
                    Text(
                        text = "(${product.reviewCount})",
                        style = MaterialTheme.typography.bodySmall,
                        color = MaterialTheme.colorScheme.onSurfaceVariant
                    )
                }
            }
            
            Spacer(modifier = Modifier.height(8.dp))
            
            // ์ƒํ’ˆ ์„ค๋ช…
            Text(
                text = product.description,
                style = MaterialTheme.typography.bodyMedium,
                color = MaterialTheme.colorScheme.onSurfaceVariant
            )
            
            Spacer(modifier = Modifier.height(16.dp))
            
            // ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ถ”๊ฐ€ ๋ฒ„ํŠผ
            Button(
                onClick = onAddToCart,
                modifier = Modifier
                    .fillMaxWidth()
                    .testTag("add-to-cart-${product.id}")
            ) {
                Text("์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ์ถ”๊ฐ€")
            }
        }
    }
}

๐ŸŽจ UI ํŒจํ„ด ์˜ˆ์ œ

/**
 * ์ž์ฃผ ์‚ฌ์šฉ๋˜๋Š” ๊ณ ๊ธ‰ UI ํŒจํ„ด๋“ค
 */

// 1. Pull to Refresh ํŒจํ„ด
@Composable
fun PullToRefreshExample() {
    var isRefreshing by remember { mutableStateOf(false) }
    var items by remember { mutableStateOf(generateItems()) }
    
    // React์˜ useCallback๊ณผ ์œ ์‚ฌํ•œ ํ•จ์ˆ˜ ๋ฉ”๋ชจ์ด์ œ์ด์…˜
    val onRefresh = remember<() -> Unit> {
        {
            isRefreshing = true
            // ์‹ค์ œ๋กœ๋Š” ๋„คํŠธ์›Œํฌ ํ˜ธ์ถœ
            CoroutineScope(Dispatchers.Main).launch {
                delay(2000)
                items = generateItems()
                isRefreshing = false
            }
        }
    }
    
    PullToRefreshBox(
        isRefreshing = isRefreshing,
        onRefresh = onRefresh
    ) {
        LazyColumn {
            items(items) { item ->
                ItemCard(item = item)
            }
        }
    }
}

// 2. ๋ฌดํ•œ ์Šคํฌ๋กค ํŒจํ„ด
@Composable
fun InfiniteScrollExample() {
    var items by remember { mutableStateOf(generateItems()) }
    var isLoading by remember { mutableStateOf(false) }
    var hasMore by remember { mutableStateOf(true) }
    
    // ๋” ๋งŽ์€ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ํ•จ์ˆ˜
    val loadMore = remember {
        {
            if (!isLoading && hasMore) {
                isLoading = true
                CoroutineScope(Dispatchers.Main).launch {
                    delay(1000)
                    val newItems = generateItems()
                    items = items + newItems
                    isLoading = false
                    hasMore = items.size < 100 // ์ตœ๋Œ€ 100๊ฐœ๊นŒ์ง€
                }
            }
        }
    }
    
    LazyColumn {
        items(items) { item ->
            ItemCard(item = item)
        }
        
        // ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ
        if (isLoading) {
            item {
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(16.dp),
                    contentAlignment = Alignment.Center
                ) {
                    CircularProgressIndicator()
                }
            }
        }
        
        // ์Šคํฌ๋กค ๋ ๊ฐ์ง€
        if (hasMore && !isLoading) {
            item {
                LaunchedEffect(Unit) {
                    loadMore()
                }
            }
        }
    }
}

// 3. ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ํŒจํ„ด
@Composable
fun SearchExample() {
    var searchQuery by remember { mutableStateOf("") }
    var searchResults by remember { mutableStateOf<List<String>>(emptyList()) }
    var isSearching by remember { mutableStateOf(false) }
    
    // ๋””๋ฐ”์šด์Šค ๊ฒ€์ƒ‰
    LaunchedEffect(searchQuery) {
        if (searchQuery.isNotBlank()) {
            isSearching = true
            delay(300) // 300ms ๋””๋ฐ”์šด์Šค
            
            // ์‹ค์ œ๋กœ๋Š” API ๊ฒ€์ƒ‰
            val results = performSearch(searchQuery)
            searchResults = results
            isSearching = false
        } else {
            searchResults = emptyList()
            isSearching = false
        }
    }
    
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        // ๊ฒ€์ƒ‰ ์ž…๋ ฅ ํ•„๋“œ
        TextField(
            value = searchQuery,
            onValueChange = { searchQuery = it },
            placeholder = { Text("๊ฒ€์ƒ‰์–ด๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”") },
            modifier = Modifier.fillMaxWidth(),
            leadingIcon = {
                Icon(Icons.Default.Search, contentDescription = null)
            },
            trailingIcon = {
                if (searchQuery.isNotBlank()) {
                    IconButton(
                        onClick = { searchQuery = "" }
                    ) {
                        Icon(Icons.Default.Clear, contentDescription = null)
                    }
                }
            }
        )
        
        Spacer(modifier = Modifier.height(16.dp))
        
        // ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ
        when {
            isSearching -> {
                Box(
                    modifier = Modifier.fillMaxSize(),
                    contentAlignment = Alignment.Center
                ) {
                    CircularProgressIndicator()
                }
            }
            
            searchResults.isEmpty() && searchQuery.isNotBlank() -> {
                Box(
                    modifier = Modifier.fillMaxSize(),
                    contentAlignment = Alignment.Center
                ) {
                    Text("๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค")
                }
            }
            
            searchResults.isNotEmpty() -> {
                LazyColumn {
                    items(searchResults) { result ->
                        SearchResultItem(
                            text = result,
                            query = searchQuery
                        )
                    }
                }
            }
        }
    }
}

// ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํ•ญ๋ชฉ - ํ•˜์ด๋ผ์ดํŠธ ๊ธฐ๋Šฅ ํฌํ•จ
@Composable
fun SearchResultItem(
    text: String,
    query: String
) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(vertical = 4.dp)
            .clickable { /* ํด๋ฆญ ์ฒ˜๋ฆฌ */ },
        elevation = CardDefaults.cardElevation(2.dp)
    ) {
        Text(
            text = buildAnnotatedString {
                val startIndex = text.indexOf(query, ignoreCase = true)
                if (startIndex != -1) {
                    append(text.substring(0, startIndex))
                    withStyle(
                        style = SpanStyle(
                            background = Color.Yellow,
                            fontWeight = FontWeight.Bold
                        )
                    ) {
                        append(text.substring(startIndex, startIndex + query.length))
                    }
                    append(text.substring(startIndex + query.length))
                } else {
                    append(text)
                }
            },
            modifier = Modifier.padding(16.dp)
        )
    }
}

// 4. ํƒญ ๋„ค๋น„๊ฒŒ์ด์…˜ ํŒจํ„ด
@Composable
fun TabNavigationExample() {
    var selectedTab by remember { mutableStateOf(0) }
    val tabs = listOf("ํ™ˆ", "๊ฒ€์ƒ‰", "์•Œ๋ฆผ", "ํ”„๋กœํ•„")
    
    Column(
        modifier = Modifier.fillMaxSize()
    ) {
        // ํƒญ ๋ฐ”
        TabRow(
            selectedTabIndex = selectedTab,
            modifier = Modifier.fillMaxWidth()
        ) {
            tabs.forEachIndexed { index, title ->
                Tab(
                    selected = selectedTab == index,
                    onClick = { selectedTab = index },
                    text = { Text(title) }
                )
            }
        }
        
        // ํƒญ ์ฝ˜ํ…์ธ 
        when (selectedTab) {
            0 -> HomeTabContent()
            1 -> SearchTabContent()
            2 -> NotificationTabContent()
            3 -> ProfileTabContent()
        }
    }
}

// ๊ฐ ํƒญ์˜ ์ฝ˜ํ…์ธ 
@Composable
fun HomeTabContent() {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Text(
            text = "๐Ÿ  ํ™ˆ ํƒญ",
            style = MaterialTheme.typography.headlineLarge
        )
    }
}

@Composable
fun SearchTabContent() {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Text(
            text = "๐Ÿ” ๊ฒ€์ƒ‰ ํƒญ",
            style = MaterialTheme.typography.headlineLarge
        )
    }
}

@Composable
fun NotificationTabContent() {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Text(
            text = "๐Ÿ”” ์•Œ๋ฆผ ํƒญ",
            style = MaterialTheme.typography.headlineLarge
        )
    }
}

@Composable
fun ProfileTabContent() {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Text(
            text = "๐Ÿ‘ค ํ”„๋กœํ•„ ํƒญ",
            style = MaterialTheme.typography.headlineLarge
        )
    }
}

// ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜๋“ค
fun generateItems(): List<String> {
    return (1..20).map { "Item $it" }
}

fun performSearch(query: String): List<String> {
    // ์‹ค์ œ๋กœ๋Š” API ํ˜ธ์ถœ
    return listOf(
        "๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ 1: $query",
        "๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ 2: $query ๊ด€๋ จ",
        "๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ 3: $query ์ •๋ณด"
    )
}

Jetpack Compose๋Š” modernํ•œ Android UI ๊ฐœ๋ฐœ์„ ์œ„ํ•œ ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ์ž…๋‹ˆ๋‹ค.
declarative programming์„ ํ†ตํ•ด ์ง๊ด€์ ์ด๊ณ  ํšจ์œจ์ ์ธ UI ๊ฐœ๋ฐœ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ๐Ÿš€

Categories:

Updated: