<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>JSJH의 블로그</title>
    <link>https://jsjh3363.tistory.com/</link>
    <description>jsjh 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Tue, 2 Jun 2026 05:42:39 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>JSJH._.</managingEditor>
    <image>
      <title>JSJH의 블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/7390491/attach/634d51f43acc42f499c221d7bb4c30d0</url>
      <link>https://jsjh3363.tistory.com</link>
    </image>
    <item>
      <title>[6주차].앱 완성 정리와 배포</title>
      <link>https://jsjh3363.tistory.com/48</link>
      <description>&lt;h1 data-heading=&quot;[6주차] 앱 완성 정리와 배포&quot;&gt;[6주차] 앱 완성 정리와 배포&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 주는 지금까지 만든 단어장 앱을 최종 정리하고, 실제 스마트폰에 설치할 수 있는 APK 파일로 추출하는 마지막 수업입니다.&lt;br /&gt;앱 이름과 아이콘을 직접 변경하며 나만의 앱 형태로 마무리하고, 1~5주차 동안 배운 Kotlin, Jetpack Compose, 앱 개발 흐름을 간단히 되돌아봅니다.&lt;/p&gt;
&lt;h2 data-heading=&quot;오늘의 목표&quot; data-ke-size=&quot;size26&quot;&gt;오늘의 목표&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;1~5주차 동안 만든 단어장 앱의 구조를 정리한다.&lt;/li&gt;
&lt;li&gt;Kotlin과 Jetpack Compose의 핵심 개념을 복습한다.&lt;/li&gt;
&lt;li&gt;앱의 실제 이름을 원하는 이름으로 변경한다.&lt;/li&gt;
&lt;li&gt;홈 화면에 표시되는 앱 아이콘 이미지를 교체한다.&lt;/li&gt;
&lt;li&gt;APK 파일을 추출하여 스마트폰에 직접 설치한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-heading=&quot;1. 지금까지 만든 앱 되돌아보기&quot; data-ke-size=&quot;size26&quot;&gt;1. 지금까지 만든 앱 되돌아보기&lt;/h2&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1주차: 안드로이드 스튜디오 시작과 첫 화면 띄우기
2주차: 단어 카드 UI 만들기
3주차: 리스트, 정렬, 뜻 숨기기, 삭제 기능 만들기
4주차: 새 단어 추가 팝업창 만들기
5주차: 플래시카드 학습 화면 이동 만들기
6주차: 앱 이름과 아이콘 변경, APK 배포
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 단순한 텍스트 화면에서 시작했지만, 현재는 단어 추가, 삭제, 정렬, 뜻 숨기기, 플래시카드 학습까지 가능한 하나의 앱 형태가 되었습니다.&lt;br /&gt;이번 시간에는 기능을 더 추가하기보다, 지금까지 만든 앱을 정리하고 실제 스마트폰에 설치하는 것을 목표로 합니다.&lt;/p&gt;
&lt;h2 data-heading=&quot;2. 현재 앱의 전체 구조&quot; data-ke-size=&quot;size26&quot;&gt;2. 현재 앱의 전체 구조&lt;/h2&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;MainActivity.kt 
└─ 앱의 시작 지점
data 패키지 
├─ Word.kt 
└─ WordRepository.kt
ui 패키지 
├─ HomeScreen.kt 
└─ StudyScreen.kt
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-heading=&quot;1. &amp;#96;MainActivity.kt&amp;#96;&quot; data-ke-size=&quot;size23&quot;&gt;1. MainActivity.kt&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱이 처음 실행될 때 시작되는 파일입니다.&lt;br /&gt;setContent { } 안에서 Jetpack Compose 화면을 실제 앱 화면으로 띄웁니다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;setContent {    ...}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 5주차부터는 NavHost를 사용하여 홈 화면과 학습 화면 사이의 이동을 관리합니다.&lt;/p&gt;
&lt;h3 data-heading=&quot;2. &amp;#96;data&amp;#96; 패키지&quot; data-ke-size=&quot;size23&quot;&gt;2. data 패키지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱에서 사용하는 데이터를 관리하는 공간입니다.&lt;/p&gt;
&lt;h4 data-heading=&quot;&amp;#96;Word.kt&amp;#96;&quot; data-ke-size=&quot;size20&quot;&gt;Word.kt&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단어 하나의 정보를 저장하는 데이터 설계도입니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;data class Word(    
	val id: Long,    
	val spelling: String,    
	val meaning: String,    
	val isHidden: Boolean = false)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Word는 단어 카드 한 장이 가져야 하는 정보를 하나로 묶어둔 형태입니다.&lt;/p&gt;
&lt;h4 data-heading=&quot;&amp;#96;WordRepository.kt&amp;#96;&quot; data-ke-size=&quot;size20&quot;&gt;WordRepository.kt&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단어 목록을 저장하고 관리하는 데이터 창고 역할을 합니다.&lt;br /&gt;이 파일에서는 다음과 같은 기능을 처리합니다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;단어 목록 저장
단어 추가
단어 삭제
뜻 숨김 상태 변경
앱 재실행 시 데이터 불러오기
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-heading=&quot;3. &amp;#96;ui&amp;#96; 패키지&quot; data-ke-size=&quot;size23&quot;&gt;3. ui 패키지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 화면에 보이는 UI를 만드는 공간입니다.&lt;/p&gt;
&lt;h4 data-heading=&quot;&amp;#96;HomeScreen.kt&amp;#96;&quot; data-ke-size=&quot;size20&quot;&gt;HomeScreen.kt&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단어 목록이 보이는 메인 화면입니다.&lt;br /&gt;주요 기능은 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;단어 리스트 표시
최신순 / 알파벳순 정렬
뜻 숨기기
단어 삭제
새 단어 추가 
팝업 열기
학습 화면으로 이동
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-heading=&quot;&amp;#96;StudyScreen.kt&amp;#96;&quot; data-ke-size=&quot;size20&quot;&gt;StudyScreen.kt&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플래시카드 방식으로 단어를 학습하는 화면입니다.&lt;br /&gt;주요 기능은 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;단어를 카드 형태로 표시
카드를 누르면 뜻 표시
이전 / 다음 단어 이동
마지막 카드에서 학습 종료
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-heading=&quot;3. Kotlin 핵심 문법 복습&quot; data-ke-size=&quot;size26&quot;&gt;3. Kotlin 핵심 문법 복습&lt;/h2&gt;
&lt;h3 data-heading=&quot;1. &amp;#96;val&amp;#96;과 &amp;#96;var&amp;#96;&quot; data-ke-size=&quot;size23&quot;&gt;1. val과 var&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;val words = WordRepository.words
var sortType by remember { mutableStateOf(SortType.LATEST) }
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;val은 한 번 정하면 바뀌지 않는 값입니다.&lt;br /&gt;var는 나중에 값이 바뀔 수 있는 변수입니다.&lt;br /&gt;앱에서는 사용자가 버튼을 누르거나 입력을 하면 값이 바뀌는 경우가 많기 때문에 var를 자주 사용합니다.&lt;/p&gt;
&lt;h3 data-heading=&quot;2. &amp;#96;data class&amp;#96;&quot; data-ke-size=&quot;size23&quot;&gt;2. data class&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;data class Word(    
	val id: Long,    
	val spelling: String,    
	val meaning: String,    
	val isHidden: Boolean = false
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;data class는 여러 데이터를 하나의 묶음으로 표현할 때 사용합니다.&lt;br /&gt;이 앱에서는 단어 하나를 표현하기 위해 Word라는 데이터 클래스를 만들었습니다.&lt;/p&gt;
&lt;h3 data-heading=&quot;3. &amp;#96;object&amp;#96;&quot; data-ke-size=&quot;size23&quot;&gt;3. object&lt;/h3&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;object WordRepository {    
	val words = mutableStateListOf&amp;lt;Word&amp;gt;()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;object는 앱 전체에서 하나만 존재하는 객체를 만들 때 사용합니다.&lt;br /&gt;WordRepository는 여러 화면에서 함께 사용하는 단어 데이터 창고 역할을 합니다.&lt;/p&gt;
&lt;h3 data-heading=&quot;4. &amp;#96;if&amp;#96;&quot; data-ke-size=&quot;size23&quot;&gt;4. if&lt;/h3&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;text = if (word.isHidden) &quot;●●●●●&quot; else word.meaning
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;if를 사용하면 조건에 따라 다른 값을 보여줄 수 있습니다.&lt;br /&gt;이 코드에서는 isHidden 값에 따라 단어의 뜻을 숨기거나 보여줍니다.&lt;/p&gt;
&lt;h3 data-heading=&quot;5. 람다 &amp;#96;{ }&amp;#96;&quot; data-ke-size=&quot;size23&quot;&gt;5. 람다 { }&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;onClick = { isSheetOpen = true }
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;람다는 나중에 실행할 코드를 중괄호 안에 넣어두는 방식입니다.&lt;br /&gt;앱 개발에서는 버튼 클릭, 화면 이동, 데이터 변경 같은 이벤트를 처리할 때 자주 사용됩니다.&lt;/p&gt;
&lt;h2 data-heading=&quot;4. Jetpack Compose 핵심 개념 복습&quot; data-ke-size=&quot;size26&quot;&gt;4. Jetpack Compose 핵심 개념 복습&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jetpack Compose는 화면을 함수처럼 구성하는 Android UI 개발 도구입니다.&lt;/p&gt;
&lt;h3 data-heading=&quot;1. &amp;#96;@Composable&amp;#96;&quot; data-ke-size=&quot;size23&quot;&gt;1. @Composable&lt;/h3&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;@Composablefun HomeScreen() {    ...}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Composable이 붙은 함수는 화면을 그리는 함수입니다.&lt;br /&gt;일반 함수가 계산이나 처리를 담당한다면, Composable 함수는 실제로 눈에 보이는 UI를 만듭니다.&lt;/p&gt;
&lt;h3 data-heading=&quot;2. &amp;#96;Modifier&amp;#96;&quot; data-ke-size=&quot;size23&quot;&gt;2. Modifier&lt;/h3&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;Modifier    
	.fillMaxSize()    
	.background(Color(0xFFF5F5F5))    
	.padding(24.dp)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Modifier는 UI의 크기, 여백, 배경, 정렬 등을 조절하는 도구입니다.&lt;br /&gt;자주 사용한 Modifier는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;fillMaxSize()  = 화면 전체 채우기
