분리 조건? Split fun Jetpack Compose • Preview를 통한 빠른 UI 확인 • Screen ◦ Components를 합쳐서 Screen을 구성하도록 • Components ◦ Screen에 해당하는 Components 구성 ◦ 디자인 시스템에서의 Components 구성 Preview를 통해 UI 확인이 가능하도록 크게 3가지로 분리하여 사용합니다. Screen 단위는 가장 기본적인 단위이며, Navigation 사용 시 각각을 Screen 단위로 나누게 됩니다. 다음으로 Components. Components는 크게 2가지로 나눌 수 있는데, Screen 단위에 맞는 Components(ex 리스트 형태의 화면이나 그 안에서의 화면들), 디자인 시스템에서의 Components로 보통 나누고 있습니다.
= rememberNavController(), ) { var listItem by remember { mutableStateOf(ListItem(emptyList())) } val navBackStackEntry by navController.currentBackStackEntryAsState() val currentDestination = navBackStackEntry?.destination Scaffold( topBar = { TopAppBar( title = { Text(text = currentDestination?.route ?: "") }, ) }, bottomBar = { NavigationBar { list.forEach { screen -> NavigationBarItem( label = { Text( text = screen.title, ) }, selected = currentDestination?.hierarchy?.any { it.route == screen.trigger.name } == true, icon = { Icon( painter = painterResource(id = screen.icon), contentDescription = screen.title ) }, onClick = { navController.navigate(screen.trigger.name) { // Pop up to the start destination of the graph to // avoid building up a large stack of destinations // on the back stack as users select items popUpTo(navController.graph.findStartDestination().id) { saveState = true } // Avoid multiple copies of the same destination when // reselecting the same item launchSingleTop = true // Restore state when reselecting a previously selected item restoreState = true } } ) More 311 line 이 샘플 코드는 GitHub에 풀 코드가 올라가있습니다. 하나의 MainScreen 이란 이름에 311 줄의 라인을 그냥 작성했습니다. 이 코드엔 Scaffold부터 Navigation 등 모든 코드가 하나로 포함되어 있습니다.
다양한 데이터 구조를 잡기가 힘듦 ◦ ViewModel 사용 시 Preview 어려움 • 코드 분리를 하지 않아 수정이 어려움 이 코드의 단점 tip 당연한 이야기지만 코드가 길어지니 유지 보수가 어려워집니다. 그리고 Preview 역시 어렵습니다. 특히나 우리가 많이 활용하는 외부 주입 객체가 있는 경우에는 Preview가 불가능할 수 있습니다.
분리 방법은? ◦ 단순히 작은 단위로 나눈다 ◦ 클린 코드 형태로 분리 ▪ 함수는 하나의 역할과 맡는 이름을 가지도록 한다 ◦ Stateful, Stateless를 이해한 분리 분리 조건? tip 이러한 코드의 분리 조건은 어떻게 가져가는 것이 좋을까요? 단순히 작은 단위로 나누는 것이 좋을지, 클린 코드 형태로 분리하는 것이 좋을지, 아니면 선언형 UI에서 주로 이야기하는 Stateful, Stateless를 이해한 분리를 하는 것이 좋을지
= { // bottom Bar 관련 }, modifier = Modifier .fillMaxSize() ) { Box( modifier = Modifier .padding(it) ) { NavHost( ) { composable(route = NavigationSample.Trigger.HOME.name) { // Home 관련 } composable(route = NavigationSample.Trigger.WEB.name) { // WebView 관련 } } } } 먼저 어디서부터 어떠한 코드를 분리하는 것이 좋을지를 순서대로 이야기해 보겠습니다. 모든 코드에서 큰 부분에 대해서 역할을 분리하였습니다.
= { // bottom Bar 관련 }, modifier = Modifier .fillMaxSize() ) { Box( modifier = Modifier .padding(it) ) { NavHost( ) { composable(route = NavigationSample.Trigger.HOME.name) { // Home 관련 } composable(route = NavigationSample.Trigger.WEB.name) { // WebView 관련 } } } } 후보 1 몇 개의 대상을 나누어보았는데 먼저 후보 1. 상단의 TopBar에 해당하는 영역입니다. 저는 이 부분은 코드에서 특별히 분리하지 않고 있습니다. 길어질 부분이 특별히 없기도 하고, 분리했을 때의 이점이 크게 없는 부분입니다.
= { // bottom Bar 관련 }, modifier = Modifier .fillMaxSize() ) { Box( modifier = Modifier .padding(it) ) { NavHost( ) { composable(route = NavigationSample.Trigger.HOME.name) { // Home 관련 } composable(route = NavigationSample.Trigger.WEB.name) { // WebView 관련 } } } } 후보 2 후보 2번은 BottomBar의 부분입니다. 아무리 중복 코드를 제거한다 해도 길어질 수 있습니다. 기능적인 빠른 확인도 가능한 부분이라 분리해서 관리해도 좋은 예가 됩니다. 오늘의 발표에선 분리하지 않습니다.
= { // bottom Bar 관련 }, modifier = Modifier .fillMaxSize() ) { Box( modifier = Modifier .padding(it) ) { NavHost( ) { composable(route = NavigationSample.Trigger.HOME.name) { // Home 관련 } composable(route = NavigationSample.Trigger.WEB.name) { // WebView 관련 } } } } 후보 3 후보 3 마지막 후보 3번 Screen은 매우 길어지게 됩니다. 지금은 2개의 Screen뿐이지만 하나씩 늘어나면 코드 양도 함께 늘어납니다. 그러니 자연스럽게 2개의 Screen을 분리하는 것이 가장 적합한 예입니다.
mutableStateOf(ListItem(emptyList())) } Column { LazyColumn { items(listItem.items) { item -> Surface( shape = MaterialTheme.shapes.small, ) { if (item.editMode) { Column() { } } else { Column() { } } } } } } } @Preview(showBackground = true) @Composable private fun PreviewHomeScreen() { HomeScreen() } 이 코드에 대한 Preview를 적용했습니다. 그런데 한 가지 문제가 있습니다. Preview는 그려졌지만 실제로 의미 없는 Preview가 되었습니다. 이유는 3번째 줄의 remember{} 부분 때문입니다.
외부에서 값을 변경하거나 이벤트로 전달할 수 있는 형태 Stateful Stateless Stateful versus stateless State 구글 문서상 Stateful은 remember를 사용해 객체를 저장하는 Composable을 포함하는 부분입니다. 일반적으론 ViewModel의 state를 collect() 하는 부분을 말합니다. stateless는 외부에서 값을 변경하거나 이벤트로 전달할 수 있는 형태 즉 외부에서 주입하는 값에 따라 화면이 달라지는 형태를 뜻합니다. * 이 문서 참고 : https://developer.android.com/develop/ui/compose/state
mutableStateOf(ListItem(emptyList())) } HomeScreen(listItem) } @Composable internal fun HomeScreen(listItem: ListItem) { LazyColumn { items(listItem.items) { item -> Text( text = item.text, style = MaterialTheme.typography.bodyMedium, ) } } } 두 개의 함수가 있습니다. 위 HomeScreen은 remember{}를 포함하고 있고, 아래 HomeScreen은 listItem을 외부에서 주입받고 있습니다.
mutableStateOf(ListItem(emptyList())) } HomeScreen(listItem) } @Composable internal fun HomeScreen(listItem: ListItem) { LazyColumn { items(listItem.items) { item -> Text( text = item.text, style = MaterialTheme.typography.bodyMedium, ) } } } HomeScreen (Stateful) HomeScreen (Stateless) state 도식화하면 오른쪽과 같습니다. 위 HomeScreen에서 viewModel 또는 remember를 통해 값을 정의할 수 있고, 이를 아래 HomeScreen에 주입하게 됩니다. 아래 HomeScreen은 이제 외부에서 정의해 주는 값을 표현하게 됩니다.
전달할 수 있는 형태로 테스트가 유연 • 단일 정보 소스로 관리 • 캡슐화 • 공유 가능 • 가로채기 가능 • 분리 Stateful Stateless Stateful versus stateless State Stateful 은 상태를 갖는 Composable 함수를 구성하기에 테스트가 어렵습니다. Stateless는 외부에서 값을 변경하거나, 이벤트를 통해 전달할 수 있는 형태로 테스트(Preview)가 유연해지게 되는데, 아래와 같이 5가지 장점이 생깁니다. • 단일 정보 소스: 상태를 복제하는 대신 옮겼기 때문에 정보 소스가 하나만 있습니다. 버그 방지에 도움이 됩니다. • 캡슐화됨: 스테이트풀(Stateful) 컴포저블 만 상태를 수정할 수 있습니다. 철저히 내부적 속성입니다. • 공유 가능함: 호이스팅한 상태를 여러 컴포저블과 공유할 수 있습니다. 다른 컴포저블에서 name을 읽으려는 경우 호이스팅을 통해 그렇게 할 수 있습니다. • 가로채기 가능함: 스테이트리스(Stateless) 컴포저블의 호출자는 상태를 변경하기 전에 이벤트를 무시할지 수정할지 결정할 수 있습니다. • 분리됨: 스테이트리스(Stateless) 컴포저블의 상태가 저장될 수 있습니다. 액세스할 수 있습니다 예를 들어 이제 name를 ViewModel로 이동할 수 있습니다.
var listItem by remember { mutableStateOf( ListItem( listOf( ListItem.Item( index = 0, text = "message", editMode = false, ), ListItem.Item( index = 1, text = "", editMode = true, ), ) ) ) } HomeScreen( listItem = listItem, onEvent = { listItem = it } ) } Preview를 통해 listItem을 주입하게 되고, event도 받게 되니 이전과 다르게 Preview를 잘 활용할 수 있도록 변경되었습니다.
장점은 있지만 필요할까? */ @Composable private fun SaveButton( changeItem: ListItem.Item, modifier: Modifier, onEditModeOff: (changeItem: ListItem.Item) -> Unit, ) { Button( onClick = { onEditModeOff(changeItem) }, modifier = modifier ) { SaveText() } } /** * Text 하나 뿐인데 굳이...? */ @Composable private fun SaveText() { Text(text = "Save") } 이미 Components인 Text, Button을 Screen에서 활용할 건데 Title, SubText, Content … 등으로 과도한 적용? Don’t Example 1 첫 번째 예제 Composable 함수를 어디까지 나누는 것이 좋을까요? 무작정 많이 나누는 것은 답은 아닙니다.
장점은 있지만 필요할까? */ @Composable private fun SaveButton( changeItem: ListItem.Item, modifier: Modifier, onEditModeOff: (changeItem: ListItem.Item) -> Unit, ) { Button( onClick = { onEditModeOff(changeItem) }, modifier = modifier ) { SaveText() } } /** * Text 하나 뿐인데 굳이...? */ @Composable private fun SaveText() { Text(text = "Save") } 이미 Components인 Text, Button을 Screen에서 활용할 건데 Title, SubText, Content … 등으로 과도한 적용? Don’t Example 1 첫 번째 예제 여기서처럼 SaveText()까지 Composable로 나누는 것이 적합할까요? 개인적인 명확한 다음 의미 없다. StringResource도 있고, 재사용 가능한 형태로 코드를 작성해야 수정도 쉬운데 나중에 Save 대신 다른 이름이 온다면? 그럼 그때 또 수정하고 대응하려고 함수명까지 교체해야 할까요? 이 부분은 디자인을 눈으로 보고 합쳐져있는 UI에 대해 확인이 가능해야 하는데 이미 그 범위를 벗어난 형태로 보입니다.
fun HomeButton( onClick: () -> Unit, modifier: Modifier = Modifier, text: String, ) { Button( onClick = onClick, modifier = modifier ) { Text(text = text) } } /** * 사용하는 경우 */ Row { HomeButton( onClick = { }, text = "Save", modifier = Modifier .weight(1f) ) HomeButton( onClick = { }, text = "x", modifier = Modifier .weight(1f) .padding(start = 10.dp) ) } Button을 공용 Components로 만들어 사용하는 방법 Suggestion Example 1 첫 번째 예제 그래서 정말 필요한 형태로 바꾼다면 Button을 Text 주입으로도 동작할 수 있도록 코드를 수정하는 방법입니다. 이렇게 하면 새로운 버튼 추가도 빠르게 할 수 있고, Text 관련 정보가 변경되어도, HomeButton 부분만 수정하면 됩니다.
modifier: Modifier = Modifier, text: String, ) { Button( onClick = onClick, modifier = modifier ) { Text(text = text) } } @Preview(showBackground = true) @Composable private fun PreviewHomeButton() { Row { HomeButton( onClick = {}, modifier = Modifier .weight(1f), text = "Save", ) HomeButton( onClick = {}, modifier = Modifier .weight(1f) .padding(start = 10.dp), text = "x", ) } } Modifier 기준으로 최상 또는 최하단에 둔다? 사용할 때도 중간에? Maybe Example 2 코드 가이드 위치 X 두 번째 예제 오른쪽 코드만 먼저 보면 코드 가이드 위치가 틀렸습니다. 린트 오류가 발생할 거예요.
onClick: () -> Unit = {}, ) { Button( onClick = onClick, modifier = modifier ) { Text(text = text) } } @Preview(showBackground = true) @Composable private fun PreviewHomeButton() { Row { HomeButton( onClick = {}, text = "Save", modifier = Modifier .weight(1f) ) HomeButton( onClick = {}, modifier = Modifier .weight(1f) .padding(start = 10.dp), text = "x", ) } } Components의 Modifier 위치는 Optional Parameter 중 가장 첫 번째 위치로 Suggestion Example 2 두 번째 예제 구글 문서상 지금과 같이 필수 값은 modifier 위에, 옵션 값은 modifier 아래에 두는 것입니다. 어떻게 보면 기준을 잡고 위/아래로 분리한 것입니다.
onClick: () -> Unit = {}, ) { Button( onClick = onClick, modifier = modifier ) { Text(text = text) } } @Preview(showBackground = true) @Composable private fun PreviewHomeButton() { Row { HomeButton( onClick = {}, text = "Save", modifier = Modifier .weight(1f) ) HomeButton( onClick = {}, modifier = Modifier .weight(1f) .padding(start = 10.dp), text = "x", ) } } Components의 Modifier 위치는 Optional Parameter 중 가장 첫 번째 위치로 개인적으로 사용할 때도 Modifier는 맨 마지막에 둔다. • Modifier가 길어지기 때문에 아래로 내림 Suggestion Example 2 코드 가이드 위치 개인적인 위치 두 번째 예제 그럼 사용할 때는? 구글 가이드는 따로 없는 것 같고, 순서대로 표기하는 방식을 사용하고 있습니다. 개인적으론 Modifier가 길어지는 것이 싫어서 항상 최하단에 위치시키고 있습니다.
onClick: () -> Unit = {}, ) { Button( onClick = onClick, ) { Text( text = text, modifier = modifier ) } } Modifier는 Container(최상위 Components)에 적용하지 않으면 의도하지 않은 UI로 노출 Maybe Example 3 세 번째 예제 Modifier는 어디에 적용하는 것이 좋을까요? Composable 함수 아무 곳에서 나 외부에서 주입한 Modifier를 적용하는 것이 맞을까요?
onClick: () -> Unit = {}, ) { Button( onClick = onClick, ) { Text( text = text, modifier = modifier ) } } Modifier는 Container(최상위 Components)에 적용하지 않으면 의도하지 않은 UI로 노출 Maybe Example 3 의도하지 않은 UI 노출 유발 세 번째 예제 일단 임의로 Text 부분에 넣어보겠습니다. 그럼 외부에서 설정한 Modifier 정보는 button Container가 아닌 Text에 포함됩니다. 대부분의 사람들은 당연히 container에 적용될 거라고 기대할 것이지만 그렇지 않았습니다.
TextStyle 등을 한 번에 적용하는 것이 가능 • Button ◦ 디자인 가이드에 따라 Button을 공통화하고, 디자인을 적용할 수 있다(사용의 편리) • TextField ◦ 디자인 가이드에 따라 TextField을 공통화 목적 Design Components 디자인 컴포넌트를 사용하는 이유는 명확합니다. 우리 서비스 자체에서 사용하는 디자인을 포함하여 컴포넌트 화 시키는 부분입니다. 내부에서 사용하는 디자인 시스템이 없더라도 디자인 컴포넌트는 만들어 두는 것이 좋습니다. Text를 예로 들면 내부에서만 사용하는 textStyle을 강제로 적용할 수도 있고, Button 색상 가이드에 맞게 적용하는 것 역시 가능합니다.
Modifier = Modifier, enabled: Boolean = true, contentPadding: PaddingValues = ExampleButtonDefaults.ContentPadding, textStyle: TextStyle = ExampleButtonDefaults.defaultTextStyle, colors: ExampleButtonColors = ExampleButtonDefaults.filledButtonColors(), ) { Button( onClick = onClick, colors = ButtonDefaults.buttonColors( containerColor = colors.containerColor().value, contentColor = colors.contentColor().value, ), modifier = modifier .defaultMinSize(minHeight = ExampleButtonDefaults.MinHeight) ) { Text( text = text, style = textStyle, color = colors.contentColor().value, modifier = Modifier .padding(contentPadding) ) } } Defaults 정의 값을 초기화 코틀린의 함수에는 Defaults 정의가 가능한데, 컴포즈에서도 동일하게 사용합니다. 여기서 Defaults를 나눌 때는 크게 2가지가 있는데 누가 보아도 이해할 수 있는 true/false와 같은 기본값, 그리고 Defaults를 통해 명확한 이름을 제공하는 형태가 있습니다.
PaddingValues(start = 16.dp, end = 16.dp, top = 12.dp, bottom = 12.dp) val defaultTextStyle: TextStyle @Composable get() = MaterialTheme.typography.bodyMedium @Composable fun filledButtonColors( containerColor: Color = MaterialTheme.colorScheme.secondary, contentColor: Color = MaterialTheme.colorScheme.onSecondary, ): ExampleButtonColors = ExampleButtonColors( containerColor = containerColor, contentColor = contentColor, ) } @Immutable data class ExampleButtonColors internal constructor( private val containerColor: Color, private val contentColor: Color, ) { @Composable internal fun containerColor(): State<Color> { return rememberUpdatedState(containerColor) } @Composable internal fun contentColor(): State<Color> { return rememberUpdatedState(contentColor) } } Defaults에는 값을 가지고 구분하기 힘들거나, colors와 같은 정보들을 포함합니다. MinHeight를 미리 지정하거나, shape 역시 미리 지정할 수 있습니다. Color 정보는 기존 머트리얼에 포함되어 있는 정보를 참고하여 구성하시면 도움 됩니다.
Modifier = Modifier, enabled: Boolean = true, contentPadding: PaddingValues = ExampleButtonDefaults.ContentPadding, textStyle: TextStyle = ExampleButtonDefaults.defaultTextStyle, colors: ExampleButtonColors = ExampleButtonDefaults.filledButtonColors(), ) • 필수값, 옵션값으로 나뉘고 • 옵션값은 inline과 defaults로 구분하여 사용 Parameters 종류 Parameters 필수로 입력 필수 값은 값이 비어있으니 외부에서 필수로 적용하는 값을 말하고
Modifier = Modifier, enabled: Boolean = true, contentPadding: PaddingValues = ExampleButtonDefaults.ContentPadding, textStyle: TextStyle = ExampleButtonDefaults.defaultTextStyle, colors: ExampleButtonColors = ExampleButtonDefaults.filledButtonColors(), ) • 필수값, 옵션값으로 나뉘고 • 옵션값은 inline과 defaults로 구분하여 사용 Parameters 종류 Parameters 옵션 inline 옵션 값 중에 inline은 누가 보아도 바로 이해할 수 있는 값을 나타냅니다.
한 값을 제외한 부분을 Defaults로 정의 • 색상 정보, 버튼의 폰트 정보, Padding 등을 정의 ◦ 디자인 가이드상 크게 변하지 않는 부분을 정의 ◦ 상황에 따라 외부에 노출하여 수정 가능하도록 정의 inline Defaults Parameters 종류 Parameters defaults는 한눈에 예측 가능한 값을 제외한 부분을 정의합니다. 상황에 따라 외부에 노출하거나, 노출하지 않을 수 있습니다.(노출하지 않는다면 강제 적용이라고 보면 됩니다.)
값의 적용 빈도가 최소한 인 경우 활용 compositionLocal staticCompositonLocal 2개의 compositionLocal CompositionLocal CompositionLocal은 크게 2가지가 제공됩니다. compositionLocal은 새로운 값의 적용 빈도가 높을 경우 활용하는 것이고, staticCompositionLocal은 새로운 값의 적용 빈도가 최소한인 경우 활용할 수 있습니다. 두 개의 recomposition 범위가 명확히 다르기 때문에 테스트해 보시면 명확한 차이를 볼 수 있습니다.
Surface( onClick = onClick, modifier = modifier.semantics { role = Role.Button }, enabled = enabled, shape = shape, color = containerColor, contentColor = contentColor, shadowElevation = shadowElevation, border = border, interactionSource = interactionSource ) { ProvideContentColorTextStyle( contentColor = contentColor, textStyle = MaterialTheme.typography.labelLarge ) { Row( Modifier.defaultMinSize( minWidth = ButtonDefaults.MinWidth, minHeight = ButtonDefaults.MinHeight ) .padding(contentPadding), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, content = content ) } } } Local 정보는 이 위치에 사용 내부 버튼에는 이와 같이 Local 처리하는 부분이 포함되어 있습니다. ProvideContentColorTextStyle이라는 함수를 사용하고 있습니다.
() -> Unit ) { val mergedStyle = LocalTextStyle.current.merge(textStyle) CompositionLocalProvider( LocalContentColor provides contentColor, LocalTextStyle provides mergedStyle, content = content ) } 이를 따라가보면 2개의 LocalContentColor, LocalTextStyle을 지정하고 있습니다. 이때 주의해야 할 부분이 있는데 꼭 merge 형태를 취해야 합니다. 단순히 값을 바꾸는 것이 아니라 기존 값과 사용자가 추가한 값 중 사용자가 추가한 값에 대한 우선순위가 높아야 합니다.
사용 ◦ val mergedStyle = LocalTextStyle.current.merge(textStyle) 사용시 주의 CompositionLocal 사용 시 주의해야 할 부분이 그 부분입니다. 구글에서는 compositionLocal 사용을 지양하라고 하지만 사용해야 할 경우가 생깁니다. 이때는 무조건 외부에서 주입하는 값의 우선순위가 높도록 지정해야 합니다. 꼭 필요한 정보가 아니면 사용하지 않는 것도 하나의 예입니다.
= LocalContext.current CompositionLocalProvider( LocalWebOwner provides WebView(context) ) { Scaffold() { Box { NavHost( navController = navController, startDestination = NavigationSample.Trigger.HOME.name, ) { composable(route = NavigationSample.Trigger.HOME.name) { HomeScreenThree() } composable(route = NavigationSample.Trigger.WEB.name) { WebScreen() } } } } } NavController 코드 위치 중요 Don’t Navigation 이 위치가 중요 바로 navController에서 값을 불러오는 currentBackStackEntry 부분입니다. 이 위치가 지금 오른쪽과 같이 작성되어 있다면 왼쪽처럼 화면에 흰 바탕이 보였다가 웹 로드가 다시 될 것입니다. 이유는 리컴포지션 시작 부분이 CompositionLocal 위에서 시작되어 CompositionLocal 도 다시 부르게 된다는 점입니다. static으로 작성되어 있을 경우 영향의 범위가 하위 모든 컴포넌트에 영향을 미치게 됩니다.
val navBackStackEntry by navController.currentBackStackEntryAsState() val currentDestination = navBackStackEntry?.destination Scaffold { Box { NavHost( navController = navController, startDestination = NavigationSample.Trigger.HOME.name, ) { composable(route = NavigationSample.Trigger.HOME.name) { HomeScreenThree() } composable(route = NavigationSample.Trigger.WEB.name) { WebScreen() } } } } } 이런 경우 CompositionLocalProvider 정의 안에 navController 사용 부분을 포함해야 함 Use Navigation 리컴포지션이 민감하니 이런 부분을 주의하여 지금과 같이 꼭! 위치를 조정해야 합니다.
val navBackStackEntry by navController.currentBackStackEntryAsState() val currentDestination = navBackStackEntry?.destination Scaffold { Box { NavHost( navController = navController, startDestination = NavigationSample.Trigger.HOME.name, ) { composable(route = NavigationSample.Trigger.HOME.name) { HomeScreenThree() } composable(route = NavigationSample.Trigger.WEB.name) { WebScreen() } } } } } 이런 경우 CompositionLocalProvider 정의 안에 navController 사용 부분을 포함해야 함 Use Navigation 여기로 이동 안으로 이동하면 웹뷰 객체는 살아있고, 웹만 리로드 함을 알 수 있습니다.
과도한 분리는 오히려 유지 보수성을 떨어지도록 한다 • 개인적으로는 ◦ Screen ◦ Screen에 해당하는 Components ▪ 해당 Screen에서 분리하기 위함 ◦ Design Components ▪ 공통으로 관리하고 대부분 디자인 시스템으로 관리 정리하면 Conclusion