fillMaxWidth() = 가로 전체 채우기
padding()      = 여백 주기
background()   = 배경색 지정하기
weight()       = 남은 공간을 비율로 나누기
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-heading=&quot;3. 상태 State&quot; data-ke-size=&quot;size23&quot;&gt;3. 상태 State&lt;/h3&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;var sortType by remember { mutableStateOf(SortType.LATEST) }
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compose에서는 화면이 바뀌어야 할 때 상태값을 사용합니다.&lt;br /&gt;상태값이 바뀌면, 그 상태값을 사용하는 UI가 자동으로 다시 그려집니다.&lt;br /&gt;예를 들어 정렬 버튼을 누르면 sortType 값이 바뀌고, 단어 리스트가 새로운 정렬 기준에 맞게 다시 그려집니다.&lt;/p&gt;
&lt;h3 data-heading=&quot;4. 조건부 화면 표시&quot; data-ke-size=&quot;size23&quot;&gt;4. 조건부 화면 표시&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;if (isSheetOpen) {    
	AddWordSheet(onDismiss = { isSheetOpen = false })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compose에서는 화면을 직접 보이게 하거나 숨기는 명령을 사용하기보다, 상태값에 따라 화면을 그릴지 말지를 결정합니다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;isSheetOpen == true  &amp;rarr; 팝업창 표시
isSheetOpen == false &amp;rarr; 팝업창 표시 안 함
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-heading=&quot;5. 화면 이동 Navigation&quot; data-ke-size=&quot;size23&quot;&gt;5. 화면 이동 Navigation&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;navController.navigate(&quot;study&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;navigate()는 다른 화면으로 이동할 때 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;navController.popBackStack()
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;popBackStack()은 이전 화면으로 돌아갈 때 사용합니다.&lt;br /&gt;5주차에서는 이 기능을 사용하여 홈 화면에서 플래시카드 학습 화면으로 이동하고, 다시 홈 화면으로 돌아오는 구조를 만들었습니다.&lt;/p&gt;
&lt;h2 data-heading=&quot;5. 앱 개발 전체 흐름&quot; data-ke-size=&quot;size26&quot;&gt;5. 앱 개발 전체 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 단어장 앱은 실제 앱 개발 과정을 작게 줄여서 경험해본 예시입니다.&lt;br /&gt;보통통 앱 개발 흐름은 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 만들 앱을 구상한다.
2. 화면 구조를 설계한다.
3. 필요한 데이터를 정한다.
4. UI 화면을 만든다.
5. 버튼과 기능을 연결한다.
6. 데이터를 저장한다.
7. 화면 이동을 붙인다.
8. 테스트하며 오류를 수정한다.
9. 앱 이름과 아이콘을 변경한다.
10. APK 파일로 추출하여 설치한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-heading=&quot;6. 앱 이름 바꾸기 &amp;#96;strings.xml&amp;#96;&quot; data-ke-size=&quot;size26&quot;&gt;6. 앱 이름 바꾸기 strings.xml&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 이름은 스마트폰에 설치했을 때 홈 화면이나 앱 목록에 표시되는 이름입니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;왼쪽 트리에서 app &amp;gt; res &amp;gt; values &amp;gt; strings.xml 파일을 엽니다.&lt;/li&gt;
&lt;li&gt;아래 코드를 찾습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;&amp;lt;string name=&quot;app_name&quot;&amp;gt;AppdongBootcamp&amp;lt;/string&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;AppdongBootcamp 부분을 원하는 앱 이름으로 바꿉니다.&lt;br /&gt;예시:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;&amp;lt;string name=&quot;app_name&quot;&amp;gt;나만의 단어장&amp;lt;/string&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한글 이름으로 변경해도 됩니다.&lt;/p&gt;
&lt;h2 data-heading=&quot;7. 홈 화면 앱 아이콘 이미지 교체 &amp;#96;Image Asset&amp;#96;&quot; data-ke-size=&quot;size26&quot;&gt;7. 홈 화면 앱 아이콘 이미지 교체 Image Asset&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 스마트폰 홈 화면에 표시되는 앱 아이콘을 변경합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;왼쪽 트리에서 제일 바깥쪽 app 폴더를 우클릭합니다.&lt;/li&gt;
&lt;li&gt;New ➜ Image Asset을 클릭합니다.&lt;/li&gt;
&lt;li&gt;팝업창 중간의 Foreground Layer 탭에서 Path 버튼을 누릅니다.&lt;/li&gt;
&lt;li&gt;미리 준비한 아이콘용 이미지 파일을 선택합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PNG 이미지 추천&lt;/li&gt;
&lt;li&gt;정사각형 이미지 추천&lt;/li&gt;
&lt;li&gt;너무 복잡한 사진보다 단순한 이미지 추천&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이미지가 너무 크면 Resize 슬라이더를 줄여 초록색 안전 구역 안에 맞춥니다.&lt;/li&gt;
&lt;li&gt;오른쪽 아래의 Next를 누릅니다.&lt;/li&gt;
&lt;li&gt;Finish를 누르면 기존 기본 아이콘이 새 아이콘으로 교체됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-heading=&quot;8. 마지막 Build: APK 파일 추출하기&quot; data-ke-size=&quot;size26&quot;&gt;8. 마지막 Build: APK 파일 추출하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Android Studio 없이도 스마트폰에 설치할 수 있는 APK 파일을 만들어봅니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Android Studio 상단 메뉴바에서 Build를 클릭합니다.&lt;/li&gt;
&lt;li&gt;Build Bundle(s) / APK(s) ➜ Build APK(s)를 클릭합니다.&lt;/li&gt;
&lt;li&gt;오른쪽 아래에서 Gradle Build Running 로딩이 끝날 때까지 기다립니다.&lt;/li&gt;
&lt;li&gt;빌드가 완료되면 오른쪽 아래에 작은 팝업이 뜹니다.&lt;/li&gt;
&lt;li&gt;팝업 안의 파란색 locate 글씨를 클릭합니다.&lt;/li&gt;
&lt;li&gt;윈도우 탐색기가 열리면 app-debug.apk 파일을 확인합니다.&lt;/li&gt;
&lt;li&gt;app-debug.apk 파일을 카카오톡 &amp;ldquo;나에게 보내기&amp;rdquo;로 전송합니다.&lt;/li&gt;
&lt;li&gt;스마트폰에서 APK 파일을 다운로드합니다.&lt;/li&gt;
&lt;li&gt;다운로드한 APK 파일을 눌러 설치합니다.&lt;/li&gt;
&lt;li&gt;알 수 없는 출처 앱 설치 허용 경고가 뜨면 허용 후 설치를 진행합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-heading=&quot;9. 마무리&quot; data-ke-size=&quot;size26&quot;&gt;9. 마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 부트캠프에서 만든 앱은 큰 규모의 앱은 아니지만, 앱 개발의 핵심 흐름을 포함하고 있습니다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;화면 만들기
데이터 만들기
리스트 보여주기
버튼 이벤트 처리하기
상태에 따라 화면 바꾸기
데이터 저장하기
화면 이동하기
앱 이름과 아이콘 바꾸기
APK로 설치하기
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 Android 앱 개발을 더 공부하고 싶다면 다음 순서로 이어가면 좋습니다.&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;Kotlin 기본 문법&amp;rarr; Jetpack Compose UI&amp;rarr; 상태 관리&amp;rarr; Navigation&amp;rarr; 데이터 저장&amp;rarr; API 통신&amp;rarr; 서버 / DB 연결&amp;rarr; 로그인&amp;rarr; 배포
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 만든 앱은 스마트폰 내부에 데이터를 저장하는 로컬 앱입니다.&lt;br /&gt;다음 단계에서는 인터넷에서 데이터를 가져오거나, 서버와 연결하여 사용자별 데이터를 저장하는 앱을 만들 수 있습니다.&lt;/p&gt;</description>
      <category>Tech/앱동-kotlin 부트캠프</category>
      <category>Android</category>
      <category>APPDONG</category>
      <category>kotlin</category>
      <author>JSJH._.</author>
      <guid isPermaLink="true">https://jsjh3363.tistory.com/48</guid>
      <comments>https://jsjh3363.tistory.com/48#entry48comment</comments>
      <pubDate>Wed, 20 May 2026 00:36:46 +0900</pubDate>
    </item>
    <item>
      <title>[5주차].플래시카드 화면 이동 (Navigation)</title>
      <link>https://jsjh3363.tistory.com/47</link>
      <description>&lt;h1 data-heading=&quot;[5주차] 플래시카드 화면 이동 (Navigation)&quot;&gt;[5주차] 플래시카드 화면 이동 (Navigation)&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;학습용 플래시카드로 페이지 화면을 전환하는 기능을 붙입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-heading=&quot;오늘의 목표&quot; data-ke-size=&quot;size26&quot;&gt;오늘의 목표&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하단의 학습 시작 버튼을 누르면 플래시카드가 있는 화면으로 분기되어 넘어갑니다.&lt;/li&gt;
&lt;li&gt;넘어간 화면의 플래시카드를 누르면 단어의 뜻이 보입니다.&lt;/li&gt;
&lt;li&gt;돌아가기 버튼을 누르면 다시 메인 단어 리스트로 복귀합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-heading=&quot;1. 5주차 클래스 파일 만들기&quot; data-ke-size=&quot;size26&quot;&gt;1. 5주차 클래스 파일 만들기&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;왼쪽 트리에서 ui 패키지 폴더 우클릭 ➜ New -&amp;gt; Kotlin Class/File 클릭.&lt;/li&gt;
&lt;li&gt;이름 칸에 StudyScreen 입력 후 엔터 ➜ 맨 밑에 있는 &lt;b&gt;5주차 완성 하단 코드 - [C] StudyScreen.kt&lt;/b&gt; 긁어서 통째로 복붙.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-heading=&quot;2. 5주차 화면 이동 네비게이션 연결&quot; data-ke-size=&quot;size26&quot;&gt;2. 5주차 화면 이동 네비게이션 연결&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;ui 패키지 안의 기존 HomeScreen.kt 파일 열기.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;5주차 완성 하단 코드 - [B] HomeScreen.kt&lt;/b&gt; 전체 복붙 후 덮어쓰기. (하단에 버튼 UI 한 줄 생깁니다).&lt;/li&gt;
&lt;li&gt;MainActivity.kt 파일을 엽니다.&lt;/li&gt;
&lt;li&gt;가장 중요한 화면 간의 라우팅 구조를 &lt;b&gt;5주차 완성 하단 코드 - [A] MainActivity.kt&lt;/b&gt; 복붙해서 통째로 덮어쓰기.&lt;/li&gt;
&lt;li&gt;상단 초록색 (Run) 클릭.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-heading=&quot;3. 기본적인 android 문법 (5주차)&quot; data-ke-size=&quot;size26&quot;&gt;3. 기본적인 android 문법 (5주차)&lt;/h2&gt;
&lt;h3 data-heading=&quot;1. &amp;#96;NavHost&amp;#96; 및 &amp;#96;composable&amp;#96;&quot; data-ke-size=&quot;size23&quot;&gt;1. NavHost 및 composable&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jetpack Compose 생태계에서 공식 권장하는 화면 내비게이션 관리 라우팅 컴포넌트입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;NavHost: 내비게이션 그래프 전체를 관리하는 최상위 컨테이너 구역.&lt;/li&gt;
&lt;li&gt;composable(&quot;경로&quot;): 지정된 문자열 경로로 접근할 때 화면에 렌더링될 실제 요소(스크린 컴포넌트)를 바인딩.&lt;/li&gt;
&lt;li&gt;navController.navigate(&quot;경로&quot;): 입력된 시스템 경로값을 라우터 스택에 추가하고 전환을 실행.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-heading=&quot;2. 함수형 파라미터 전달 &amp;#96;() -&amp;gt; Unit&amp;#96;&quot; data-ke-size=&quot;size23&quot;&gt;2. 함수형 파라미터 전달 () -&amp;gt; Unit&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하위 UI 컴포넌트가 직접 화면 이동 권한을 갖지 않도록, 이벤트 제어를 상위 스택으로 위임하는 패턴(State Hoisting)입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HomeScreen 블록 내부에서는 버튼이 클릭되었다는 사실만 콜백으로 알리고, 실질적인 라우터 처리 명령은 최상위 MainActivity에 정의하게끔 람다 함수로 책임 전가.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-heading=&quot;3. &amp;#96;navController.popBackStack()&amp;#96;&quot; data-ke-size=&quot;size23&quot;&gt;3. navController.popBackStack()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드의 화면 이동 기록은 LIFO(Last-In First-Out) 스택(Stack) 메모리 자료구조를 따릅니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가장 최근에 진입한 화면 요소(디스플레이)를 시스템 물리 스택 공간에서 제거(pop)하고, 바로 이전 상태에 렌더링된 메인 화면 컴포넌트로 복귀하게함.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-heading=&quot;4. &amp;#96;mutableIntStateOf(0)&amp;#96;&quot; data-ke-size=&quot;size23&quot;&gt;4. mutableIntStateOf(0)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정수(Int) 데이터 주소값을 추적하는 UI 전용 상태 객체입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단어 인덱스 변수가 증가 혹은 감소할 때마다 해당 상태 참조 포인트에 의존하는 UI 노드 부분만 데이터를 추출하여 빠르게 재렌더링.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-heading=&quot;4. 에러 대응&quot; data-ke-size=&quot;size26&quot;&gt;4. 에러 대응&lt;/h2&gt;
&lt;h3 data-heading=&quot;발생할 수 있는 문제&quot; data-ke-size=&quot;size23&quot;&gt;발생할 수 있는 문제&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;navController 등에서 빨간색 밑줄 무더기 출몰&lt;br /&gt;1주차 때 배웠던 dependencies 추가 시 navigation-compose 문자열을 누락했거나, 우측 상단 파란색 Sync 안 돌렸을 확률이 농후함.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 data-heading=&quot;5주차 완성 하단 코드 (선택 후 전체 덮어쓰기)&quot;&gt;5주차 완성 하단 코드 (선택 후 전체 덮어쓰기)&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MainActivity.kt&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;package com.example.bootcamp_wordlist

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.example.bootcamp_wordlist.data.WordRepository
import com.example.bootcamp_wordlist.ui.HomeScreen
import com.example.bootcamp_wordlist.ui.StudyScreen
import com.example.bootcamp_wordlist.ui.theme.Bootcamp_wordlistTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 앱 시작 시 저장된 단어 불러오기
        WordRepository.load(this)
        enableEdgeToEdge()
        setContent {
            Bootcamp_wordlistTheme {
                val navController = rememberNavController()
                NavHost(
                    navController = navController,
                    startDestination = &quot;home&quot;
                ) {
                    composable(&quot;home&quot;) {
                        HomeScreen(
                            onNavigateToStudy = { navController.navigate(&quot;study&quot;) }
                        )
                    }
                    composable(&quot;study&quot;) {
                        StudyScreen(
                            onNavigateBack = { navController.popBackStack() }
                        )
                    }
                }
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HomeScreen.kt&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;roboconf&quot;&gt;&lt;code&gt;package com.example.bootcamp_wordlist.ui  
  
import androidx.compose.foundation.background  
import androidx.compose.foundation.clickable  
import androidx.compose.foundation.gestures.detectTapGestures  
import androidx.compose.foundation.layout.*  
import androidx.compose.foundation.lazy.LazyColumn  
import androidx.compose.foundation.lazy.items  
import androidx.compose.foundation.shape.CircleShape  
import androidx.compose.foundation.shape.RoundedCornerShape  
import androidx.compose.material.icons.Icons  
import androidx.compose.material.icons.filled.Add  
import androidx.compose.material.icons.filled.Delete  
import androidx.compose.material.icons.outlined.Visibility  
import androidx.compose.material.icons.outlined.VisibilityOff  
import androidx.compose.material3.*  
import androidx.compose.runtime.*  
import androidx.compose.ui.Alignment  
import androidx.compose.ui.Modifier  
import androidx.compose.ui.draw.clip  
import androidx.compose.ui.graphics.Color  
import androidx.compose.ui.input.pointer.pointerInput  
import androidx.compose.ui.platform.LocalContext  
import androidx.compose.ui.text.font.FontWeight  
import androidx.compose.ui.unit.dp  
import androidx.compose.ui.unit.sp  
import com.example.bootcamp_wordlist.data.Word  
import com.example.bootcamp_wordlist.data.WordRepository  
  
enum class SortType { LATEST, ALPHABET }  
  
@Composable  
fun HomeScreen(onNavigateToStudy: () -&amp;gt; Unit) {  
    val context = LocalContext.current  
    val words = WordRepository.words  
    var sortType by remember { mutableStateOf(SortType.LATEST) }  
    var isSheetOpen by remember { mutableStateOf(false) }  
  
    val sortedWords = when (sortType) {  
        SortType.LATEST -&amp;gt; words.sortedByDescending { it.id }  
        SortType.ALPHABET -&amp;gt; words.sortedBy { it.spelling }  
    }  
    Box(modifier = Modifier.fillMaxSize().background(Color(0xFFF5F5F5))) {  
        Column(modifier = Modifier.fillMaxSize()) {  
            // 상단 헤더  
            Column(  
                modifier = Modifier  
                    .fillMaxWidth()  
                    .background(Color(0xFFE0E0E0))  
                    .padding(start = 24.dp, end = 24.dp, top = 48.dp, bottom = 16.dp)  
            ) {  
                Text(text = &quot;단어장&quot;, fontSize = 22.sp, fontWeight = FontWeight.Bold)  
                Spacer(modifier = Modifier.height(16.dp))  
                Row(  
                    modifier = Modifier.fillMaxWidth(),  
                    horizontalArrangement = Arrangement.spacedBy(8.dp)  
                ) {  
                    SortButton(&quot;최신순&quot;, sortType == SortType.LATEST, { sortType = SortType.LATEST }, Modifier.weight(1f))  
                    SortButton(&quot;알파벳순&quot;, sortType == SortType.ALPHABET, { sortType = SortType.ALPHABET }, Modifier.weight(1f))  
                }  
            }            // 단어 리스트  
            LazyColumn(  
                modifier = Modifier.fillMaxWidth().weight(1f),  
                contentPadding = PaddingValues(start = 16.dp, end = 16.dp, top = 12.dp, bottom = 100.dp),  
                verticalArrangement = Arrangement.spacedBy(8.dp)  
            ) {  
                items(sortedWords, key = { it.id }) { word -&amp;gt;  
                    WordItem(word = word)  
                }  
            }        }        // 하단 버튼 영역  
        Row(  
            modifier = Modifier  
                .align(Alignment.BottomCenter)  
                .fillMaxWidth()  
                .navigationBarsPadding()  
                .padding(horizontal = 24.dp, vertical = 24.dp),  
            horizontalArrangement = Arrangement.SpaceBetween,  
            verticalAlignment = Alignment.CenterVertically  
        ) {  
            OutlinedButton(  
                onClick = onNavigateToStudy,  
                shape = RoundedCornerShape(50),  
                modifier = Modifier.height(48.dp)  
            ) {  
                Text(&quot;학습 시작&quot;, fontWeight = FontWeight.Medium, color = Color.Black)  
            }  
            FloatingActionButton(  
                onClick = { isSheetOpen = true },  
                shape = CircleShape,  
                containerColor = Color.White,  
                contentColor = Color.DarkGray  
            ) {  
                Icon(Icons.Default.Add, contentDescription = &quot;단어 추가&quot;)  
            }  
        }        if (isSheetOpen) {  
            AddWordSheet(onDismiss = { isSheetOpen = false })  
        }  
    }  
}  
  
@Composable  
fun SortButton(text: String, isSelected: Boolean, onClick: () -&amp;gt; Unit, modifier: Modifier = Modifier) {  
    Button(  
        onClick = onClick,  
        modifier = modifier.height(40.dp),  
        shape = RoundedCornerShape(12.dp),  
        colors = ButtonDefaults.buttonColors(  
            containerColor = if (isSelected) Color(0xFF1A1A1A) else Color.White,  
            contentColor = if (isSelected) Color.White else Color(0xFF666666)  
        ),  
        elevation = ButtonDefaults.buttonElevation(0.dp)  
    ) {  
        Text(text = text, fontSize = 14.sp)  
    }  
}  
  
@Composable  
fun WordItem(word: Word) {  
    val context = LocalContext.current  
    Card(  
        modifier = Modifier.fillMaxWidth(),  
        shape = RoundedCornerShape(16.dp),  
        colors = CardDefaults.cardColors(containerColor = Color.White),  
        elevation = CardDefaults.cardElevation(0.dp)  
    ) {  
        Row(  
            modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 14.dp),  
            verticalAlignment = Alignment.CenterVertically  
        ) {  
            Text(text = word.spelling, fontSize = 18.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))  
            Text(  
                text = if (word.isHidden) &quot;●●●●●&quot; else word.meaning,  
                fontSize = 14.sp,  
                color = if (word.isHidden) Color(0xFFCCCCCC) else Color(0xFF888888),  
                modifier = Modifier.weight(1f)  
            )  
            IconButton(onClick = { WordRepository.toggleHidden(context, word.id) }) {  
                Icon(  
                    imageVector = if (word.isHidden) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,  
                    contentDescription = if (word.isHidden) &quot;뜻 보이기&quot; else &quot;뜻 숨기기&quot;,  
                    tint = Color(0xFFAAAAAA)  
                )  
            }  
            IconButton(onClick = { WordRepository.deleteWord(context, word.id) }) {  
                Icon(Icons.Default.Delete, contentDescription = &quot;삭제&quot;, tint = Color(0xFFAAAAAA))  
            }  
        }    }}  
  
@Composable  
fun AddWordSheet(onDismiss: () -&amp;gt; Unit) {  
    val context = LocalContext.current  
    var spelling by remember { mutableStateOf(&quot;&quot;) }  
    var meaning by remember { mutableStateOf(&quot;&quot;) }  
  
    Box(  
        modifier = Modifier  
            .fillMaxSize()  
            .background(Color.Black.copy(alpha = 0.4f))  
            .clickable { onDismiss() }  
    ) {  
        Column(  
            modifier = Modifier  
                .align(Alignment.BottomCenter)  
                .fillMaxWidth()  
                .clip(RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp))  
                .background(Color.White)  
                .navigationBarsPadding()  
                .imePadding()  
                .pointerInput(Unit) {  
                    detectTapGestures { }  
                }                .padding(24.dp)  
        ) {  
            Box(  
                modifier = Modifier  
                    .width(48.dp).height(4.dp)  
                    .clip(RoundedCornerShape(2.dp))  
                    .background(Color(0xFFDDDDDD))  
                    .align(Alignment.CenterHorizontally)  
            )  
            Spacer(modifier = Modifier.height(20.dp))  
            Text(&quot;새 단어 추가&quot;, fontSize = 20.sp, fontWeight = FontWeight.Bold)  
            Spacer(modifier = Modifier.height(20.dp))  
            OutlinedTextField(  
                value = spelling, onValueChange = { spelling = it },  
                placeholder = { Text(&quot;영단어 입력&quot;) },  
                modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(12.dp), singleLine = true  
            )  
            Spacer(modifier = Modifier.height(12.dp))  
            OutlinedTextField(  
                value = meaning, onValueChange = { meaning = it },  
                placeholder = { Text(&quot;뜻 입력&quot;) },  
                modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(12.dp), singleLine = true  
            )  
            Spacer(modifier = Modifier.height(20.dp))  
            Button(  
                onClick = {  
                    if (spelling.isNotBlank() &amp;amp;&amp;amp; meaning.isNotBlank()) {  
                        WordRepository.addWord(context, spelling.trim(), meaning.trim())  
                        onDismiss()  
                    }  
                },  
                modifier = Modifier.fillMaxWidth().height(52.dp),  
                shape = RoundedCornerShape(12.dp),  
                enabled = spelling.isNotBlank() &amp;amp;&amp;amp; meaning.isNotBlank(),  
                colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFE0E0E0), contentColor = Color(0xFF333333))  
            ) {  
                Text(&quot;저장&quot;, fontWeight = FontWeight.Bold, fontSize = 16.sp)  
            }  
            Spacer(modifier = Modifier.height(16.dp))  
        }  
    }}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;StudyScreen.kt (새 파일)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;package com.example.bootcamp_wordlist.ui  
  
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.icons.Icons  
import androidx.compose.material.icons.automirrored.filled.ArrowBack  
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.FontWeight  
import androidx.compose.ui.unit.dp  
import androidx.compose.ui.unit.sp  
import com.example.bootcamp_wordlist.data.WordRepository  
  
@Composable  
fun StudyScreen(onNavigateBack: () -&amp;gt; Unit) {  
    // remember { ... } = 화면이 다시 그려져도 이 목록이 다시 섞이지 않게 한 번만 실행  
    // shuffled() = 매번 접속 시 랜덤으로 섞어서 다양한 순서로 공부하도록  
    val words = remember { WordRepository.words.toList().shuffled() }  
  
    // mutableIntStateOf = Int 전용 State (일반 mutableStateOf보다 정수에 연산 최적화)  
    // 현재 보여주는 단어의 위치 (몇 번째 카드인지)  
    var currentIndex by remember { mutableIntStateOf(0) }  
  
    // 카드를 탭했을 때 뜻을 보여줄지 말지 결정하는 플래그  
    var isRevealed by remember { mutableStateOf(false) }  
  
    // 단어가 한 개도 없으면 안내 화면을 보여주고 여기서 함수를 끝냄 (return)    // return = 이 줄 이후 코드는 실행하지 않고 바로 종료  
    if (words.isEmpty()) {  
        Box(  
            modifier = Modifier.fillMaxSize().background(Color(0xFFF5F5F5)),  
            contentAlignment = Alignment.Center  
        ) {  
            Column(horizontalAlignment = Alignment.CenterHorizontally) {  
                Text(&quot;학습할 단어가 없습니다.&quot;, color = Color.Gray)  
                Spacer(modifier = Modifier.height(16.dp))  
                Button(onClick = onNavigateBack) { Text(&quot;돌아가기&quot;) }  
            }        }        return  
    }  
    // words[currentIndex] = 현재 인덱스에 해당하는 단어 하나  
    val currentWord = words[currentIndex]  
    // 마지막 카드 여부 판단 (인덱스는 0부터 시작 &amp;rarr; 마지막 = 전체 개수 - 1)    
    val isLastCard = currentIndex == words.size - 1  
    Column(modifier = Modifier.fillMaxSize().background(Color(0xFFF5F5F5))) {  
        // 상단 앱바: 뒤로가기 + 제목 + 진행 번호 뱃지  
        Row(  
            modifier = Modifier  
                .fillMaxWidth()  
                .background(Color(0xFFE0E0E0))  
                .padding(start = 16.dp, end = 24.dp, top = 48.dp, bottom = 16.dp),  
            verticalAlignment = Alignment.CenterVertically  
        ) {  
            // 뒤로가기: 누르면 onNavigateBack() 호출 &amp;rarr; 홈스크린으로 돌아감  
            IconButton(onClick = onNavigateBack) {  
                Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = &quot;뒤로가기&quot;)  
            }  
            Text(  
                text = &quot;플래시카드&quot;,  
                fontSize = 18.sp,  
                fontWeight = FontWeight.Bold,  
                modifier = Modifier.weight(1f) // 뱃지를 반대쪽 끝으로 밀어냄  
            )  
            // 진행 번호 뱃지 (ex: &quot;2/5&quot;)            // currentIndex + 1: 인덱스는 0부터 시작하니 +1 해서 표시  
            Box(  
                modifier = Modifier  
                    .background(Color(0xFF999999), shape = RoundedCornerShape(8.dp))  
                    .padding(horizontal = 12.dp, vertical = 4.dp)  
            ) {  
                Text(  
                    text = &quot;${currentIndex + 1}/${words.size}&quot;,  
                    color = Color.White,  
                    fontWeight = FontWeight.Bold,  
                    fontSize = 14.sp  
                )  
            }  
        }        // 플래시카드 영역: 남는 세로 공간 전체를 차지하고 카드를 가운데 정렬  
        Box(  
            modifier = Modifier.fillMaxWidth().weight(1f).padding(24.dp),  
            contentAlignment = Alignment.Center  
        ) {  
            Card(  
                modifier = Modifier  
                    .fillMaxWidth()  
                    .fillMaxHeight(0.7f) // 영역 높이의 70%만 사용 (너무 크면 이상해 보이는 것 방지)  
                    .clickable { isRevealed = !isRevealed }, // 탭하면 isRevealed 뒤집기  
                shape = RoundedCornerShape(32.dp),  
                colors = CardDefaults.cardColors(containerColor = Color.White),  
                elevation = CardDefaults.cardElevation(0.dp)  
            ) {  
                Column(  
                    modifier = Modifier.fillMaxSize().padding(32.dp),  
                    horizontalAlignment = Alignment.CenterHorizontally,  
                    verticalArrangement = Arrangement.Center // 세로 기준 정가운데 정렬  
                ) {  
                    // 영단어: 항상 보임  
                    Text(text = currentWord.spelling, fontSize = 36.sp, fontWeight = FontWeight.Bold)  
                    Spacer(modifier = Modifier.height(32.dp))  
                    // HorizontalDivider = 영단어와 뜻 사이를 구분하는 가로 선  
                    HorizontalDivider(color = Color(0xFFEEEEEE), thickness = 1.dp)  
                    Spacer(modifier = Modifier.height(32.dp))  
                    // isRevealed 상태에 따라 다른 텍스트 표시  
                    // 탭하기 전: 힌트 문구 표시 / 탭한 후: 실제 뜻 표시  
                    if (isRevealed) {  
                        Text(text = currentWord.meaning, fontSize = 24.sp, fontWeight = FontWeight.Medium, color = Color(0xFF555555))  
                    } else {  
                        Text(text = &quot;탭하면 뜻이 보여요&quot;, fontSize = 13.sp, color = Color(0xFFCCCCCC))  
                    }  
                }  
            }        }        // 이전 / 다음 버튼  
        Row(  
            modifier = Modifier  
                .fillMaxWidth()  
                .navigationBarsPadding() // 하단 네비게이션 바 위에 배치  
                .padding(start = 24.dp, end = 24.dp, bottom = 24.dp),  
            horizontalArrangement = Arrangement.spacedBy(12.dp) // 버튼 사이 간격  
        ) {  
            OutlinedButton(  
                onClick = {  
                    // 첫 번째 카드가 아닐 때만 작동 (음수로 내려가는 것 방지)  
                    if (currentIndex &amp;gt; 0) {  
                        currentIndex-- // 인덱스 -1 &amp;rarr; 이전 단어로 이동  
                        isRevealed = false // 다음 카드는 다시 가려진 상태로 리셋  
                    }  
                },  
                enabled = currentIndex &amp;gt; 0, // 첫 카드면 버튼 비활성화  
                modifier = Modifier.weight(1f).height(56.dp),  
                shape = RoundedCornerShape(16.dp),  
            ) {  
                Text(&quot;이전&quot;, fontWeight = FontWeight.Bold, fontSize = 16.sp, color = Color.Black)  
            }  
            Button(  
                onClick = {  
                    // 마지막 카드면 홈스크린으로 복귀, 아니면 다음 카드로  
                    if (isLastCard) onNavigateBack()  
                    else { currentIndex++; isRevealed = false } // 인덱스 +1 + 뜻 다시 가리기  
                },  
                modifier = Modifier.weight(1f).height(56.dp),  
                shape = RoundedCornerShape(16.dp),  
                // 마지막 카드일 때: 검정 (종료 강조), 아닐 때: 회색 (일반 진행)  
                colors = ButtonDefaults.buttonColors(  
                    containerColor = if (isLastCard) Color(0xFF1A1A1A) else Color(0xFFE0E0E0),  
                    contentColor = if (isLastCard) Color.White else Color(0xFF333333)  
                )  
            ) {  
                // 마지막 카드면 &quot;종료&quot;, 아니면 &quot;다음&quot; 표시  
                Text(text = if (isLastCard) &quot;종료&quot; else &quot;다음&quot;, fontWeight = FontWeight.Bold, fontSize = 16.sp)  
            }  
        }    }}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;색상 문제 해결&lt;/li&gt;
&lt;li&gt;Theme.kt 바꾸기&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;  package com.example.bootcamp_wordlist.ui.theme  
  
import androidx.compose.material3.MaterialTheme  
import androidx.compose.material3.lightColorScheme  
import androidx.compose.runtime.Composable  
import androidx.compose.ui.graphics.Color  
  
// 항상 라이트 모드 고정 (다크모드 무시)  
private val AppColorScheme = lightColorScheme(  
    primary = Color(0xFF1A1A1A),  
    onPrimary = Color.White,  
    secondary = Color(0xFF666666),  
    onSecondary = Color.White,  
    background = Color(0xFFF5F5F5),  
    onBackground = Color(0xFF1A1A1A),  
    surface = Color.White,  
    onSurface = Color(0xFF1A1A1A),  
    surfaceVariant = Color(0xFFE0E0E0),  
    onSurfaceVariant = Color(0xFF666666),  
    outline = Color(0xFFDDDDDD),  
)  
  
@Composable  
fun BootcampwordlistTheme(  
    content: @Composable () -&amp;gt; Unit  
) {  
    MaterialTheme(  
        colorScheme = AppColorScheme,  
        typography = Typography,  
        content = content  
    )  
}
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Tech/앱동-kotlin 부트캠프</category>
      <category>Android</category>
      <category>APP</category>
      <category>APPDONG</category>
      <category>kotlin</category>
      <author>JSJH._.</author>
      <guid isPermaLink="true">https://jsjh3363.tistory.com/47</guid>
      <comments>https://jsjh3363.tistory.com/47#entry47comment</comments>
      <pubDate>Mon, 11 May 2026 21:38:50 +0900</pubDate>
    </item>
    <item>
      <title>앱동 kotlin 세미나 PPT</title>
      <link>https://jsjh3363.tistory.com/46</link>
      <description>&lt;p&gt;&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/VgTAM/dJMcafGx86w/vyqHwSgYgP6B0sA8PdL0FK/%EC%BD%94%ED%8B%80%EB%A6%B0%28Kotlin%29_%EC%84%B8%EB%AF%B8%EB%82%98_.pptx?attach=1&amp;amp;knm=tfile.pptx&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;코틀린(Kotlin)_세미나_.pptx&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.28MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>Tech/앱동-kotlin 부트캠프</category>
      <category>APPDONG</category>
      <category>kotlin</category>
      <author>JSJH._.</author>
      <guid isPermaLink="true">https://jsjh3363.tistory.com/46</guid>
      <comments>https://jsjh3363.tistory.com/46#entry46comment</comments>
      <pubDate>Fri, 8 May 2026 17:17:24 +0900</pubDate>
    </item>
    <item>
      <title>[4주차].새 단어 입력과 팝업창</title>
      <link>https://jsjh3363.tistory.com/45</link>
      <description>&lt;h1 data-heading=&quot;수업 계획&quot;&gt;[4주차] 새 단어 입력과 팝업창&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이번 주는 ➕ 버튼을 달아서 내가 직접 새 단어를 추가하고 영구 저장하는 주차입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-heading=&quot;오늘의 목표&quot; data-ke-size=&quot;size26&quot;&gt;오늘의 목표&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;화면 구석에 더하기(➕) 버튼이 생깁니다.&lt;/li&gt;
&lt;li&gt;버튼을 누르면 단어를 입력할 수 있는 팝업창(바텀시트)이 밑에서 올라옵니다.&lt;/li&gt;
&lt;li&gt;단어와 뜻을 적고 저장을 누르면 진짜로 스크롤 리스트에 단어가 추가됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-heading=&quot;1. 4주차 UI 코드 이식&quot; data-ke-size=&quot;size26&quot;&gt;1. 4주차 UI 코드 이식&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;지난주에 쓰던 ui 폴더 안의 HomeScreen.kt 파일 열기.&lt;/li&gt;
&lt;li&gt;스크롤을 맨 밑으로 내려서 4주차 완성 하단 코드 안의 전체 텍스트 복사.&lt;/li&gt;
&lt;li&gt;기존 HomeScreen.kt 내용을 통째로 복붙하여 덮어쓰기.&lt;/li&gt;
&lt;li&gt;상단 초록색 (Run) 클릭.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-heading=&quot;2. 기본적인 android 문법 (4주차)&quot; data-ke-size=&quot;size26&quot;&gt;2. 기본적인 android 문법 (4주차)&lt;/h2&gt;
&lt;h3 data-heading=&quot;1. &amp;#96;var isSheetOpen by remember { mutableStateOf(false) }&amp;#96;&quot; data-ke-size=&quot;size23&quot;&gt;1. var isSheetOpen by remember { mutableStateOf(false) }&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI 컴포넌트의 가시성 상태를 저장하고 컴포즈 트리가 이를 추적하게 만드는 선언문입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상태값이 변경되면, 지정된 해당 상태를 참조하는 UI 노드 부분만 자동으로 재랜더링(Recomposition).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-heading=&quot;2. &amp;#96;Box { }&amp;#96;&quot; data-ke-size=&quot;size23&quot;&gt;2. Box { }&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 요소들을 Z축(깊이) 기준으로 겹쳐서 배치할 수 있는 레이아웃 뷰 그룹입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FAB를 우측 하단에 상대적으로 띄우거나, 모달(Modal) 뷰를 전체 화면 위로 오버레이 할 때 주로 사용.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-heading=&quot;3. &amp;#96;OutlinedTextField { }&amp;#96;&quot; data-ke-size=&quot;size23&quot;&gt;3. OutlinedTextField { }&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외곽선 테두리가 포함된 텍스트 입력 UI 컴포넌트입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;onValueChange: 사용자가 키보드로 문자를 입력할 때마다 호출되는 콜백 루틴으로, 변수 상태를 실시간 업데이트.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-heading=&quot;4. &amp;#96;FloatingActionButton&amp;#96;&quot; data-ke-size=&quot;size23&quot;&gt;4. FloatingActionButton&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면 콘텐츠 위의 고정된 위치에 배치되어, 주요 기능 전환을 수행하는 버튼 컴포넌트입니다.&lt;/p&gt;
&lt;h3 data-heading=&quot;5. &amp;#96;Spacer(modifier = Modifier)&amp;#96;&quot; data-ke-size=&quot;size23&quot;&gt;5. Spacer(modifier = Modifier)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI 컴포넌트 사이에 빈 공간을 생성하는 레이아웃 요소입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;height나 width 속성을 부여하여 뷰 간의 물리적인 여백과 간격을 제어.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-heading=&quot;6. &amp;#96;if (isSheetOpen) { }&amp;#96; (선언적 렌더링)&quot; data-ke-size=&quot;size23&quot;&gt;6. if (isSheetOpen) { } (선언적 렌더링)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령형 언어처럼 뷰 요소의 VISIBLE / GONE 함수를 별도로 호출하지 않습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상태값(isSheetOpen)이 true일 때만 해당 코드 블록 내부의 UI 컴포넌트가 뷰 트리에 렌더링되게 하는 조건문 처리 방식.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-heading=&quot;3. 에러 대응&quot; data-ke-size=&quot;size26&quot;&gt;3. 에러 대응&lt;/h2&gt;
&lt;h3 data-heading=&quot;발생할 수 있는 문제&quot; data-ke-size=&quot;size23&quot;&gt;발생할 수 있는 문제&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단어 저장이 안 됨&lt;br /&gt;팝업창 닫는 버튼만 눌렀거나, 저장(addWord) 코드가 잘 들어갔는지 체크.&lt;/li&gt;
&lt;li&gt;키보드가 입력창을 가림&lt;br /&gt;imePadding() 코드가 빠졌는지 구조 확인.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 data-heading=&quot;4주차 완성 하단 코드&quot;&gt;4주차 완성 하단 코드&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HomeScreen.kt 전체 파일 (통째로 덮어쓰기 하세요)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;package com.example.bootcamp_wordlist.ui // 지우지 마세요

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.outlined.Visibility
import androidx.compose.material.icons.outlined.VisibilityOff
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.bootcamp_wordlist.data.Word
import com.example.bootcamp_wordlist.data.WordRepository

enum class SortType { LATEST, ALPHABET }

@Composable
fun HomeScreen() {
    val words = WordRepository.words
    var sortType by remember { mutableStateOf(SortType.LATEST) }

    // 모달창 가시성 상태 변수
    var isSheetOpen by remember { mutableStateOf(false) }

    val sortedWords = when (sortType) {
        SortType.LATEST -&amp;gt; words.sortedByDescending { it.id }
        SortType.ALPHABET -&amp;gt; words.sortedBy { it.spelling }
    }

    // 화면 겹쳐올리기용 Box 컨테이너
    Box(modifier = Modifier.fillMaxSize().background(Color(0xFFF5F5F5))) {
        Column(modifier = Modifier.fillMaxSize()) {
            // 상단 헤더 영역
            Column(
                modifier = Modifier
                    .fillMaxWidth()
                    .background(Color(0xFFE0E0E0))
                    .padding(start = 24.dp, end = 24.dp, top = 48.dp, bottom = 16.dp)
            ) {
                Text(text = &quot;단어장&quot;, fontSize = 22.sp, fontWeight = FontWeight.Bold)
                Spacer(modifier = Modifier.height(16.dp))
                Row(
                    modifier = Modifier.fillMaxWidth(),
                    horizontalArrangement = Arrangement.spacedBy(8.dp)
                ) {
                    SortButton(&quot;최신순&quot;, sortType == SortType.LATEST, { sortType = SortType.LATEST }, Modifier.weight(1f))
                    SortButton(&quot;알파벳순&quot;, sortType == SortType.ALPHABET, { sortType = SortType.ALPHABET }, Modifier.weight(1f))
                }
            }
            // 단어 리스트 영역
            LazyColumn(
                modifier = Modifier.fillMaxWidth().weight(1f),
                // 하단 플로팅 버튼 여백(100.dp) 확보
                contentPadding = PaddingValues(start = 16.dp, end = 16.dp, top = 12.dp, bottom = 100.dp),
                verticalArrangement = Arrangement.spacedBy(8.dp)
            ) {
                items(sortedWords, key = { it.id }) { word -&amp;gt;
                    WordItem(word = word)
                }
            }
        }
        // 플로팅 추가 버튼(FAB) 영역
        Box(
            modifier = Modifier
                .align(Alignment.BottomEnd)
                .navigationBarsPadding() // 시스템 하단바 여백 확보
                .padding(24.dp)
        ) {
            FloatingActionButton(
                onClick = { isSheetOpen = true }, // 클릭 시 모달창 표시 상태로 변경
                shape = CircleShape,
                containerColor = Color.White,
                contentColor = Color.DarkGray
            ) {
                Icon(Icons.Default.Add, contentDescription = &quot;단어 추가&quot;)
            }
        }
        // 모달창 조건부 렌더링
        if (isSheetOpen) {
            AddWordSheet(onDismiss = { isSheetOpen = false })
        }
    }
}

// ─── 정렬 버튼 컴포넌트 (3주차와 동일) ───
@Composable
fun SortButton(text: String, isSelected: Boolean, onClick: () -&amp;gt; Unit, modifier: Modifier = Modifier) {
    Button(
        onClick = onClick,
        modifier = modifier.height(40.dp),
        shape = RoundedCornerShape(12.dp),
        // isSelected 값에 따라 색상 동적 변경 (선택됨: 검정/흰 글씨, 미선택: 흰/회색 글씨)
        colors = ButtonDefaults.buttonColors(
            containerColor = if (isSelected) Color(0xFF1A1A1A) else Color.White,
            contentColor = if (isSelected) Color.White else Color(0xFF666666)
        ),
        elevation = ButtonDefaults.buttonElevation(0.dp) // 그림자 없애기
    ) {
        Text(text = text, fontSize = 14.sp)
    }
}

// ─── 단어 카드 컴포넌트 (3주차와 동일) ───
@Composable
fun WordItem(word: Word) {
    val context = LocalContext.current // 창고 접근용 열쇠
    Card(
        modifier = Modifier.fillMaxWidth(),
        shape = RoundedCornerShape(16.dp),
        colors = CardDefaults.cardColors(containerColor = Color.White),
        elevation = CardDefaults.cardElevation(0.dp)
    ) {
        Row(
            modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 14.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Text(text = word.spelling, fontSize = 18.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
            // isHidden true &amp;rarr; 점 5개, false &amp;rarr; 진짜 뜻 출력
            Text(
                text = if (word.isHidden) &quot;●●●●●&quot; else word.meaning,
                fontSize = 14.sp,
                color = if (word.isHidden) Color(0xFFCCCCCC) else Color(0xFF888888),
                modifier = Modifier.weight(1f)
            )
            // 눈 아이콘: 누를 때마다 isHidden true &amp;harr; false 토글
            IconButton(onClick = { WordRepository.toggleHidden(context, word.id) }) {
                Icon(
                    imageVector = if (word.isHidden) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
                    contentDescription = &quot;뜻 숨기기&quot;,
                    tint = Color(0xFFAAAAAA)
                )
            }
            // 휴지통: 누르면 이 단어 영구 삭제
            IconButton(onClick = { WordRepository.deleteWord(context, word.id) }) {
                Icon(Icons.Default.Delete, contentDescription = &quot;삭제&quot;, tint = Color(0xFFAAAAAA))
            }
        }
    }
}

// ─── 단어 추가 BottomSheet ───
// onDismiss: 시트를 닫을 때 호출할 함수 (isSheetOpen = false 로 만들어줌)
@Composable
fun AddWordSheet(onDismiss: () -&amp;gt; Unit) {
    val context = LocalContext.current

    // 입력창에 사용자가 타이핑할 때마다 실시간으로 값이 바뀌는 State 변수
    var spelling by remember { mutableStateOf(&quot;&quot;) }
    var meaning by remember { mutableStateOf(&quot;&quot;) }

    // Box로 전체 화면을 덮는 반투명 딤 배경 생성
    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.Black.copy(alpha = 0.4f)) // 40% 투명도 검정 오버레이
            .clickable { onDismiss() } // 딤 배경 클릭 시 시트 닫기
    ) {
        // 실제 시트 영역: 화면 아래쪽에 딱 붙여서 배치
        Column(
            modifier = Modifier
                .align(Alignment.BottomCenter) // Box 안에서 아래 가운데 정렬
                .fillMaxWidth()
                // topStart, topEnd만 둥글게 &amp;rarr; 위쪽만 깎인 반원형 시트 모양
                .clip(RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp))
                .background(Color.White)
                .navigationBarsPadding() // 하단 네비게이션 바 위에 올라오도록 여백 확보
                .imePadding()            // 키보드가 올라올 때 시트도 같이 위로 밀려남
                .pointerInput(Unit) {
                    detectTapGestures { }
                }
                .padding(24.dp)
        ) {
            // 상단 손잡이 바 (BottomSheet 디자인 관례)
            Box(
                modifier = Modifier
                    .width(48.dp)
                    .height(4.dp)
                    .clip(RoundedCornerShape(2.dp))
                    .background(Color(0xFFDDDDDD))
                    .align(Alignment.CenterHorizontally)
            )
            Spacer(modifier = Modifier.height(20.dp))

            Text(&quot;새 단어 추가&quot;, fontSize = 20.sp, fontWeight = FontWeight.Bold)
            Spacer(modifier = Modifier.height(20.dp))

            // OutlinedTextField = 테두리가 있는 입력창
            // value: 현재 입력된 텍스트 (spelling State와 연결)
            // onValueChange: 글자 칠 때마다 호출 &amp;rarr; spelling 변수를 새 값으로 업데이트
            OutlinedTextField(
                value = spelling,
                onValueChange = { spelling = it }, // it = 방금 입력된 전체 텍스트
                placeholder = { Text(&quot;영단어 입력&quot;) }, // 아무것도 없을 때 흐리게 보이는 힌트
                modifier = Modifier.fillMaxWidth(),
                shape = RoundedCornerShape(12.dp),
                singleLine = true // 엔터 눌러도 줄바꿈 안 됨 (한 줄 입력만 허용)
            )
            Spacer(modifier = Modifier.height(12.dp))

            OutlinedTextField(
                value = meaning,
                onValueChange = { meaning = it },
                placeholder = { Text(&quot;뜻 입력&quot;) },
                modifier = Modifier.fillMaxWidth(),
                shape = RoundedCornerShape(12.dp),
                singleLine = true
            )
            Spacer(modifier = Modifier.height(20.dp))

            Button(
                onClick = {
                    // isNotBlank() = 공백만 있는 경우도 막아줌 (스페이스바 연타 방지)
                    if (spelling.isNotBlank() &amp;amp;&amp;amp; meaning.isNotBlank()) {
                        // .trim() = 앞뒤 공백 제거 후 창고에 저장
                        WordRepository.addWord(context, spelling.trim(), meaning.trim())
                        onDismiss() // 저장 후 시트 닫기
                    }
                },
                modifier = Modifier.fillMaxWidth().height(52.dp),
                shape = RoundedCornerShape(12.dp),
                // enabled = 두 칸 모두 입력됐을 때만 버튼 활성화 (하나라도 비면 회색으로 비활성)
                enabled = spelling.isNotBlank() &amp;amp;&amp;amp; meaning.isNotBlank(),
                colors = ButtonDefaults.buttonColors(
                    containerColor = Color(0xFFE0E0E0),
                    contentColor = Color(0xFF333333)
                )
            ) {
                Text(&quot;저장&quot;, fontWeight = FontWeight.Bold, fontSize = 16.sp)
            }
            Spacer(modifier = Modifier.height(16.dp))
        }
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Tech/앱동-kotlin 부트캠프</category>
      <category>Android</category>
      <category>APPDONG</category>
      <category>kotlin</category>
      <category>앱동</category>
      <author>JSJH._.</author>
      <guid isPermaLink="true">https://jsjh3363.tistory.com/45</guid>
      <comments>https://jsjh3363.tistory.com/45#entry45comment</comments>
      <pubDate>Mon, 4 May 2026 23:41:17 +0900</pubDate>
    </item>
    <item>
      <title>[3주차].리스트 스크롤 만들기</title>
      <link>https://jsjh3363.tistory.com/44</link>
      <description>&lt;h1 data-heading=&quot;[3주차] 리스트 스크롤 만들기&quot;&gt;[3주차] 리스트 스크롤 만들기&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이번 주는 앱 화면에 단어 여러 개를 세로로 정렬하고 버튼을 작동시키는 주차입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-heading=&quot;오늘의 목표&quot; data-ke-size=&quot;size26&quot;&gt;오늘의 목표&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아래로 쭉 스크롤되는 리스트가 나옵니다.&lt;/li&gt;
&lt;li&gt;정렬 버튼으로 리스트 순서를 바꿀 수 있습니다.&lt;/li&gt;
&lt;li&gt;눈 모양 버튼 누르면 단어 뜻이 ●●●●●로 변합니다. 다시 누르면 보입니다.&lt;/li&gt;
&lt;li&gt;휴지통 누르면 지워집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-heading=&quot;1. 3주차 데이터 창고 파일 만들기&quot; data-ke-size=&quot;size26&quot;&gt;1. 3주차 데이터 창고 파일 만들기&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;왼쪽 폴더 트리에서 data 폴더 우클릭 ➜ New -&amp;gt; Kotlin Class/File 클릭.&lt;/li&gt;
&lt;li&gt;이름 칸에 WordRepository 입력 후 엔터 ➜ 3주차 준비 코드 - WordRepository.kt 전체 복붙 (데이터를 저장소에 읽고 쓰는 데이터 창고입니다).&lt;/li&gt;
&lt;li&gt;MainActivity.kt 파일을 열기.&lt;/li&gt;
&lt;li&gt;super.onCreate(savedInstanceState) 바로 아랫줄에 WordRepository.load(this)적기.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-heading=&quot;2. 3주차 UI 코드 이식&quot; data-ke-size=&quot;size26&quot;&gt;2. 3주차 UI 코드 이식&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;2주차에 작업했던 ui 폴더 안의 HomeScreen.kt 파일 열기.&lt;/li&gt;
&lt;li&gt;스크롤을 맨 밑으로 내려서 3주차 완성 하단 코드 안의 전체 텍스트 복사.&lt;/li&gt;
&lt;li&gt;기존 HomeScreen.kt 내용을 모조리 싹 다 지우고 방금 복사한 코드로 덮어쓰기.&lt;/li&gt;
&lt;li&gt;상단 초록색 (Run) 클릭.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-heading=&quot;3. 기본적인 android 문법 (3주차)&quot; data-ke-size=&quot;size26&quot;&gt;3. 기본적인 android 문법 (3주차)&lt;/h2&gt;
&lt;h3 data-heading=&quot;1. &amp;#96;remember { mutableStateOf() }&amp;#96;&quot; data-ke-size=&quot;size23&quot;&gt;1. remember { mutableStateOf() }&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compose에서 &lt;b&gt;버튼 클릭이나 데이터 변경에 따라 화면이 바뀌게 하려면&lt;/b&gt; 상태(State)를 선언해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;var sortType by remember { mutableStateOf(SortType.LATEST) }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;mutableStateOf(초기값): 변경을 감지할 수 있는 상태값 생성.&lt;/li&gt;
&lt;li&gt;remember { }: 화면이 다시 그려져도(Recomposition) 값이 초기화되지 않고 기억됨.&lt;/li&gt;
&lt;li&gt;값이 바뀌면 &amp;rarr; 이 상태를 참조하는 UI만 자동으로 다시 그려짐.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-heading=&quot;2. &amp;#96;mutableStateListOf&amp;#96;&quot; data-ke-size=&quot;size23&quot;&gt;2. mutableStateListOf&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI가 데이터의 변경을 관찰할 수 있도록 지원하는 상태 감지형 리스트 컴포넌트입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리스트 내부의 요소가 추가 및 삭제될 때 Compose 시스템이 이를 감지하여 해당 UI를 자동으로 재구축(Recomposition).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-heading=&quot;3. &amp;#96;LazyColumn { }&amp;#96;과 &amp;#96;items()&amp;#96;&quot; data-ke-size=&quot;size23&quot;&gt;3. LazyColumn { }과 items()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면에 표시되는 항목들만 동적으로 렌더링하여 메모리와 성능을 최적화하는 세로 스크롤 레이아웃입니다.&lt;/p&gt;
&lt;pre class=&quot;sml&quot;&gt;&lt;code&gt;LazyColumn {
    items(words, key = { it.id }) { word -&amp;gt;
        WordItem(word = word)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;items(명단): 전달된 컬렉션 개수만큼 반복하며 람다 블록 내부에 있는 뷰 컴포넌트를 스크롤 위치에 맞게 생성.&lt;/li&gt;
&lt;li&gt;key = { it.id }: 데이터 변경 및 정렬 시 뷰의 재사용 성능을 높이기 위해 각 항목에 부여하는 고유 식별자.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-heading=&quot;4. &amp;#96;LocalContext.current&amp;#96;&quot; data-ke-size=&quot;size23&quot;&gt;4. LocalContext.current&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 시스템 내장 자원에 접근하기 위해 현재 실행 중인 Context 객체를 가져옵니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;val context = LocalContext.current로 가져온 인스턴스를 저장소 접근 권한이 포함된 메서드 파라미터로 전달.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-heading=&quot;5. 상태(State) 와 토글&quot; data-ke-size=&quot;size23&quot;&gt;5. 상태(State) 와 토글&lt;/h3&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;text = if (word.isHidden) &quot;●●●●●&quot; else word.meaning
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컴포즈의 조건적 렌더링. isHidden의 Boolean 값 따라 출력 텍스트 변화.&lt;/li&gt;
&lt;li&gt;클릭 시 toggleHidden 함수를 호출하여 Boolean 값을 반전, 해당 메모리를 관찰하고 있던 상태 UI가 즉시 업데이트.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-heading=&quot;6. &amp;#96;.sortedBy { }&amp;#96; 및 &amp;#96;.sortedByDescending { }&amp;#96;&quot; data-ke-size=&quot;size23&quot;&gt;6. .sortedBy { } 및 .sortedByDescending { }&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리스트 내부의 객체를 특정 기준 프로퍼티에 따라 정렬하는 코틀린 표준 라이브러리 함수입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Descending: 지정한 기준을 큰 값부터 작은 값 순으로 내림차순 정렬. 최신순 정렬.&lt;/li&gt;
&lt;li&gt;오름차순: 지정한 기준을 작은 값부터 큰 값 순으로 정렬. 알파벳 A-Z 구별에 사용.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-heading=&quot;7. &amp;#96;onClick = { }&amp;#96;&quot; data-ke-size=&quot;size23&quot;&gt;7. onClick = { }&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 컴포넌트를 터치(클릭)했을 때 호출되는 이벤트 콜백입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;람다식 내부에 데이터베이스 수정, UI 상태 변경 등의 비즈니스 로직을 정의.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-heading=&quot;4. 에러 대응&quot; data-ke-size=&quot;size26&quot;&gt;4. 에러 대응&lt;/h2&gt;
&lt;h3 data-heading=&quot;발생할 수 있는 문제&quot; data-ke-size=&quot;size23&quot;&gt;발생할 수 있는 문제&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지웠던 단어가 앱을 껐다 켜면 다시 나타남&lt;br /&gt;정상 작동 중입니다. 에러 방지용 초기 더미데이터가 복구되도록 강사가 설계해두었습니다.&lt;/li&gt;
&lt;li&gt;화면이 안 나옴 / 빨간줄 점벅&lt;br /&gt;빨간줄 뜬 곳에 텍스트 커서(마우스) 올리고 Alt + Enter 누른 뒤 Import 클릭.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 data-heading=&quot;3주차 준비 코드&quot;&gt;3주차 준비 코드&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WordRepository.kt 파일&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;package com.example.bootcamp_wordlist.data // data 폴더에 넣었으므로 .data로 끝납니다. (지우지 마세요)

import android.content.Context
import androidx.compose.runtime.mutableStateListOf

// 강사가 미리 설계해둔 데이터 창고 구조입니다.
object WordRepository {
    val words = mutableStateListOf&amp;lt;Word&amp;gt;()

    private const val PREF_NAME = &quot;word_prefs&quot;
    private const val KEY_WORDS = &quot;words&quot;
    private const val SEPARATOR = &quot;||&quot;
    private const val FIELD_SEP = &quot;::&quot;

    fun load(context: Context) {
        val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
        val raw = prefs.getString(KEY_WORDS, &quot;&quot;) ?: &quot;&quot;
        words.clear()
        if (raw.isNotBlank()) {
            raw.split(SEPARATOR).forEach { entry -&amp;gt;
                val parts = entry.split(FIELD_SEP)
                if (parts.size == 4) {
                    words.add(
                        Word(
                            id = parts[0].toLongOrNull() ?: return@forEach,
                            spelling = parts[1],
                            meaning = parts[2],
                            isHidden = parts[3].toBoolean()
                        )
                    )
                }
            }
        }
        if (words.isEmpty()) {
            words.addAll(listOf(
                Word(id = 1L, spelling = &quot;apple&quot;, meaning = &quot;사과&quot;),
                Word(id = 2L, spelling = &quot;banana&quot;, meaning = &quot;바나나&quot;),
                Word(id = 3L, spelling = &quot;cherry&quot;, meaning = &quot;체리&quot;),
            ))
            save(context)
        }
    }

    private fun save(context: Context) {
        val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
        val raw = words.joinToString(SEPARATOR) {
            &quot;${it.id}${FIELD_SEP}${it.spelling}${FIELD_SEP}${it.meaning}${FIELD_SEP}${it.isHidden}&quot;
        }
        prefs.edit().putString(KEY_WORDS, raw).apply()
    }

    fun addWord(context: Context, spelling: String, meaning: String) {
        // id 기본값인 System.currentTimeMillis()가 자동으로 고유 id 역할을 함
        words.add(Word(spelling = spelling, meaning = meaning))
        save(context)
    }

    fun deleteWord(context: Context, id: Long) {
        words.removeIf { it.id == id }
        save(context)
    }

    fun toggleHidden(context: Context, id: Long) {
        val index = words.indexOfFirst { it.id == id }
        if (index != -1) {
            words[index] = words[index].copy(isHidden = !words[index].isHidden)
            save(context)
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1 data-heading=&quot;3주차 완성 하단 코드&quot;&gt;3주차 완성 하단 코드&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HomeScreen.kt 전체 파일 (통째로 덮어쓰기 하세요)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;xl&quot;&gt;&lt;code&gt;package com.example.bootcamp_wordlist.ui // ui 폴더에 넣었으므로 .ui로 끝납니다. (지우지 마세요)

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.outlined.Visibility
import androidx.compose.material.icons.outlined.VisibilityOff
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.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.bootcamp_wordlist.data.Word
import com.example.bootcamp_wordlist.data.WordRepository

// 정렬 상태 제어 열거형
enum class SortType { LATEST, ALPHABET }

@Composable
fun HomeScreen() {
    // 리스트 원본 데이터 연동
    // WordRepository.words = 창고에서 전체 단어 리스트를 가져옴
    // mutableStateListOf로 만들어져 있어서 리스트가 바뀌면 화면도 자동 갱신됨
    val words = WordRepository.words

    // remember { mutableStateOf(...) } = 정렬 상태를 기억하는 변수
    // 이 값이 바뀌면 &amp;rarr; sortedWords가 바뀌고 &amp;rarr; LazyColumn이 자동으로 다시 그려짐
    var sortType by remember { mutableStateOf(SortType.LATEST) }

    // when = 코틀린의 switch문. sortType 값에 따라 다른 정렬 결과를 반환
    // sortedByDescending { it.id } = id 큰 것(최신)부터 내림차순 정렬
    // sortedBy { it.spelling } = spelling 알파벳 오름차순 정렬
    val sortedWords = when (sortType) {
        SortType.LATEST -&amp;gt; words.sortedByDescending { it.id }
        SortType.ALPHABET -&amp;gt; words.sortedBy { it.spelling }
    }

    // Box = 자식 요소들을 Z축으로 겹쳐 쌓는 레이아웃
    // 여기선 Column(리스트) 위에 FAB 버튼을 둥둥 띄우기 위해 사용
    Box(modifier = Modifier.fillMaxSize().background(Color(0xFFF5F5F5))) {
        Column(modifier = Modifier.fillMaxSize()) {

            // 상단 헤더: 제목 + 정렬 버튼 2개를 감싸는 회색 영역
            Column(
                modifier = Modifier
                    .fillMaxWidth()
                    .background(Color(0xFFE0E0E0)) // 헤더 배경색 (연회색)
                    .padding(start = 24.dp, end = 24.dp, top = 48.dp, bottom = 16.dp)
                    // top = 48.dp: 상단 상태바(시계/배터리) 영역과 겹치지 않게 여백 확보
            ) {
                Text(text = &quot;단어장&quot;, fontSize = 22.sp, fontWeight = FontWeight.Bold)
                Spacer(modifier = Modifier.height(16.dp)) // 제목과 버튼 사이 세로 여백

                // Row 안에 버튼 2개를 가로로 나란히 배치
                // spacedBy(8.dp) = 버튼과 버튼 사이에 8dp 간격 자동 삽입
                Row(
                    modifier = Modifier.fillMaxWidth(),
                    horizontalArrangement = Arrangement.spacedBy(8.dp)
                ) {
                    // weight(1f) = 가로 공간을 1:1로 똑같이 나눠 가짐
                    SortButton(
                        text = &quot;최신순&quot;,
                        isSelected = sortType == SortType.LATEST, // 현재 선택된 버튼인지 비교
                        onClick = { sortType = SortType.LATEST }, // 클릭 시 상태값 변경 &amp;rarr; 화면 갱신
                        modifier = Modifier.weight(1f)
                    )
                    SortButton(
                        text = &quot;알파벳순&quot;,
                        isSelected = sortType == SortType.ALPHABET,
                        onClick = { sortType = SortType.ALPHABET },
                        modifier = Modifier.weight(1f)
                    )
                }
            }

            // LazyColumn = 스크롤 가능한 세로 리스트
            // 화면에 보이는 항목만 그려서 수백 개도 버벅임 없이 처리 가능
            // weight(1f) = 헤더 높이를 뺀 나머지 세로 공간을 전부 차지
            LazyColumn(
                modifier = Modifier.fillMaxWidth().weight(1f),
                // contentPadding = 리스트 전체의 안쪽 여백 (개별 카드 여백과 다름)
                contentPadding = PaddingValues(start = 16.dp, end = 16.dp, top = 12.dp, bottom = 24.dp),
                // spacedBy = 카드와 카드 사이의 간격
                verticalArrangement = Arrangement.spacedBy(8.dp)
            ) {
                // items() = words 리스트 개수만큼 반복해서 WordItem을 찍어냄
                // key = { it.id } = 각 카드에 고유 번호표를 붙여줌
                //   &amp;rarr; 정렬/삭제 시 Compose가 어떤 카드가 바뀐 건지 정확히 알아서 부드럽게 처리
                items(sortedWords, key = { it.id }) { word -&amp;gt;
                    WordItem(word = word) // word 데이터 하나씩 카드 UI에 넘겨주기
                }
            }
        }
    }
}

// ─── 정렬 버튼 컴포넌트 ───
// isSelected: 이 버튼이 현재 선택된 상태인지 (true면 검정, false면 흰색)
@Composable
fun SortButton(text: String, isSelected: Boolean, onClick: () -&amp;gt; Unit, modifier: Modifier = Modifier) {
    Button(
        onClick = onClick,
        modifier = modifier.height(40.dp),
        shape = RoundedCornerShape(12.dp), // 모서리 둥글기
        // if-else로 선택 여부에 따라 색상을 동적으로 바꿈
        // isSelected = true  &amp;rarr; 검정 배경 + 흰 글씨 (선택됨)
        // isSelected = false &amp;rarr; 흰 배경 + 회색 글씨 (선택 안 됨)
        colors = ButtonDefaults.buttonColors(
            containerColor = if (isSelected) Color(0xFF1A1A1A) else Color.White,
            contentColor = if (isSelected) Color.White else Color(0xFF666666)
        ),
        elevation = ButtonDefaults.buttonElevation(0.dp) // 그림자 없애기
    ) {
        Text(text = text, fontSize = 14.sp)
    }
}

// ─── 단어 카드 컴포넌트 ───
@Composable
fun WordItem(word: Word) {
    // LocalContext.current = 안드로이드 시스템 자원(SharedPreferences 등)에 접근하는 열쇠
    // WordRepository 함수 호출 시 파라미터로 넘겨줘야 함
    val context = LocalContext.current

    // Card = 흰 배경의 둥근 네모 박스
    Card(
        modifier = Modifier.fillMaxWidth(),
        shape = RoundedCornerShape(16.dp), // 모서리 16dp만큼 둥글게
        colors = CardDefaults.cardColors(containerColor = Color.White),
        elevation = CardDefaults.cardElevation(0.dp) // 그림자 없애기
    ) {
        // Row = 영단어 / 뜻 / 버튼 2개를 가로로 나란히 배치
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(horizontal = 16.dp, vertical = 14.dp), // 카드 안쪽 여백
            verticalAlignment = Alignment.CenterVertically // 세로 기준 가운데 정렬
        ) {
            // 영단어 텍스트
            // weight(1f) = 오른쪽 버튼들이 차지하고 남은 공간을 뜻 텍스트와 1:1로 나눔
            Text(
                text = word.spelling,
                fontSize = 18.sp,
                fontWeight = FontWeight.SemiBold, // 반굵게
                modifier = Modifier.weight(1f)
            )

            // 뜻 텍스트: isHidden이 true면 점 5개, false면 진짜 뜻 표시
            // if-else가 Text의 text 파라미터 안에 바로 들어갈 수 있음
            Text(
                text = if (word.isHidden) &quot;●●●●●&quot; else word.meaning,
                fontSize = 14.sp,
                // 숨김 상태일 때는 연한 회색, 보임 상태일 때는 진한 회색
                color = if (word.isHidden) Color(0xFFCCCCCC) else Color(0xFF888888),
                modifier = Modifier.weight(1f)
            )

            // 눈 아이콘 버튼: 누를 때마다 isHidden 값을 true &amp;harr; false 뒤집기
            // word.id를 넘겨야 어떤 카드의 상태를 바꿀지 창고가 찾을 수 있음
            IconButton(onClick = { WordRepository.toggleHidden(context, word.id) }) {
                Icon(
                    // isHidden 상태에 따라 눈 아이콘 모양도 바뀜 (열린 눈 &amp;harr; 닫힌 눈)
                    imageVector = if (word.isHidden) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
                    contentDescription = if (word.isHidden) &quot;뜻 보이기&quot; else &quot;뜻 숨기기&quot;,
                    tint = Color(0xFFAAAAAA) // 아이콘 색상 회색으로
                )
            }

            // 휴지통 버튼: 누르면 이 word.id를 가진 단어를 창고에서 완전 삭제
            IconButton(onClick = { WordRepository.deleteWord(context, word.id) }) {
                Icon(
                    imageVector = Icons.Default.Delete,
                    contentDescription = &quot;삭제&quot;,
                    tint = Color(0xFFAAAAAA)
                )
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Tech/앱동-kotlin 부트캠프</category>
      <category>APP</category>
      <category>APPDONG</category>
      <category>kotlin</category>
      <author>JSJH._.</author>
      <guid isPermaLink="true">https://jsjh3363.tistory.com/44</guid>
      <comments>https://jsjh3363.tistory.com/44#entry44comment</comments>
      <pubDate>Sun, 26 Apr 2026 18:57:13 +0900</pubDate>
    </item>
    <item>
      <title>[2주차].단어 리스트 UI 그리기</title>
      <link>https://jsjh3363.tistory.com/43</link>
      <description>&lt;h1 data-heading=&quot;[2주차] 단어 리스트 UI 그리기&quot;&gt;[2주차] 단어 리스트 UI 그리기&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이번 주는 앱 화면에 &lt;b&gt;단어장의 껍데기를 만들어 화면에 띄우는&lt;/b&gt; 주차입니다.&lt;/li&gt;
&lt;li&gt;아직 눌러도 아무 반응 없으며, 철저히 &lt;b&gt;보이는 것만 만드는 주차&lt;/b&gt;입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-heading=&quot;오늘의 목표&quot; data-ke-size=&quot;size26&quot;&gt;오늘의 목표&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스마트폰에 단어 카드가 하나 뜹니다.&lt;/li&gt;
&lt;li&gt;오른쪽에 아이콘이 보이지만, 아직 눌러도 기능은 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-heading=&quot;1. 패키지(폴더) 나누기&quot; data-ke-size=&quot;size26&quot;&gt;1. 패키지(폴더) 나누기&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드를 깔끔하게 정리하기 위해 서랍장(폴더)을 두 개 만듭니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;왼쪽 폴더 트리에서 app &amp;gt; kotlin+java &amp;gt; com.example...(자신의 패키지명) 폴더 우클릭.&lt;/li&gt;
&lt;li&gt;New -&amp;gt; Package 클릭.&lt;/li&gt;
&lt;li&gt;깜빡이는 이름 맨 뒤에 data 라고만 이어서 치고 엔터 (데이터 보관용 폴더).&lt;/li&gt;
&lt;li&gt;똑같이 상위 패키지를 한 번 더 우클릭해서 ui 라고 치고 패키지 1개 더 생성 (화면용 폴더).&lt;br /&gt;ui는 이미 자동생성 되어있을수도..&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-heading=&quot;2. 2주차 뼈대 파일 생성&quot; data-ke-size=&quot;size26&quot;&gt;2. 2주차 뼈대 파일 생성&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;방금 만든 data 폴더 우클릭 ➜ New -&amp;gt; Kotlin Class/File 클릭.&lt;br /&gt;데이터 클래스로 만들어도 되긴하지만 일단은 기본클래스로 만들기&lt;/li&gt;
&lt;li&gt;빈칸에 Word 입력 후 엔터 ➜ 2주차 준비 코드 - Word.kt 전체 복붙.&lt;/li&gt;
&lt;li&gt;ui 폴더 우클릭 ➜ 똑같은 방법으로 이름은 HomeScreen 입력 후 엔터 ➜ 2주차 준비 코드 - HomeScreen.kt 전체 복붙.&lt;/li&gt;
&lt;li&gt;마지막으로 화면을 띄워줄 MainActivity.kt 열어서 안의 WordCardScreen() 글자를 HomeScreen() 으로 수정.&lt;br /&gt;시작부분의 함수를 바꾸기.&lt;br /&gt;밑의 함수는 쓸모 없어짐.&lt;br /&gt;그러면 오류와 함께 자동으로 {kotlin}import com.example.bootcamp_wordlist.ui.HomeScreen 삽입 가능하게함&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-heading=&quot;3. 빈 화면에 데이터 심기&quot; data-ke-size=&quot;size26&quot;&gt;3. 빈 화면에 데이터 심기&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;방금 만든 HomeScreen.kt 파일 열기.&lt;/li&gt;
&lt;li&gt;HomeScreen 함수 안쪽의 주석 // TODO: 여기에 더미 데이터 넣기 찾기.&lt;/li&gt;
&lt;li&gt;아래 코드를 복사해서 주석 밑에 붙여넣고 임시 단어 생성하기.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;// 더미 데이터(사과) 만들기 (Long 타입 id는 L 붙이기)
val dummyWord = Word(
    id = 1L,
    spelling = &quot;apple&quot;,
    meaning = &quot;사과&quot;,
    isHidden = false
)

// UI 그리는 함수에 이 단어 넘겨주기
WordItem(word = dummyWord)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예시 출력을 위한 더미데이터입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-heading=&quot;4. 2주차 UI 코드 이식&quot; data-ke-size=&quot;size26&quot;&gt;4. 2주차 UI 코드 이식&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;스크롤을 맨 밑으로 내려서 2주차 완성 코드 안의 WordItem 함수 몸통 전체 복사.&lt;/li&gt;
&lt;li&gt;HomeScreen.kt 맨 밑에 비어있는 fun WordItem(word: Word) 함수 덮어쓰기.&lt;/li&gt;
&lt;li&gt;상단 초록색 (Run) 클릭.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-heading=&quot;5. 기본적인 android 문법 (2주차)&quot; data-ke-size=&quot;size26&quot;&gt;5. 기본적인 android 문법 (2주차)&lt;/h2&gt;
&lt;h3 data-heading=&quot;1. &amp;#96;Card { }&amp;#96;&quot; data-ke-size=&quot;size23&quot;&gt;1. Card { }&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;둥근 박스나 그림자를 가진 카드 모양 UI&lt;/b&gt;.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;// Card = 둥근 네모 박스 (전체 껍데기)
Card(
    modifier = Modifier.fillMaxWidth(),
    shape = RoundedCornerShape(16.dp),
    // elevation = 기본 그림자 없애기
    elevation = CardDefaults.cardElevation(0.dp)
) { ... }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RoundedCornerShape() = 모서리 얼마나 둥글게 깎을지 결정.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-heading=&quot;2. &amp;#96;Row { }&amp;#96;&quot; data-ke-size=&quot;size23&quot;&gt;2. Row { }&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안에 있는 요소들을 &lt;b&gt;왼쪽에서 오른쪽으로 가로 배치&lt;/b&gt;하는 레이아웃.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지난주에 배운 Column은 &lt;b&gt;세로(위아래)&lt;/b&gt; 정렬.&lt;/li&gt;
&lt;li&gt;이번 주에 쓰는 Row는 &lt;b&gt;가로(양옆)&lt;/b&gt; 정렬.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-heading=&quot;3. &amp;#96;weight(1f)&amp;#96;&quot; data-ke-size=&quot;size23&quot;&gt;3. weight(1f)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가로 배치(Row) 안에서 &lt;b&gt;남는 공간을 똑같은 비율로 꽉 채우게 하는는 옵션&lt;/b&gt;.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;Text(
    text = word.spelling,
    modifier = Modifier.weight(1f) // 여백 1배수 할당
)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Text에 weight(1f), 뜻 Text에 weight(1f)를 주었으므로 화면을 &lt;b&gt;1:1 비율로 양분&lt;/b&gt;함.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-heading=&quot;4. &amp;#96;IconButton { }&amp;#96; &amp;amp; &amp;#96;Icon()&amp;#96;&quot; data-ke-size=&quot;size23&quot;&gt;4. IconButton { } &amp;amp; Icon()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면에 쓸모 있는 &lt;b&gt;클릭 버튼과 아이콘 이미지&lt;/b&gt; 띄우기.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;IconButton(onClick = { /* 나중에 기능 붙일 곳 */ }) {
    Icon(
        imageVector = Icons.Default.Delete,
        contentDescription = &quot;삭제&quot;
    )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;IconButton = 클릭 가능한 원형 투명 구역 만들기.&lt;/li&gt;
&lt;li&gt;Icon = 안드로이드 내장 아이콘 그리기.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-heading=&quot;5. &amp;#96;Modifier&amp;#96; (생김새 조작 리모컨)&quot; data-ke-size=&quot;size23&quot;&gt;5. Modifier (생김새 조작 리모컨)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI 블록의 크기(fillMaxWidth()), 여백(padding()), 정렬을 조작하는 설정값입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;안드로이드 Compose의 모든 화면 요소는 괄호 안에 modifier = 를 넣어 비율과 간격을 세팅.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-heading=&quot;6. &amp;#96;dp&amp;#96;와 &amp;#96;sp&amp;#96; (반응형 단위)&quot; data-ke-size=&quot;size23&quot;&gt;6. dp와 sp (반응형 단위)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다양한 스마트폰 크기에도 안 깨지게 화면을 그리는 안드로이드 전용 크기 단위.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;dp (Density-independent Pixels): 여백, 상자 크기 같은 물리적 박스 사이즈.&lt;/li&gt;
&lt;li&gt;sp (Scale-independent Pixels): 글씨 전용 크기.&lt;br /&gt;저번 주차 설명 내용&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-heading=&quot;6. 에러 대응&quot; data-ke-size=&quot;size26&quot;&gt;6. 에러 대응&lt;/h2&gt;
&lt;h3 data-heading=&quot;발생할 수 있는 문제&quot; data-ke-size=&quot;size23&quot;&gt;발생할 수 있는 문제&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아이콘 코드에 빨간줄 뜸&lt;br /&gt;Alt + Enter 눌러서 import 선택&lt;br /&gt;Alt+Enter가 굉장히 유용합니다.&lt;/li&gt;
&lt;li&gt;글씨만 있고 카드가 안 생김&lt;br /&gt;Card 괄호 { } 안에 다른 코드가 제대로 들어갔는지 확인&lt;/li&gt;
&lt;li&gt;아무것도 안 뜸&lt;br /&gt;실습 1에서 WordItem(word = dummyWord) 코드를 안 적었음&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 data-heading=&quot;2주차 준비 코드&quot;&gt;2주차 준비 코드&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Word.kt 파일&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;package com.example.bootcamp_wordlist.data // data 폴더에 넣었으므로 .data로 끝납니다. (맨 윗줄은 지우지 마세요)

// Word = 단어 하나가 가지는 정보 꾸러미
data class Word(
    val id: Long = System.currentTimeMillis(), // 추가 시각(밀리초)을 고유 id로 자동 사용
    val spelling: String,
    val meaning: String,
    val isHidden: Boolean = false
)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HomeScreen.kt 파일&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;package com.example.bootcamp_wordlist.ui // ui 폴더에 넣었으므로 .ui로 끝납니다. (맨 윗줄은 지우지 마세요)

import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.outlined.Visibility
import androidx.compose.material3.*
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 com.example.bootcamp_wordlist.data.Word

@Composable
fun HomeScreen() {
    Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
        // TODO: 여기에 더미 데이터 넣기
    }
}

// 2주차 동안 밑에서 코드를 채워넣을 빈 껍데기 함수
@Composable
fun WordItem(word: Word) {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1 data-heading=&quot;2주차 완성 코드&quot;&gt;2주차 완성 코드&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HomeScreen.kt 파일 하단의 WordItem 함수 (이걸 덮어쓰기 하세요)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;@Composable
fun WordItem(word: Word) {
    // Card = 둥근 네모 박스 (전체 껍데기)
    Card(
        modifier = Modifier.fillMaxWidth(),
        shape = RoundedCornerShape(16.dp),
        colors = CardDefaults.cardColors(containerColor = Color.White),
        // elevation = 기본 그림자 없애기
        elevation = CardDefaults.cardElevation(0.dp)
    ) {
        // Row = 가로 배치 레이아웃
        Row(
            modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 14.dp),
            // verticalAlignment = 세로 중앙 정렬
            verticalAlignment = Alignment.CenterVertically
        ) {
            // Text 1: 단어 
            Text(
                text = word.spelling,
                fontSize = 18.sp,
                fontWeight = FontWeight.SemiBold,
                modifier = Modifier.weight(1f)
            )
            // Text 2: 뜻 
            Text(
                text = word.meaning,
                fontSize = 14.sp,
                color = Color(0xFF888888),
                modifier = Modifier.weight(1f)
            )
            
            // IconButton 1: 투명 원형 버튼 (눈 모양)
            IconButton(onClick = { /* 나중에 기능 추가 */ }) {
                Icon(
                    imageVector = Icons.Outlined.Visibility,
                    contentDescription = &quot;숨기기&quot;,
                    // tint = 기본 검정색 대신 회색 덧칠
                    tint = Color(0xFFAAAAAA)
                )
            }

            // IconButton 2: 투명 원형 버튼 (삭제)
            IconButton(onClick = { /* 나중에 기능 추가 */ }) {
                Icon(
                    imageVector = Icons.Default.Delete,
                    contentDescription = &quot;삭제&quot;,
                    tint = Color(0xFFAAAAAA)
                )
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Tech/앱동-kotlin 부트캠프</category>
      <category>Android</category>
      <category>APPDONG</category>
      <category>kotlin</category>
      <author>JSJH._.</author>
      <guid isPermaLink="true">https://jsjh3363.tistory.com/43</guid>
      <comments>https://jsjh3363.tistory.com/43#entry43comment</comments>
      <pubDate>Mon, 6 Apr 2026 16:39:18 +0900</pubDate>
    </item>
    <item>
      <title>[1주차].안드로이드 스튜디오 시작: 첫 화면 띄우기</title>
      <link>https://jsjh3363.tistory.com/42</link>
      <description>&lt;h1 data-heading=&quot;[1주차] 안드로이드 스튜디오 시작: 첫 화면 띄우기&quot;&gt;[1주차] 안드로이드 스튜디오 시작: 첫 화면 띄우기&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이후 활동동안 쓸 &lt;b&gt;개발 환경을 세팅&lt;/b&gt;하고, 앱을 어떻게 만들건지 대충 알아보는시간입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-heading=&quot;오늘의 목표&quot; data-ke-size=&quot;size26&quot;&gt;오늘의 목표&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스마트폰에 본인이 적은 텍스트가 적힌 &lt;b&gt;실제 안드로이드 앱 화면&lt;/b&gt;이 뜹니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-heading=&quot;1. 사전 과제 점검 (환경 세팅)&quot; data-ke-size=&quot;size26&quot;&gt;1. 사전 과제 점검 (환경 세팅)&lt;/h2&gt;
&lt;h2 data-heading=&quot;2. 템플릿 이식&quot; data-ke-size=&quot;size26&quot;&gt;2. 템플릿 이식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드는 초기 세팅할 게 너무너무 많습니다. (Gradle, Manifest 등등...)&lt;br /&gt;그래서 &lt;b&gt;저희는 파일을 다운받지 않고, 일단 빈 프로젝트에 코드를 복붙&lt;/b&gt;할 겁니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;안드로이드 스튜디오에서 New Project -&amp;gt; Empty Activity 로 생성.&lt;br /&gt;(Name 칸에 소문자로 정확히 &lt;b&gt;bootcamp_wordlist&lt;/b&gt; 입력 ➜ 그래야 앞으로 6주 동안 복붙할 때 패키지 에러가 1도 안 납니다!)&lt;/li&gt;
&lt;li&gt;MainActivity.kt 파일 열기.&lt;/li&gt;
&lt;li&gt;[1주차 코드] 텍스트 복사.&lt;/li&gt;
&lt;li&gt;Ctrl+A (전체선택) -&amp;gt; Ctrl+V (붙여넣기).&lt;/li&gt;
&lt;li&gt;상단 중앙에 스마트폰이 잡혔는지 확인 후 &amp;rarr; 초록색 (Run) 클릭.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-heading=&quot;3. Kotlin 중요한 기본 문법&quot; data-ke-size=&quot;size26&quot;&gt;3. Kotlin 중요한 기본 문법&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;var 와 val 차이:&lt;/b&gt; var는 변하는 값, val은 죽어도 안 변하는 값.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;함수 (fun):&lt;/b&gt; 우리 앱의 모든 화면은 fun 이름() { } 으로 생성.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;람다{ }:&lt;/b&gt; 함수 간략 표기법. 괄호 위치에 유의. 람다가 생김새가 많이 생소합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Java &amp;amp; Kotlin 공부&lt;/b&gt;: AI한테 물어도 되지만, 예전에 언어 공부한 자료를 블로그에 올려놔서 한번 봐주세요.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Java : &lt;a href=&quot;https://jsjh3363.tistory.com/category/Language/JAVA&quot;&gt;https://jsjh3363.tistory.com/category/Language/JAVA&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Kotlin : &lt;a href=&quot;https://jsjh3363.tistory.com/category/Language/Kotlin&quot;&gt;https://jsjh3363.tistory.com/category/Language/Kotlin&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-heading=&quot;4. 기본적인 android 문법&quot; data-ke-size=&quot;size26&quot;&gt;4. 기본적인 android 문법&lt;/h2&gt;
&lt;h3 data-heading=&quot;1. &amp;#96;setContent { }&amp;#96;&quot; data-ke-size=&quot;size23&quot;&gt;1. setContent { }&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드 앱에서 &lt;b&gt;Compose 화면을 실제로 띄우는 시작 지점&lt;/b&gt;.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;setContent { //화면의 시작
    MaterialTheme {  
        WordCardScreen()  
    }  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-heading=&quot;2. &amp;#96;@Composable&amp;#96;&quot; data-ke-size=&quot;size23&quot;&gt;2. @Composable&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;화면을 그리는 함수&lt;/b&gt;라는 표시.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Composable  
fun WordCardScreen() {  
    ...  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 계산하는 함수가 아니라 &lt;b&gt;실제로 화면에 보이는 UI를 만드는 함수&lt;/b&gt;.&lt;/p&gt;
&lt;h3 data-heading=&quot;3. &amp;#96;Text()&amp;#96;&quot; data-ke-size=&quot;size23&quot;&gt;3. Text()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화면에 &lt;b&gt;글자를 띄우는 가장 기본적인 함수&lt;/b&gt;.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;Text(text = &quot;Hello Appdong!&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 글씨는 거의 다 이걸로 화면에 띄우게 됩니다.&lt;/p&gt;
&lt;h3 data-heading=&quot;4. &amp;#96;Column()&amp;#96;&quot; data-ke-size=&quot;size23&quot;&gt;4. Column()&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안에 있는 요소들을 &lt;b&gt;위에서 아래로 세로 배치&lt;/b&gt;하는 레이아웃.&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;Column {  
    Text(&quot;첫 번째&quot;)  
    Text(&quot;두 번째&quot;)  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Column = 세로&lt;/li&gt;
&lt;li&gt;Row = 가로&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-heading=&quot;5. &amp;#96;Modifier&amp;#96;&quot; data-ke-size=&quot;size23&quot;&gt;5. Modifier&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크기, 여백, 배경색, 정렬 같은 &lt;b&gt;화면 옵션을 붙이는 도구&lt;/b&gt;.&lt;/p&gt;
&lt;pre class=&quot;x86asm&quot;&gt;&lt;code&gt;modifier = Modifier  
    .fillMaxSize()  
    .background(Color(0xFFF5F5F5))  
    .padding(24.dp)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능이 너무 방대해서 지금은 그냥 꾸미기 옵션 붙이는 체인 정도로 이해.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;fillMaxSize() = 화면 꽉 채우기&lt;/li&gt;
&lt;li&gt;background() = 배경색 칠하기&lt;/li&gt;
&lt;li&gt;padding() = 여백 주기&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-heading=&quot;6. &amp;#96;dp&amp;#96;, &amp;#96;sp&amp;#96;&quot; data-ke-size=&quot;size23&quot;&gt;6. dp, sp&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compose에서는 크기 단위를 자주 씁니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;dp : 여백, 크기, 높이 같은 UI 크기 단위&lt;/li&gt;
&lt;li&gt;sp : 글자 크기 단위&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-heading=&quot;요약&quot; data-ke-size=&quot;size23&quot;&gt;요약&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;setContent = 화면 시작&lt;/li&gt;
&lt;li&gt;@Composable = 화면 함수&lt;/li&gt;
&lt;li&gt;Text = 글자 띄우기&lt;/li&gt;
&lt;li&gt;Column = 세로 배치&lt;/li&gt;
&lt;li&gt;Modifier = 꾸미기 옵션&lt;/li&gt;
&lt;li&gt;dp, sp = 크기 단위&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-heading=&quot;5. 앱 개발 방법&quot; data-ke-size=&quot;size26&quot;&gt;5. 앱 개발 방법&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정석은 아니고 내가 하는 방법.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;먼저 머리속으로 앱 기본 구상&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Figma&lt;/b&gt;로 화면 디자인.&lt;br /&gt;&lt;span&gt;Pasted image 20260319000358.png&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;디자인 스크린샷 확인, Dev mode로 각 요소의 코드 파악&lt;/li&gt;
&lt;li&gt;연결된 피그마 mcp로 AI와 연결&lt;/li&gt;
&lt;li&gt;AI로 기본 코드 작성&lt;/li&gt;
&lt;li&gt;세부내용 비교, (버그 수정, 테스트, 기능추가, 공부) 반복 &amp;gt; 완성&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-heading=&quot;6. 다음 주차 준비&quot; data-ke-size=&quot;size26&quot;&gt;6. 다음 주차 준비&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;본격적인 앱 개발(2~6주차)을 위해 환경설정 파일(Gradle)에 안드로이드 도구(네비게이션, 추가 아이콘 등)를 미리 설치해 둡니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;왼쪽 폴더 트리 제일 밑의 Gradle Scripts 열기.&lt;/li&gt;
&lt;li&gt;build.gradle.kts (Module :app) 파일 더블 클릭.&lt;/li&gt;
&lt;li&gt;스크롤을 맨 밑으로 내려서 dependencies { ... } 찾기.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기존에 있던 dependencies { ... } 전체를 지우고&lt;/b&gt;, 아래 코드로 통째로 &lt;b&gt;덮어쓰기&lt;/b&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;// 라이브러리(설명서) 도구 상자 모음집
dependencies {
    // 1. 안드로이드 기본 뼈대 코드 (필수)
    implementation(&quot;androidx.core:core-ktx:1.12.0&quot;)
    implementation(&quot;androidx.lifecycle:lifecycle-runtime-ktx:2.7.0&quot;)
    implementation(&quot;androidx.activity:activity-compose:1.8.2&quot;)

    // 2. 화면을 그리기 위한 Compose 도구들 (필수)
    implementation(platform(&quot;androidx.compose:compose-bom:2024.02.00&quot;))
    implementation(&quot;androidx.compose.ui:ui&quot;)
    implementation(&quot;androidx.compose.ui:ui-graphics&quot;)
    implementation(&quot;androidx.compose.ui:ui-tooling-preview&quot;)
    implementation(&quot;androidx.compose.material3:material3&quot;)

    // 3. 앱동 단어장 앱만을 위한 '특별' 추가 도구들 (이번에 새로 넣는 것!)
    implementation(&quot;androidx.navigation:navigation-compose:2.7.7&quot;)     // 화면 휙 넘어가는 기능
    implementation(&quot;androidx.compose.material:material-icons-extended:1.6.3&quot;) // 휴지통, 눈알 모양 추가 아이콘
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;(중요)&lt;/b&gt; 스튜디오 우측 상단에 파란색 Sync Now 글씨가 뜨면 무조건 클릭&lt;/li&gt;
&lt;li&gt;아래쪽에서 다운로드 끝나면 세팅 완료.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-heading=&quot;7. 과제&quot; data-ke-size=&quot;size26&quot;&gt;7. 과제&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;java/kotlin언어 익숙해져서 오기&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 data-heading=&quot;1주차 코드&quot;&gt;1주차 코드&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MainActivity.kt&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;xl&quot;&gt;&lt;code&gt;package com.example.bootcamp_wordlist
// 안드로이드 앱의 기본 액티비티 관련 import
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
// Compose UI를 만들 때 필요한 import
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
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

// MainActivity = 안드로이드 앱이 처음 실행될 때 시작되는 메인 진입점
class MainActivity : ComponentActivity() {
    // onCreate = 앱이 처음 켜질 때 자동으로 실행되는 함수
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 화면을 상태바/네비게이션바 끝까지 확장해서 사용하도록 설정
        enableEdgeToEdge()
        // setContent { } = 내부 Compose 코드를 실제로 띄움
        setContent {
            // MaterialTheme = 안드로이드 기본 테마 적용
            MaterialTheme {
                // 우리가 직접 만든 화면 함수 호출
                WordCardScreen()
            }
        }
    }
}

@Composable
fun WordCardScreen() {
    // Column = 안에 있는 요소를 위에서 아래로 세로 배치 하는 레이아웃
    Column(
        modifier = Modifier
            // fillMaxSize() = 화면 전체 크기만큼 꽉 채우기
            .fillMaxSize()
            // background() = 배경색 칠하기
            .background(Color(0xFFF5F5F5))
            // padding(24.dp) = 바깥쪽 여백
            .padding(24.dp),
        // horizontalAlignment = 가로 기준 가운데 정렬
        horizontalAlignment = Alignment.CenterHorizontally,
        // verticalArrangement = 세로 기준 가운데 정렬
        verticalArrangement = Arrangement.Center
    ) {
        // Card = 둥근 네모 박스 하나 만든다고 생각
        Card(
            // shape = 카드 모서리를 얼마나 둥글게 할지 설정
            shape = RoundedCornerShape(24.dp)
        ) {
            // 카드 안쪽에도 다시 Column을 써서 글자 2개를 세로로 배치
            Column(
                modifier = Modifier.padding(horizontal = 40.dp, vertical = 32.dp),
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                // Text() = 화면에 글자를 띄우는 가장 기본적인 함수
                Text(
                    // text = 실제로 화면에 보일 글자
                    text = &quot;Hello Appdong!&quot;,
                    // fontSize = 글자 크기
                    fontSize = 28.sp,
                    // fontWeight = 글자 굵기
                    fontWeight = FontWeight.Bold
                )
                Text(
                    text = &quot;성공&quot;,
                    // 첫 번째 글자보다 조금 작게
                    fontSize = 16.sp,
                    // 회색 글씨로 표시
                    color = Color.Gray,
                    // 위쪽으로 여백 12 주기
                    modifier = Modifier.padding(top = 12.dp)
                )
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Tech/앱동-kotlin 부트캠프</category>
      <author>JSJH._.</author>
      <guid isPermaLink="true">https://jsjh3363.tistory.com/42</guid>
      <comments>https://jsjh3363.tistory.com/42#entry42comment</comments>
      <pubDate>Tue, 31 Mar 2026 09:30:21 +0900</pubDate>
    </item>
    <item>
      <title>[0주차].안드로이드 스튜디오 설치 &amp;amp; 개발환경 세팅 가이드</title>
      <link>https://jsjh3363.tistory.com/41</link>
      <description>&lt;h1&gt;[사전 과제] 안드로이드 스튜디오 설치 &amp;amp; 개발환경 세팅 가이드&lt;/h1&gt;
&lt;p&gt;앱동 부트캠프를 원활하게 진행하기 위해, &lt;strong&gt;수업까지 반드시 해와야 하는 세팅&lt;/strong&gt;입니다.&lt;br&gt;설명이 부정확할수도 있어서 혹시나 막히는 부분은 유튜브나 AI를 참고해 주세요.&lt;/p&gt;
&lt;h2&gt;1단계: 안드로이드 스튜디오 (Android Studio) 설치&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://developer.android.com/studio&quot;&gt;다운로드 링크&lt;/a&gt;&lt;/strong&gt; 에 접속.&lt;/li&gt;
&lt;li&gt;다운로드 버튼을 눌러 설치 파일 다운.&lt;/li&gt;
&lt;li&gt;다운로드받은 파일을 실행, &lt;strong&gt;계속 눌러 설치 완료&lt;/strong&gt;. &lt;ul&gt;
&lt;li&gt;⚠ &lt;strong&gt;컴퓨터 사용자 이름이나 설치 경로에 한글이 들어가면 에러나기가 쉽습니다&lt;/strong&gt; &lt;/li&gt;
&lt;li&gt;가급적 C드라이브 기본 경로에 그대로 설치하세요. &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;안드로이드 스튜디오 처음 실행 후 기본 다운로드 시작. &lt;h2&gt;2단계: 스마트폰 개발자 모드&lt;/h2&gt;
우리는 에뮬레이터 대신 &lt;strong&gt;본인 스마트폰&lt;/strong&gt;에 앱을 테스트할 겁니다.&lt;br&gt;에뮬레이터가 간단하긴 하지만, 노트북 특성상 렉을 너무 많이 유발합니다.&lt;/li&gt;
&lt;li&gt;폰 &lt;code&gt;설정&lt;/code&gt; 열기&lt;/li&gt;
&lt;li&gt;맨 밑으로 내려서 &lt;code&gt;휴대전화 정보&lt;/code&gt; 터치&lt;/li&gt;
&lt;li&gt;&lt;code&gt;소프트웨어 정보&lt;/code&gt; 터치&lt;/li&gt;
&lt;li&gt;중간 쯤에 있는 &lt;strong&gt;&lt;code&gt;빌드번호&lt;/code&gt; 7번 연속으로 터치&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;켜졌다는 안내가 뜨면 성공&lt;/li&gt;
&lt;li&gt;다시 &lt;code&gt;설정&lt;/code&gt; 화면으로 돌아가서 스크롤을 맨 밑에 &lt;code&gt;개발자 옵션&lt;/code&gt; 메뉴가 새로 생김.&lt;/li&gt;
&lt;li&gt;진입후 중간쯤에 &lt;code&gt;무선 디버깅&lt;/code&gt; 항목 켜기&lt;h2&gt;3단계: 안드로이드 스튜디오랑 폰 연결&lt;/h2&gt;
&lt;/li&gt;
&lt;li&gt;안드로이드 스튜디오 우상단 &lt;code&gt;Device Manager&lt;/code&gt; 진입&lt;br&gt; ![[Pasted image 20260318231845.png]]&lt;/li&gt;
&lt;li&gt;상단의 와이파이 기호 클릭 후, &lt;code&gt;Pair Devices Using Wi-Fi&lt;/code&gt; 클릭&lt;br&gt; (노트북-휴대폰이 같은 와이파이에 연결되어있어야 합니다.)&lt;br&gt; ![[Pasted image 20260318232059.png]]&lt;/li&gt;
&lt;li&gt;그러면 QR코드가 화면에 나옵니다. (페어링 코드로 할수도있는데 QR이 편한거 같아요)&lt;/li&gt;
&lt;li&gt;아까 켜둔 휴대폰 &lt;code&gt;무선 디버깅&lt;/code&gt; 클릭 후, QR코드로 기기 페어링 클릭 후 QR 스캔&lt;h3&gt;웹 애뮬레이터&lt;/h3&gt;
노트북도 잘안되고 무선 연결도 잘안되는 상황에서는 웹 클라우드의 애뮬레이터를 이용할수도있습니다.&lt;br&gt;![&lt;a href=&quot;https://appetize.io/apps%5D&quot;&gt;https://appetize.io/apps]&lt;/a&gt;&lt;br&gt;위 사이트에 접속하고 로그인 한후 apks파일을 빌드한 후 .apks 파일을 업로드하면 이용가능합니다.&lt;h2&gt;4단계: 프로젝트 생성 &amp;amp; 파일 구경 (해주시면 감사합니다)&lt;/h2&gt;
&lt;/li&gt;
&lt;li&gt;안드로이드 스튜디오 첫 화면에서 &lt;strong&gt;&lt;code&gt;New Project&lt;/code&gt;&lt;/strong&gt; 클릭&lt;/li&gt;
&lt;li&gt;템플릿 목록이 뜨는데, &lt;code&gt;Empty Activity&lt;/code&gt; 선택하고 &lt;code&gt;Next&lt;/code&gt; 클릭 (기초 템플릿 같은겁니다)&lt;/li&gt;
&lt;li&gt;프로젝트 이름(Name) 칸에 &lt;code&gt;appdong-wordlist&lt;/code&gt; 처럼 이름을 적어봅니다.&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;[주의]&lt;/strong&gt; 아래쪽에 Language가 &lt;code&gt;Kotlin&lt;/code&gt;, Build configuration language가 &lt;code&gt;Kotlin DSL (build.gradle.kts)&lt;/code&gt; 로 되어 있는지 확인해주세요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Run 버튼 눌러보기 상단 ▶ 버튼 눌러서 앱 실행까지 확인&lt;h3&gt;파일 트리 보는 법&lt;/h3&gt;
화면 왼쪽 상단에 있는 폴더 계층 구조(Project 뷰)를 보세요.&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;app &amp;gt; kotlin+java &amp;gt; (패키지명) &amp;gt; MainActivity.kt&lt;/code&gt;&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;여기가 &lt;strong&gt;코틀린(Kotlin) 코드를 짤 메인 파일&lt;/strong&gt;입니다(이파일 외에도 여러 파일과 폴더를 만들고 관리 할거에요). 이 안에 화면도 그리고 기능도 넣습니다. &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Gradle Scripts &amp;gt; build.gradle.kts (Module :app)&lt;/code&gt;&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;이건 코딩하는 곳이 아니라, &lt;strong&gt;라이브러리를 가져오는 곳&lt;/strong&gt;입니다. 인터넷에서 남이 만든 기능을 쓸 때 이 파일 맨 아래 &lt;code&gt;dependencies&lt;/code&gt; 블록에 한 줄 적어주고 우상단에 뜨는 &lt;code&gt;Sync Now&lt;/code&gt;를 누르면 됩니다. &lt;h2&gt;에러 대응&lt;/h2&gt;
&lt;h3&gt;발생할수도 있는 문제&lt;/h3&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;기기가 안 잡힘&lt;br&gt;  와이파이 껐다 켜기 / Android Studio 재실행&lt;/li&gt;
&lt;li&gt;QR코드 스캔했는데 연결 안 됨&lt;br&gt;  휴대폰에서 무선 디버깅 껐다가 다시 켜기&lt;/li&gt;
&lt;li&gt;Run 눌렀는데 아무 반응 없음&lt;br&gt;  상단에서 기기 선택이 제대로 되었는지 확인&lt;/li&gt;
&lt;li&gt;Gradle Sync 계속 돌아감&lt;br&gt;  기다리기 원래 오래걸림&lt;/li&gt;
&lt;li&gt;실행 시 에러 발생&lt;br&gt;  Android Studio 재시작 후 다시 시도&lt;h2&gt;언어에 대해&lt;/h2&gt;
&lt;/li&gt;
&lt;li&gt;코틀린은 자바기반 언어입니다. 물론 사용하는 여러 함수는 안드로이드꺼지만 기본 뼈대는 Java이니, Java 기본 문법을 조금 알고 오시면 좋습니다. Kotlin도 고유 문법이 있으니 자바를 아시는 분은 Kotlin도 조금 익혀 오시면 편합니다. &lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Tech/앱동-kotlin 부트캠프</category>
      <category>Android</category>
      <category>APPDONG</category>
      <category>bootcamp</category>
      <category>kotlin</category>
      <author>JSJH._.</author>
      <guid isPermaLink="true">https://jsjh3363.tistory.com/41</guid>
      <comments>https://jsjh3363.tistory.com/41#entry41comment</comments>
      <pubDate>Tue, 31 Mar 2026 09:23:57 +0900</pubDate>
    </item>
    <item>
      <title>[Android].Ch2-5. Material Design 3</title>
      <link>https://jsjh3363.tistory.com/40</link>
      <description>&lt;h1&gt;[Android] Ch2-5. Material Design 3&lt;/h1&gt;
&lt;h2&gt;1. 개요 및 기본 형식&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun MyApp() {
    MaterialTheme {  // Material Design 3 테마 스코프 최상위 적용
        // 해당 스코프 내부에 선언된 모든 UI 컴포넌트들은 테마 설정과 규약을 상속받습니다.
        Surface {
            // 커스텀 UI 컴포넌트 구현부
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Google의 모바일 시스템 코어 디자인 가이드라인인 Material Design 버전 3를 일컫습니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MaterialTheme&lt;/code&gt; 스코프 내부에서는 정의된 색상표 상속, 공용 타이포그래피(폰트 계층), 일반 요소들의 도형 외곽 가이드라인이 자동 렌더링에 반영되어 체계화됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;2. 주요 구성 및 테마 요소&lt;/h2&gt;
&lt;h3&gt;1) MaterialTheme 객체 구조&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;앱 전체의 통합적인 색채, 활자, 모양새 규격을 정의하는 매개 시스템입니다.&lt;/li&gt;
&lt;li&gt;내부 파라미터로 &lt;code&gt;colorScheme&lt;/code&gt;, &lt;code&gt;typography&lt;/code&gt;, &lt;code&gt;shapes&lt;/code&gt; 등을 통합 관리합니다.&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;MaterialTheme(
colorScheme = lightColorScheme(),  // 라이트 모드 기준 색상 지표
typography = Typography,  // 통일된 텍스트 계층 구조 규약
shapes = Shapes  // 컴포넌트 라운딩 등 형태의 규약
) { }&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2) &lt;code&gt;.colorScheme&lt;/code&gt; (색상 체계)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;primary&lt;/code&gt;&lt;/strong&gt;: 핵심 지점(최상단 버튼, 강조 배너 등)에 칠해질 주요 시각 색상.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;onPrimary&lt;/code&gt;&lt;/strong&gt;: &lt;code&gt;primary&lt;/code&gt; 색상 영역 위쪽에 렌더링될 내부 텍스트나 아이콘들의 명도 대비를 이루는 색상.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;secondary&lt;/code&gt;&lt;/strong&gt;: primary와 분리되어 서브 상호작용 지점을 정의하는 보조 색상.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;background&lt;/code&gt;&lt;/strong&gt;: 앱 최하단 빈 여백의 디폴트 배경 컬러.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;surface&lt;/code&gt;&lt;/strong&gt;: 카드뷰 하얀 배경, 스와이프 다이얼로그 배경 색상.&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;MaterialTheme.colorScheme.primary  // 특정 뷰의 백그라운드 컬러로 직접 연동 구성
MaterialTheme.colorScheme.background  &lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3) &lt;code&gt;.typography&lt;/code&gt; (타이포그래피 규약 적용)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;displayLarge/Medium/Small&lt;/code&gt;&lt;/strong&gt;: 히어로 배너 크기 수준의 제일 거대한 제목 텍스트 속성.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;headlineLarge/Medium/Small&lt;/code&gt;&lt;/strong&gt;: 주요 섹션 요약 헤드라인.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;titleLarge/Medium/Small&lt;/code&gt;&lt;/strong&gt;: 보드 타이틀 및 구역 제목.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;bodyLarge/Medium/Small&lt;/code&gt;&lt;/strong&gt;: 통상 정보나 문서 낭독을 위한 기본 본문 크기 속성.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;labelLarge/Medium/Small&lt;/code&gt;&lt;/strong&gt;: 컴포넌트 하단 자막이나 아이콘 주석용 미니 라벨.&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;Text(
text = &amp;quot;상위 구역의 제목 표출 구역&amp;quot;,
style = MaterialTheme.typography.headlineLarge  // 테마를 통한 통일성 확보
)&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;3. 구현 형태별 사용 예시&lt;/h2&gt;
&lt;h3&gt;1) MaterialTheme의 전역 적용 (초기화 단계)&lt;/h3&gt;
&lt;p&gt;최상위 액티비티 구역에서 뷰 전체를 덮는 테마를 선언합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun App() {
    MaterialTheme {
        Surface(
            modifier = Modifier.fillMaxSize(),
            color = MaterialTheme.colorScheme.background  // 최상단 배경 속성에 테마 컬러 부여
        ) {
            // 기타 중첩 레이아웃 및 뷰 진입 지점
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2) 요소별 색채 매핑 활용&lt;/h3&gt;
&lt;p&gt;요소 디자인 시 색상 상수로 하드코딩하지 않고 시스템 테마 변수를 호출하는 모범 사례 구성입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun ColorExample() {
    Column {
        Text(
            text = &amp;quot;주요 강조 텍스트 내용&amp;quot;,
            color = MaterialTheme.colorScheme.primary
        )
        Button(
            colors = ButtonDefaults.buttonColors(
                containerColor = MaterialTheme.colorScheme.secondary
            ),
            onClick = { /* 로직 */ }
        ) {
            Text(&amp;quot;서브 동작 버튼&amp;quot;)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3) 텍스트 규약 통일의 예&lt;/h3&gt;
&lt;p&gt;각종 마진, 픽셀 등의 텍스트 사이즈를 수작업으로 하지 않고 스타일 상수를 상속시켜 앱 전반의 통일감을 부여합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun TypographyExample() {
    Column {
        Text(text = &amp;quot;메인 큰 제목&amp;quot;, style = MaterialTheme.typography.headlineLarge)
        Text(text = &amp;quot;보조 중간 제목&amp;quot;, style = MaterialTheme.typography.headlineMedium)
        Text(text = &amp;quot;주요 본문 텍스트 내역&amp;quot;, style = MaterialTheme.typography.bodyMedium)
        Text(text = &amp;quot;아이콘 라벨 텍스트&amp;quot;, style = MaterialTheme.typography.labelSmall)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4) 테마 색채 사용자 정의(Custom) 세팅&lt;/h3&gt;
&lt;p&gt;브랜드 디자이너가 요청한 공식 색상을 컴포넌트에 주입하는 로직입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;val MyColorScheme = lightColorScheme(
    primary = Color(0xFF6200EE),
    secondary = Color(0xFF03DAC6),
    background = Color(0xFFFFFBFE)
)

@Composable
fun CustomThemeApp() {
    MaterialTheme(colorScheme = MyColorScheme) {  // 직접 오버라이딩된 색상 트리구조 적용
        // 본 구간 내부는 모두 0xFF... 기반 컬러 구조로 상쇄 교체됨
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;4. 부가 설명&lt;/h2&gt;
&lt;h3&gt;1) 다크 모드(Dark Mode) 적용&lt;/h3&gt;
&lt;p&gt;다크 모드의 색상 코드를 별도 파생하여 시스템 OS 설정을 감지, 이를 스위칭할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;val DarkColorScheme = darkColorScheme(
    primary = Color(0xFFBB86FC),
    // 그 외 구성요소 어둡게 변형 
)&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2) 브랜드 일관성 확립&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;MaterialTheme&lt;/code&gt; 참조 인프라를 활용해야 향후 클라이언트 리브랜딩 지시 시, 각 소스 수백 군데를 직접 수정할 필요 없이 테마 컴포넌트 1개 구역의 컬러 상수만 전환하여 글로벌 일관성을 구축할 수 있습니다.&lt;/p&gt;
&lt;h3&gt;3) 시스템 접근성 보장&lt;/h3&gt;
&lt;p&gt;Material Design 가이드라인 내부 시스템을 기본으로 적용하게 되면 일반적인 요소들의 색상 명암 대비율 제어, 터치 반응 규격 등이 구글 접근성 표준(UI/UX 권장 사항)에 자동 부합하게 변경 최적화됩니다.&lt;/p&gt;</description>
      <category>Tech/App-Android</category>
      <category>Android</category>
      <category>APP</category>
      <category>kotlin</category>
      <author>JSJH._.</author>
      <guid isPermaLink="true">https://jsjh3363.tistory.com/40</guid>
      <comments>https://jsjh3363.tistory.com/40#entry40comment</comments>
      <pubDate>Tue, 24 Mar 2026 09:17:23 +0900</pubDate>
    </item>
    <item>
      <title>[Android.]Ch2-4. remember vs rememberSaveable</title>
      <link>https://jsjh3363.tistory.com/39</link>
      <description>&lt;h1&gt;[Android] Ch2-4. remember vs rememberSaveable&lt;/h1&gt;
&lt;h2&gt;1. 개요 및 기본 형식&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun RememberExample() {
    // remember: 통상적인 Recomposition 시에만 값 유지
    var count1 by remember { mutableStateOf(0) }
    // rememberSaveable: 화면 회전, 시스템에 의한 제약 프로세스 재시작 시에도 유지
    var count2 by rememberSaveable { mutableStateOf(0) }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;두 함수 모두 Recomposition 과정에서 UI State 손실을 방지합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rememberSaveable&lt;/code&gt;의 경우 Bundle 복원 매커니즘을 지원하여, Activity 재생성/프로세스 일시정지 단계에서도 라이프사이클을 뛰어넘는 영속화 기능을 제공합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;2. 주요 구성 및 차이점&lt;/h2&gt;
&lt;h3&gt;1) remember&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Recomposition 시 유지&lt;/strong&gt;: 관련된 Composable 함수가 자동 호출 및 갱신되어도 내부에 캐시된 이전 값을 방어하며 유지시킵니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;화면 회전 시 초기화&lt;/strong&gt;: 모바일 디바이스의 회전 등으로 인해 최상단 Activity가 Destruction 이후 Regeneration 단계에 돌입하면 내부 값이 모두 무효화되고 초기화됩니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;프로세스 재시작 시 초기화&lt;/strong&gt;: OS 메모리 확보 등을 위해 앱이 백그라운드에서 강제 종료되었다가 복귀하면 데이터가 손실됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2) rememberSaveable&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Recomposition 시 유지&lt;/strong&gt;: 기본적인 뷰 업데이트 사이클에서의 보존 기능은 동일합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;화면 회전 시 유지&lt;/strong&gt;: Activity Regeneration 과정 중에도 자동 객체 직렬화/역직렬화를 기반으로 이전 값들을 정상 복구합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;프로세스 재시작 시 유지&lt;/strong&gt;: 백그라운드에서의 예기치 않은 종료 이후 구동될 때도 저장 값이 안전하게 복구됩니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;제약 사항&lt;/strong&gt;: SavedState(Bundle) 객체에 저장 가능한 허용 범위 구조체 타입(Int, String, Boolean 등 원시 및 일부 선형 데이터)에 대해서만 작동을 보장합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;3. 활용 방식&lt;/h2&gt;
&lt;h3&gt;1) remember 활용 패턴&lt;/h3&gt;
&lt;p&gt;화면 전환 등으로 데이터가 초기화되어도 서비스 경험에 심각한 치명도를 발생시키지 않는, 온전한 임시 UI 상태 제어에 최적화된 방법입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun RememberExample() {
    var count by remember { mutableStateOf(0) }

    Button(onClick = { count++ }) {
        Text(&amp;quot;Count: $count&amp;quot;)
    }
    // 디바이스 가로 모드 진입 시 즉각 0으로 값 손실 및 초기화됨
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2) rememberSaveable 활용 패턴&lt;/h3&gt;
&lt;p&gt;입력 도중 유실될 경우 사용자 측면의 피로도가 극도로 높은 핵심 상태 정보 유지에 적용합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun RememberSaveableExample() {
    var count by rememberSaveable { mutableStateOf(0) }

    Button(onClick = { count++ }) {
        Text(&amp;quot;Count: $count&amp;quot;)
    }
    // 화면을 가로 및 세로로 회전하여도 현재까지 반영된 카운팅 횟수가 안전하게 복원됨
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3) 사용자 입력 데이터 보존의 예&lt;/h3&gt;
&lt;p&gt;장문의 텍스트 입력 폼 등은 유실 스트레스가 높으므로 필수적으로 Saveable을 구성 대상별로 각각 적용합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun UserInputForm() {
    var name by rememberSaveable { mutableStateOf(&amp;quot;&amp;quot;) }
    var email by rememberSaveable { mutableStateOf(&amp;quot;&amp;quot;) }

    Column {
        TextField(
            value = name,
            onValueChange = { name = it },
            label = { Text(&amp;quot;이름 입력 영역&amp;quot;) }
        )
        TextField(
            value = email,
            onValueChange = { email = it },
            label = { Text(&amp;quot;이메일 입력 영역&amp;quot;) }
        )
    }
    // 데이터 보존 안정성 확보
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4) 임시 뷰 컴포넌트 상태 배제법 (remember 사용 권장안)&lt;/h3&gt;
&lt;p&gt;드롭다운 탭 메뉴 확장, 다이얼로그의 임시 표출 여부는 굳이 Bundle 직렬화 딜레이와 비용을 낭비하면서까지 보존할 타겟이 아닙니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun DropdownExample() {
    var expanded by remember { mutableStateOf(false) }

    Box {
        Button(onClick = { expanded = true }) {
            Text(&amp;quot;메뉴 리스트 펼치기&amp;quot;)
        }
        DropdownMenu(
            expanded = expanded,
            onDismissRequest = { expanded = false }
        ) {
            // DropDown 리스트 뷰 구축
        }
    }
    // 화면 회전 이후 닫혀 있는 상태(초기 상태)가 되어도 크게 무방함
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;4. 부가 설명&lt;/h2&gt;
&lt;h3&gt;1) 사용 기준점 종합 가이드&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;remember를 사용해야 하는 경우&lt;/strong&gt;:&lt;ul&gt;
&lt;li&gt;다이얼로그, 드롭다운 창 등의 단순 UI 확장 표출 상태&lt;/li&gt;
&lt;li&gt;임시로 로드되는 애니메이션 상태&lt;/li&gt;
&lt;li&gt;오버헤드 방지 및 속도 최적화가 필요한 대칭성 UI&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;rememberSaveable을 사용해야 하는 경우&lt;/strong&gt;:&lt;ul&gt;
&lt;li&gt;입력 텍스트 폼, 패스워드 폼 데이터&lt;/li&gt;
&lt;li&gt;결제창에서의 유저 선택 체크 여부&lt;/li&gt;
&lt;li&gt;기타 디바이스 화면 회전이나 전화 수신 등 태스크 전환에 강건히 대응해야 하는 필수 비즈니스 로직 정보&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2) Bundle 컴포넌트 특성&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Android OS 코어에서 데이터 전달 및 단발성 보존을 할당하는 핵심 매커니즘 컴포넌트입니다.&lt;/li&gt;
&lt;li&gt;객체 직렬화가 동반되므로 원시 타입 파라미터가 아닌 커스텀 개발 모델 객체 등 특이 구조의 데이터를 보존할 때는 별도로 Saveable 인터페이스 설정 작업을 거쳐야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3) 성능 팩터 비교&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;remember&lt;/code&gt;: 힙 메모리 세션에 단발성 보관. 지연 및 과부하가 없어 상대적으로 성능 속도가 우위에 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rememberSaveable&lt;/code&gt;: OS가 시스템 내부에 상태를 안전하게 직렬화 및 적재하느라 다소간의 IO 딜레이가 발생합니다.&lt;/li&gt;
&lt;li&gt;다만 모던 모바일 디바이스 성능 체계 내에서는 이 두 프로세스 간의 체감 지연 차이는 일반적인 용례 내에서 완전히 무시 가능한 수준입니다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Tech/App-Android</category>
      <category>Android</category>
      <category>APP</category>
      <category>kotlin</category>
      <author>JSJH._.</author>
      <guid isPermaLink="true">https://jsjh3363.tistory.com/39</guid>
      <comments>https://jsjh3363.tistory.com/39#entry39comment</comments>
      <pubDate>Tue, 24 Mar 2026 09:15:58 +0900</pubDate>
    </item>
  </channel>
</rss>