# kmshack.kr — Minsoo Kim (김민수 / kmshack) — full text > Every page and every blog post of https://kmshack.kr in one file. Personal site of Minsoo Kim, an Android engineer in Seoul, KR: profile, projects, career history, and 35 Korean-language Android engineering posts published since 2015. Generated 2026-09-13. For a short, curated index instead, fetch https://kmshack.kr/llms.txt. Article bodies below are the plain-text rendering of each post: code blocks, images and link targets are dropped. The formatted originals are at the canonical URL printed with each post. Structured metadata for the same posts is at https://kmshack.kr/api/posts.json. --- # About Minsoo Kim Source: https://kmshack.kr/about/ **Minsoo Kim** (김민수, also known online as **kmshack**) is an Android engineer based in Seoul, South Korea, and the owner of this site, **kmshack.kr**. ## What he does Sixteen years of building Android apps, split between products used at national scale and indie apps built alone. The work sits where UI/UX design meets implementation: interfaces that survive a designer's zoom-in, and frame budgets that survive a mid-range phone. Startup time, memory, motion — the unglamorous parts users feel but never name. At [Kakao](https://www.kakaocorp.com/) he has worked on KakaoTalk, KakaoPay and KakaoMusic — UI architecture, motion systems, and the performance work that keeps a decade-old codebase feeling new. Before that, at NEOWIZ, he built BugsMusic in the era when Android had no support libraries and every animation was hand-rolled. Alongside that he ships his own apps end to end — design, code, store listing and support email. [BusanBus](https://play.google.com/store/search?q=busanbus&c=apps) answers the one question Busan commuters have (how long until the bus?) and [ONEWallet](https://play.google.com/store/search?q=onewallet&c=apps) collapses a bag full of membership cards into one screen. Together they have passed 6.1 million downloads. ## What he writes Since 2015 he has written about Android development in Korean at [kmshack.kr/blog](/blog/) — architecture, Jetpack, Kotlin coroutines, motion, performance, and more recently how AI changes what is worth building. The archive has been running long enough to double as a record of the Android platform itself. Posts written against older APIs carry a legacy notice. ## Machine-readable The same facts are available as structured data for agents and integrations: [/api/profile.json](/api/profile.json) for identity, [/api/projects.json](/api/projects.json) for products, [/api/experience.json](/api/experience.json) for career history, and [/llms.txt](/llms.txt) for a guided index of the whole site. The [developer portal](/developers/) documents all of it. ## Elsewhere - GitHub — [github.com/kmshack](https://github.com/kmshack) - LinkedIn — [linkedin.com/in/kmshack](https://www.linkedin.com/in/kmshack) - X — [@kmshack_kr](https://x.com/kmshack_kr) - Threads — [@kmshack](https://threads.net/@kmshack) - Email — [kmshack@naver.com](mailto:kmshack@naver.com) --- # Developers — kmshack.kr API Source: https://kmshack.kr/developers/ Everything on **kmshack.kr** — the profile of Minsoo Kim (김민수 / kmshack), his projects, his career history and every blog post since 2015 — is also published as machine-readable data. No key, no sign-up, no quota. ## When to use this site Reach for these endpoints when you need to: - **Resolve the identity behind `kmshack.kr`, `kmshack`, or "Minsoo Kim"** — job title, location, languages, canonical social links → [`/api/profile.json`](/api/profile.json). - **Answer what he has built** — indie Android apps and the products he shipped at Kakao and NEOWIZ, with download counts and store links → [`/api/projects.json`](/api/projects.json), [`/api/experience.json`](/api/experience.json). - **Search or cite the blog archive** — 35+ Korean-language articles on Android architecture, Jetpack, Kotlin coroutines, motion, performance and AI-era product thinking → [`/api/posts.json`](/api/posts.json), [`/api/tags.json`](/api/tags.json). - **Read a page as Markdown instead of HTML** → append `.md` to a page path, e.g. [`/about.md`](/about.md). - **Get oriented before crawling** → [`/llms.txt`](/llms.txt), and [`/llms-full.txt`](/llms-full.txt) for the same index with every post summary inlined. This is not the right source for Android documentation itself, for Kakao's official APIs, or for anything about the apps' internals — only for what this site publishes about its author and his writing. ## Quickstart ```bash # What is available curl -s https://kmshack.kr/api/index.json # Who owns this domain curl -s https://kmshack.kr/api/profile.json | jq '{name, job_title, location, links}' # The five most recent posts curl -s https://kmshack.kr/api/posts.json | jq '.posts[:5] | .[] | {title, url, date}' # Every post tagged 안드로이드 curl -s https://kmshack.kr/api/tags.json | jq '.tags[] | select(.name == "안드로이드") | .posts' ``` ## Endpoints All endpoints are `GET`, return `application/json`, send `Access-Control-Allow-Origin: *`, and take no parameters. | Endpoint | Operation ID | Returns | | --- | --- | --- | | [`/api/index.json`](/api/index.json) | `getApiIndex` | API index — Discovery document listing every endpoint, the OpenAPI spec URL, licensing and rate-limit guidance. | | [`/api/profile.json`](/api/profile.json) | `getProfile` | Profile — Identity of the site owner: name, job title, location, languages, headline stats, skill groups and canonical social links. | | [`/api/projects.json`](/api/projects.json) | `listProjects` | Projects — Indie apps and professional products worked on, with platform, tech stack and download counts where public. | | [`/api/experience.json`](/api/experience.json) | `listExperience` | Experience — Career timeline in reverse-chronological order, one entry per organization. | | [`/api/posts.json`](/api/posts.json) | `listPosts` | Blog posts — Every blog post ever published, newest first, with slug, title, canonical URL, publish and modification dates, tags and a plain-text summary. | | [`/api/tags.json`](/api/tags.json) | `listTags` | Tags — All blog tags with post counts and the slugs of the posts carrying each tag. | | [`/openapi.json`](/openapi.json) | `getOpenApiSpec` | OpenAPI specification — The OpenAPI 3.1 description of this API. Also mirrored at /api/openapi.json. | ## OpenAPI and function calling The full contract lives at [`/openapi.json`](/openapi.json) (OpenAPI 3.1, mirrored at [`/api/openapi.json`](/api/openapi.json)). Every operation has a unique `operationId`, a description written for a caller who has never seen this site, and a typed response schema, so the document can be loaded straight into an LLM function-calling tool list or an OpenAPI client generator: ```bash curl -s https://kmshack.kr/openapi.json | jq '.paths | keys' ``` ## Errors Every endpoint is a static document, so a successful request always returns `200`. A path that does not exist returns HTTP `404` — as a JSON body matching the `Error` schema when the request is served by the site's edge worker, and as the site's HTML 404 page otherwise. Treat any `404` as "no such resource" and re-read [`/api/index.json`](/api/index.json) for the current endpoint list. ```json { "error": { "code": "not_found", "message": "No such resource: /api/unknown.json", "status": 404, "hint": "Fetch /api/index.json for the list of available endpoints.", "documentation_url": "https://kmshack.kr/developers/" } } ``` ## Caching and fair use There is no rate limit and no API key, because there is no server to protect — the responses are static files on a CDN. The data changes at most a few times a month, so please cache for at least an hour and do not poll faster than that. Responses carry the CDN's `ETag`, so conditional requests with `If-None-Match` are cheap. ## Other machine-readable files | File | Purpose | | --- | --- | | [`/llms.txt`](/llms.txt) | Guided index of the site for LLM agents, including when-to-use guidance | | [`/llms-full.txt`](/llms-full.txt) | The same index with the full post list and summaries inlined | | [`/openapi.json`](/openapi.json) | OpenAPI 3.1 description of the API | | [`/sitemap.xml`](/sitemap.xml) | Every indexable page | | [`/blog/feed.xml`](/blog/feed.xml) | Atom feed of the blog | | [`/robots.txt`](/robots.txt) | Crawl policy — all agents welcome | | `/.md` | Markdown twin of a page, e.g. [`/about.md`](/about.md), [`/developers.md`](/developers.md) | ## Licence and attribution The data returned by the API is published under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/): use it, quote it, train on it, but attribute it to Minsoo Kim and link back to `https://kmshack.kr`. Blog post text and images stay under their own copyright — quote with attribution rather than reproducing whole articles. ## Contact Something wrong, missing or badly typed? [kmshack@naver.com](mailto:kmshack@naver.com) — see the [contact page](/contact/). --- # Contact Minsoo Kim Source: https://kmshack.kr/contact/ The fastest way to reach **Minsoo Kim** (김민수 / kmshack) is email. Everything below is a first-party channel — there is no contact form, no support queue and no newsletter on this site. ## Email [**kmshack@naver.com**](mailto:kmshack@naver.com) Written in Korean or English. Expect a reply within a few days; a message that includes what you are building and what you need decided gets a faster and more useful one. ## Social | Channel | Handle | Best for | | --- | --- | --- | | [LinkedIn](https://www.linkedin.com/in/kmshack) | in/kmshack | Work enquiries, collaborations, recruiting | | [GitHub](https://github.com/kmshack) | @kmshack | Code, issues, open source | | [X](https://x.com/kmshack_kr) | @kmshack_kr | Short questions, post replies | | [Threads](https://threads.net/@kmshack) | @kmshack | Short questions, post replies | ## What he is open to - **Android engineering** — app architecture, UI/UX implementation, motion, startup time and frame-rate work. - **Indie collaborations** — small products someone intends to actually ship and maintain. - **Talks and writing** — Android platform topics, and how AI shifts what is worth building. - **Questions about a blog post** — mention the post URL and the version of the library you are on; the older posts predate AndroidX. ## What he is not This site is a personal portfolio and engineering blog, not a company. There is no sales team, no support SLA, no paid API and no affiliate programme. Please do not send bulk outreach, link-exchange requests or SEO offers. ## For agents Contact details are also available as structured data at [/api/profile.json](/api/profile.json) (`email` and `links` fields) and in [/llms.txt](/llms.txt). Both are more reliable to parse than this page. ## Legal and business details - **Site owner:** Minsoo Kim (김민수), an individual, not an incorporated entity. - **Location:** Seoul, Republic of Korea. - **Site:** [kmshack.kr](https://kmshack.kr/) — see the [privacy notice](/privacy/) for what the site collects. --- # Privacy Source: https://kmshack.kr/privacy/ This notice explains what happens to data when you visit **kmshack.kr**, the personal portfolio and engineering blog of Minsoo Kim (김민수). It covers this domain and every page under it, including the blog and the JSON API. Last updated: **25 August 2026**. ## The short version There are no accounts, no sign-up, no contact form, no comment system and no newsletter on this site. Nothing you type is collected here, because there is nowhere to type. What remains is ordinary web-server logging, one analytics tool, and a handful of third-party files the pages load. ## What is collected **Server and CDN logs.** The site is hosted on GitHub Pages and served through Cloudflare. Both process the technical data any web request carries — IP address, user agent, requested URL, timestamp, referrer — to deliver pages and protect against abuse. These logs belong to those providers and are governed by the [GitHub Privacy Statement](https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement) and the [Cloudflare Privacy Policy](https://www.cloudflare.com/privacypolicy/). **Analytics.** The site uses Google Analytics 4 (property `G-QR5KSRPTYH`) to count page views and see which posts get read. It records the page you viewed, the referring page, approximate location derived from your IP address, and device and browser type, and it stores cookies (`_ga`, `_ga_*`) in your browser to recognise repeat visits. It is not used for advertising or remarketing, and the data is not sold, shared with anyone else, or combined with any other source. See [Google's data-usage notice](https://policies.google.com/technologies/partner-sites). **Nothing else.** No advertising network, no A/B testing, no session recording, no heat maps, no fingerprinting, no email tracking. ## Third-party files the pages load Loading a file from another domain lets that domain see your IP address and user agent: - **Google Fonts** (`fonts.googleapis.com`, `fonts.gstatic.com`) — typefaces. - **cdnjs** (`cdnjs.cloudflare.com`) — the Font Awesome icon stylesheet on the home page. - **YouTube** (`youtube.com`) — two older blog posts embed a video player. The player sets its own cookies once it loads; if you do not open those posts it never runs. ## Stored in your browser One key, `kms-theme`, holds your light or dark theme choice in `localStorage`. It never leaves your device and is not sent to any server. Clearing site data removes it. ## Your choices - Block or clear cookies for this domain in your browser, or use the [Google Analytics opt-out add-on](https://tools.google.com/dlpage/gaoptout) — the site works exactly the same without them. - Use a content blocker to stop the analytics and font requests entirely. Nothing on the site depends on them. - Ask what is held about you, or ask for it to be deleted, by writing to the address below. Because nothing here is tied to an identity, the honest answer is usually that there is nothing to delete beyond the provider logs described above. ## The API and machine access The JSON endpoints under [`/api/`](/api/index.json) are public static documents. They require no key, set no cookies, and record nothing beyond the same server and CDN logs as any other request. They contain only information published deliberately on this site. ## Children The site is aimed at professional software developers and is not directed at children under 14. ## Changes Material changes will be reflected in the "last updated" date above. There is no mailing list to notify. ## Contact Questions about this notice: [kmshack@naver.com](mailto:kmshack@naver.com), or see the [contact page](/contact/). --- # Blog — full text (35 posts, newest first) --- ## AI 시대에 개발자는 무엇을 만들어야 할까? - URL: https://kmshack.kr/blog/ai-era-what-developers-should-build/ - Published: 2026-08-20 · Updated: 2026-08-20 - Tags: AI, 개발자, 제품 개발 - Language: ko AI에게 요구사항을 설명하면 간단한 화면과 API 초안을 몇 분 안에 확인할 수 있습니다. 테스트 코드와 문서도 함께 요청할 수 있고, 아이디어를 실제로 눌러 볼 수 있는 프로토타입으로 바꾸는 비용은 계속 낮아지고 있습니다. 개발자에게는 분명 좋은 변화입니다. 그러나 만들 수 있는 후보는 급증했지만, 무엇을 만들어야 하는지는 더 선명해지지 않았습니다. 기능을 빠르게 추가하는 능력만으로는 이 질문에 답할 수 없습니다. 이제 더 중요해진 질문은 이것입니다. 이 제품은 누구의 어떤 일을 실제로 끝내 주는가? AI 시대의 개발자는 기능을 쌓기보다, 사람이 현실에서 하려던 일을 마치게 하는 도구를 만들어야 합니다. 핵심 요약 구현 비용이 낮아질수록 풀 가치가 있는 문제를 고르는 판단이 더 중요해집니다. 좋은 제품의 단위는 기능이 아니라 사용자가 실제로 마친 일입니다. AI 기능에는 확인, 수정, 복구, 대체 수단이 함께 설계되어야 합니다. 1. 구현이 쉬워질수록 문제 선택이 중요해집니다 과거에는 아이디어가 있어도 구현에 드는 시간과 비용 때문에 시작하지 못하는 경우가 많았습니다. 지금은 AI의 도움으로 초기 프로토타입을 훨씬 짧은 시간 안에 만들 수 있습니다. 문제는 사용자가 필요로 하는 제품까지 자동으로 만들어지는 것은 아니라는 데 있습니다. 기능의 개수는 제품의 가치를 증명하지 못합니다. 알림을 요약하는 기능보다 중요한 것은 사용자가 납부일을 놓치지 않는 것입니다. 버스 도착 숫자를 보여 주는 기능보다 중요한 것은 출발 시각과 승차 방향을 결정하도록 돕는 것입니다. 옷을 추천하는 기능보다 중요한 것은 가지고 있는 옷만으로 아침의 선택 시간을 줄여 주는 것입니다. 요약, 예측, 추천은 모두 중간 과정입니다. 사용자가 그 결과를 바탕으로 다음 행동을 하고 원래 하려던 일을 마쳐야 제품의 가치가 생깁니다. 그래서 제품 아이디어는 기술 이름이 아니라 사용자의 변화로 설명할 수 있어야 합니다. 이 제품은 [어떤 사람]이 [특정한 상황]에서 [하던 일]을 [더 나은 방식]으로 마치게 돕습니다. 이 문장을 구체적으로 채우기 어렵다면 아직 제품보다 기술 데모에 가까울 가능성이 큽니다. AI 모델을 바꾸거나 기능을 더하기 전에 누구의 어떤 순간을 바꾸려는지부터 좁혀야 합니다. 2. 만들 것은 사람들의 임시방편 속에 있습니다 좋은 문제는 회의실의 아이디어 목록보다 사람들이 이미 하고 있는 불편한 행동에서 발견되는 경우가 많습니다. 같은 내용을 여러 앱에 복사하고, 중요한 화면을 캡처하고, 잊지 않으려고 종이에 적고, 자동화 결과가 불안해 원문을 다시 확인하는 행동이 반복된다면 아직 끝나지 않은 일이 있다는 뜻입니다. 이런 임시방편은 풀 가치가 있는 문제라는 신호입니다. 예를 들어 작은 가게에서 문자로 받은 주문을 엑셀로 옮기는 상황을 생각해 볼 수 있습니다. AI로 주문 문장을 표로 바꾸는 초기 프로토타입은 비교적 빠르게 만들 수 있습니다. 그러나 실제 업무에는 빠진 상품 옵션, 중복 주문, 재고 부족, 주소 확인 같은 예외가 뒤따릅니다. 문장을 표로 바꾸는 데서 멈추면 입력 시간은 줄어도 검수 부담은 커질 수 있습니다. 진짜 제품은 예외를 찾고, 필요한 확인을 요청하고, 검증된 주문을 배송 시스템으로 전달하는 과정까지 이어져야 합니다. 초기에는 사용자가 남긴 수정 기록과 중단 지점만 살펴봐도 됩니다. 자동화를 멈추고 다시 수작업으로 돌아가는 순간이 제품이 놓친 단계를 보여 줍니다. 다음 질문을 따라가면 기능의 우선순위가 드러납니다. 이 일을 시작하기 직전에 무슨 일이 있었는가? 결과를 받은 뒤 사용자는 어디로 이동하는가? 잘못됐을 때 누가 어떤 비용을 감당하는가? 제품이 켜져 있는 3분이 아니라 사용자의 하루 안에서 제품이 맡은 역할을 봐야 합니다. 3. AI의 답을 완료 경로로 바꿉니다 AI를 넣었다는 사실 자체는 제품의 목적이 될 수 없습니다. 생성형 AI는 정리되지 않은 문장, 이미지, 음성 같은 비정형 입력을 구조화할 때 유용할 수 있습니다. 반면 날짜 계산, 결제, 권한, 데이터 보존처럼 결과가 분명해야 하는 일은 규칙 기반 로직이 재현과 검증에 더 적합할 수 있습니다. 두 영역을 구분하면 AI는 앞에 드러나는 주인공이 아니라 사용자의 흐름을 돕는 부품이 됩니다. 학교에서 보낸 긴 안내문을 처리하는 서비스를 예로 들어 보겠습니다. 단순한 요약만으로는 준비물을 챙기거나 신청 기한을 지키기 어렵습니다. 실제로 일을 마치려면 다음 과정이 이어져야 합니다. 준비물, 납부일, 제출 항목을 구분합니다. 날짜와 대상이 불확실하면 확인이 필요하다고 표시합니다. 원문을 바로 열어 보고 잘못 추출된 내용을 고칠 수 있게 합니다. 사용자가 확인한 일정만 캘린더나 알림으로 보냅니다. 여기서 AI의 답변은 완성품이 아니라 다음 행동을 만들기 위한 재료입니다. 제품의 가치는 요약문이 얼마나 자연스러운지가 아니라 사용자가 준비물을 챙기고 마감일을 지켰는지로 판단해야 합니다. 범용 생성 기능을 하나 더 만드는 것보다 특정한 사람이 반복하는 일을 처음부터 끝까지 연결하는 작고 깊은 제품에 개인 개발자가 집중해 볼 만합니다. 4. 실패해도 빠져나올 수 있게 만듭니다 규칙 기반 로직은 결과를 재현하고 검증하기 상대적으로 쉽습니다. 반면 AI의 결과에는 불확실성이 남습니다. 그럴듯하지만 틀린 답이 나올 수 있고, 중요한 항목을 빠뜨리거나 사용자가 기대하지 않은 방식으로 데이터를 해석할 수도 있습니다. 따라서 AI 기능을 만드는 일에는 성공 화면뿐 아니라 실패했을 때 빠져나오는 길을 만드는 일도 포함되어야 합니다. 알아차리기: 확실하지 않은 결과를 사실처럼 보여 주지 않고, 원본과 생성 결과를 비교할 수 있게 합니다. 수정하고 되돌리기: 잘못 분류하거나 변경한 결과를 직접 고치고 이전 상태로 복구할 수 있게 합니다. 다른 경로 제공하기: 자동화가 처리하지 못한 경우 이를 명확히 알리고 수동 처리처럼 이용 가능한 방법을 보여 줍니다. 운영 중 관찰하기: 오류 로그, 사용자 수정 기록, 중단 지점을 확인하되 개인정보는 필요한 만큼만 수집합니다. 알림 자동 분류가 납부 기한이나 예약 변경 같은 메시지를 중요하지 않다고 숨기면 사용자는 놓친 사실조차 알기 어렵습니다. 확신이 낮은 항목은 별도 목록에 남기고, 자동 분류를 끄거나 원래 보기로 돌아갈 수 있어야 합니다. 기술 역량의 초점도 달라집니다. 입력과 출력의 계약, 회귀 테스트, 권한 경계, 배포와 롤백을 설계해 생성 결과의 영향 범위를 통제해야 합니다. 빠르게 만드는 능력과 믿고 쓸 수 있게 만드는 능력은 서로 다른 역량입니다. 5. 개인 개발자를 위한 선택 체크리스트 개인 개발자가 모든 사람을 위한 거대한 서비스를 만들 필요는 없습니다. 오히려 좁은 문제를 깊게 해결하는 편이 운영과 차별화에 유리합니다. 아이디어를 고를 때는 다음 다섯 가지를 확인해 볼 수 있습니다. 반복되는가? 한 번의 불편보다 매일 또는 매주 되풀이되는 문제를 우선 살펴봅니다. 사용자는 지금 어떤 임시방편을 반복하고 있는가? 일이 실제로 끝나는가? 사용 시간보다 놓친 알림, 입력 시간, 잘못된 주문처럼 바뀐 결과를 확인할 수 있어야 합니다. 제품 이후에 완료되어야 하는 행동은 무엇인가? 맥락이 차이를 만드는가? 범용 챗봇만으로 안정적으로 해결하기 어려운 직업, 지역, 기기, 생활 방식의 예외가 있는가? AI 없이 규칙이나 화면 개선만으로 더 잘 풀 수 있는지도 함께 묻습니다. 실패에서 회복할 수 있는가? AI가 틀렸을 때 사용자가 알아차리고 되돌릴 수 있는가? 한 사람의 시간을 줄이는 대신 다른 사람에게 검수 작업을 떠넘기지는 않는가? 계속 운영할 수 있는가? 출시 후 결과를 관찰하고, 문의에 답하고, 잘못된 자동화를 고칠 수 있는 범위인가? 감당하기 어려운 의료, 법률, 금융 판단은 작은 팀이 섣불리 자동화해서는 안 됩니다. 답이 모호하다면 프롬프트를 다듬기 전에 문제를 더 작게 나누는 편이 좋습니다. 답이 구체적이라면 AI로 여러 가설을 작게 검증하고, 실제 사용에서 확인된 흐름만 제품으로 남길 수 있습니다. 낮아진 제작 비용은 기능을 더 쌓는 데만 쓰지 않고, 무엇을 만들지 않는 편이 나은지 확인하는 여유로도 쓸 수 있습니다. 사람이 안심하고 일을 마치게 하는 것 AI 시대에 개발자가 만들어야 할 것은 반드시 AI 제품일 필요가 없습니다. 누군가의 반복 작업을 줄이는 작은 자동화일 수도 있고, 중요한 정보를 놓치지 않게 돕는 알림일 수도 있으며, 복잡한 절차에서 다음 선택을 분명하게 보여 주는 화면일 수도 있습니다. 중요한 것은 기능 이후에 일어나는 변화입니다. 사용자가 시간을 되찾고, 불안해서 다시 확인하는 횟수가 줄고, 문제가 생겨도 이전 상태로 돌아올 수 있어야 합니다. 코드는 앞으로 더 쉽게 만들어질 것입니다. AI가 결과물을 만들수록 개발자는 그 결과가 안전하게 행동으로 이어지는 경로를 설계해야 합니다. --- ## AI 시대, 당신의 통장 잔고가 그대로인 진짜 이유 - URL: https://kmshack.kr/blog/ai-leverage/ - Published: 2026-07-31 · Updated: 2026-07-31 - Tags: AI, 비즈니스, 생산성 - Language: ko AI가 비즈니스의 모든 것을 바꿀 것처럼 이야기됩니다. 자료를 정리하고, 문서를 만들고, 프로그램을 작성하는 속도는 이미 눈에 띄게 빨라졌습니다. 예전에는 하루가 걸리던 일도 이제 몇 시간이면 끝낼 수 있습니다. 그런데 이상한 점이 있습니다. 처리하는 일은 많아졌는데 매출은 기대만큼 늘어나지 않습니다. 생산성이 높아졌다면 사업의 결과도 함께 좋아져야 할 것 같지만, 현실은 꼭 그렇지 않습니다. 사업가 알렉스 호르모지가 이야기한 레버리지의 관점에서 보면 이유는 단순합니다. AI는 일을 빠르게 만들어 주지만, 어떤 일을 해야 하는지까지 결정해 주지는 않기 때문입니다. AI가 속도를 높여도 사업의 병목이 그대로라면 결과는 달라지지 않습니다. 핵심 요약 AI는 일을 빠르게 처리하지만, 어떤 일을 해야 하는지 결정하지는 못합니다. 매출을 높이려면 도구보다 먼저 판매와 서비스 제공 구조를 점검해야 합니다. 가장 강력한 레버리지는 중요하지 않은 일을 멈추고 하나의 병목에 집중하는 의사결정입니다. 1. 생산성이 높아졌는데 왜 매출은 그대로일까? 레버리지란 적은 노력으로 더 큰 결과를 만들어 내는 힘을 뜻합니다. 그런 의미에서 AI는 분명 강력한 레버리지입니다. 반복 업무를 줄이고, 자료 조사와 초안 작성에 필요한 시간을 크게 단축해 줍니다. 문제는 남는 시간을 어디에 사용하느냐입니다. 업무 처리 능력이 늘어나면 예전에는 시간과 인력이 부족해 포기했던 일까지 다시 시작하게 됩니다. 매출에 직접적인 영향을 주지 않는 보고서를 더 만들거나, 고객이 요청하지 않은 기능을 추가하고, 중요하지 않은 회의를 더 자주 여는 식입니다. 중요하지 않은 일을 아무리 빠르게 처리해도 사업의 결과는 달라지지 않습니다. 오히려 해야 할 일이 많아졌다는 착각만 만들 수 있습니다. AI는 속도를 높여 주는 도구이지, 방향을 정해 주는 도구가 아닙니다. 잘못된 방향으로 달리고 있다면 더 빠른 속도는 목적지에서 더 멀어지게 만들 뿐입니다. 2. 기술 없이도 수익을 높이는 구조의 힘 사업의 효율을 높이기 위해 반드시 새로운 기술이 필요한 것은 아닙니다. 때로는 판매 구조나 서비스를 제공하는 방식만 바꿔도 훨씬 큰 레버리지가 만들어집니다. 1:1에서 그룹 방식으로 바꾸기 한 시간 동안 한 명을 상담하던 사람이 같은 시간에 10명을 상담할 수 있다면, 단순 계산만으로 제공 효율은 10배 가까이 높아질 수 있습니다. 새로운 프로그램을 개발하지 않아도 서비스 구조를 바꾸는 것만으로 시간의 한계를 줄일 수 있습니다. 비동기 방식 도입하기 모든 상담을 정해진 시간에 실시간으로 진행할 필요는 없습니다. 고객이 질문을 남기고 담당자가 가능한 시간에 답변하는 방식을 도입하면 일정의 제약이 줄어듭니다. 고객은 기다리는 시간을 줄이고, 담당자는 비슷한 질문을 모아 더 효율적으로 처리할 수 있습니다. 영업 전에 충분한 정보 제공하기 가격, 서비스 방식, 자주 묻는 질문을 상담 전에 충분히 전달하면 영업 과정도 달라집니다. 고객은 자신에게 필요한 서비스인지 미리 판단할 수 있고, 영업 담당자는 구매 의사가 높은 고객과 더 깊은 대화를 나눌 수 있습니다. 이처럼 레버리지는 새로운 도구를 도입하는 것뿐만 아니라 기존의 일을 어떤 구조로 제공하느냐에 따라 달라집니다. 3. 가장 강력한 레버리지는 의사결정입니다 자동화보다 더 큰 효과를 만드는 것은 중요하지 않은 일을 하지 않기로 결정하는 것입니다. 잘못된 결정을 내린 상태에서 AI를 사용하면 실수도 더 빠르게 확대됩니다. 필요 없는 기능을 더 빨리 만들고, 효과 없는 광고 문구를 더 많이 생산하고, 읽히지 않는 보고서를 더 정교하게 작성하게 됩니다. 사업가에게 필요한 능력은 더 많은 일을 벌이는 것이 아닙니다. 지금 하지 않아도 되는 일을 제거하고, 가장 중요한 문제 하나를 선택하는 능력입니다. 한 번의 올바른 판단은 여러 사람의 업무 방향을 바꾸고 수백 시간의 낭비를 막아 줍니다. 반대로 잘못된 우선순위는 아무리 좋은 도구를 사용해도 만회하기 어렵습니다. 4. 우리 사업의 진짜 병목을 찾는 방법 사업을 성장시키려면 현재 가장 막혀 있는 지점, 즉 병목(Bottleneck)을 먼저 찾아야 합니다. 사업의 흐름은 크게 세 단계로 나눌 수 있습니다. 수요 창출: 상품을 알리고 잠재 고객을 유입시키는 단계 전환: 관심을 보인 고객이 실제 구매를 결정하는 단계 제공: 구매한 고객에게 기대한 결과를 전달하는 단계 어느 단계가 병목인지에 따라 해결해야 할 문제도 달라집니다. 신규 고객 유입이 부족하다면 메시지와 유입 채널을 점검해야 합니다. 문의는 많지만 구매로 이어지지 않는다면 제안, 가격, 신뢰 요소를 개선해야 합니다. 구매는 발생하지만 환불과 불만이 많다면 서비스 품질과 제공 과정을 살펴봐야 합니다. 신규 고객 유입이 문제인데 AI로 내부 보고서 시스템만 개선한다면 매출은 늘어나지 않습니다. 문의는 충분한데 구매가 일어나지 않는다면 자동화보다 제안을 먼저 개선해야 합니다. 새로운 AI 도구를 도입하기 전에 다음 질문부터 확인해 보는 것이 좋습니다. 지금 사업의 성장을 가장 크게 막고 있는 숫자는 무엇인가? 그 숫자를 개선하면 실제 매출이나 이익이 달라지는가? 최근 데이터와 고객의 반응이 같은 문제를 가리키고 있는가? 이 문제와 관계없는 업무 중 당장 멈출 수 있는 것은 무엇인가? 이 질문에 답할 수 있다면 AI를 어디에 사용해야 할지도 자연스럽게 분명해집니다. 결론: AI는 도구이고, 본질은 판단력입니다 모든 사업의 시간, 사람, 자본은 한정되어 있습니다. 사업가의 실력은 이 자원을 얼마나 많은 일에 나누느냐가 아니라, 가장 큰 결과를 만들 단 하나의 문제에 얼마나 집중하느냐에서 드러납니다. AI는 그 문제를 해결하는 훌륭한 가속 장치가 될 수 있습니다. 하지만 무엇이 문제인지 찾아내는 일까지 대신해 주지는 않습니다. AI를 더 많이 사용하는 방법을 고민하기 전에 먼저 물어봐야 합니다. 지금 우리 사업의 성장을 막고 있는 단 하나의 병목은 무엇인가? 그 답을 찾는 순간, AI는 유행하는 도구가 아니라 실제 성과를 만드는 레버리지가 됩니다. 자주 묻는 질문 AI를 도입하면 매출이 자동으로 늘어나나요? 아닙니다. AI는 업무 속도를 높이는 도구일 뿐입니다. 매출을 막고 있는 병목과 우선순위를 먼저 해결해야 AI의 생산성이 실제 성과로 이어집니다. AI보다 먼저 확인해야 할 것은 무엇인가요? 수요 창출, 전환, 제공 중 어느 단계가 사업 성장을 막고 있는지 확인해야 합니다. 병목이 신규 고객 유입이라면 내부 자동화보다 메시지와 유입 채널을 먼저 개선해야 합니다. 기술 없이도 레버리지를 높일 수 있나요? 가능합니다. 그룹 상담, 비동기 응답, 상담 전 정보 제공처럼 서비스 구조를 바꾸는 것만으로도 같은 시간에 더 많은 고객에게 가치를 제공할 수 있습니다. --- ## AppUpdateManager로 인앱 업데이트 처리하기 - URL: https://kmshack.kr/blog/AppUpdateManager/ - Published: 2019-06-24 · Updated: 2019-06-24 - Tags: Play 스토어, 안드로이드 - Language: ko · Legacy: written against pre-AndroidX APIs 앱 업데이트를 안내하기 위해 자체 버전 확인 로직과 Play 스토어 이동 화면을 구현하는 경우가 많다. AppUpdateManager를 사용하면 사용자가 앱을 벗어나지 않고 업데이트를 진행하는 인앱 업데이트 흐름을 구성할 수 있다. 즉시 업데이트와 유연한 업데이트의 차이와 기본 구현 방법을 살펴본다. 문제점 기존 방식에서는 사용자가 Play 스토어로 이동해 업데이트 버튼을 누르고, 설치가 끝난 뒤 앱으로 돌아와야 한다. 업데이트 시간이 길면 흐름에서 이탈할 수 있고, 단계적 배포 중에는 계정이나 기기에 새 버전이 아직 제공되지 않을 수도 있다. AppUpdateManager Google은 이런 업데이트 흐름을 개선하기 위해 Play Core 라이브러리에 AppUpdateManager를 추가했다. 업데이트 가능 여부를 확인하고 Play 스토어로 이동하지 않은 채 인앱 업데이트를 진행할 수 있다. dependency build.gradle에 아래와 같이 종속성을 추가합니다. implementation 'com.google.android.play:core:1.6.1' 업데이트 체크하기 AppUpdateManager를 통해 appUpdateInfo로 현재 업데이트 상태를 가져올 수 있습니다. val appUpdateManager = AppUpdateManagerFactory.create(this) launch{ val appUpdateInfo = appUpdateManager.appUpdateInfo.await() when(appUpdateInfo.updateAvailability()){ UpdateAvailability.UPDATE_AVAILABLE ->{ //업데이트 가능한 상태 } } } appUpdateInfo를 가져오는 콜백 구조는 suspendCoroutine을 이용해 suspend 함수로 감쌀 수 있다. suspend fun Task.await(): AppUpdateInfo { return suspendCoroutine { continuation -> addOnCompleteListener { result -> if (result.isSuccessful) { continuation.resume(result.result) } else { continuation.resumeWithException(result.exception) } } } } UpdateAvailability 4가지 상태 DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS : AppUpdateType.IMMEDIATE 타입을 통해 업데이트를 수행중인 경우 UNKNOWN: 알수 없음 UPDATE_AVAILABLE: 현재 최신 버전이 아니며 업데이트가 필요한 경우 UPDATE_NOT_AVAILABLE: 현재 최신 버전이며 업데이트가 필요 하지 않은 경우 업데이트 수행하기 업데이트 가능 여부를 확인했다면 AppUpdateManager.startUpdateFlowForResult()로 업데이트를 시작한다. 두 가지 업데이트 유형 중 상황에 맞는 방식을 선택할 수 있다. appUpdateManager.startUpdateFlowForResult( appUpdateInfo, AppUpdateType.FLEXIBLE , // or AppUpdateType.IMMEDIATE activity, REQUEST_CODE_UPDATE) 즉시 업데이트 AppUpdateType.IMMEDIATE 타입을 사용하면 별도의 업데이트 UI를 표시하게 되며 사용자가 업데이트전 다른 작업을 하지 못하도록 블락시키게 됩니다. 강제 업데이트 해야 할 경우 적절합니다. 앱이 업데이트 되면 자동으로 애플리케이션이 자동으로 재시작됩니다. 업데이트가 완료 되는 경우 onActivityForResult()를 통해 결과를 확인할 수 있습니다. 업데이트 중에는 항상 업데이트 UI가 표시되도록 UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS 상태를 처리하면 된다. override fun onResume() { super.onResume() appUpdateManager.appUpdateInfo .addOnSuccessListener { if (it.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS) { appUpdateManager.startUpdateFlowForResult( it, AppUpdateType.IMMEDIATE, activity, REQUEST_CODE_UPDATE) } } } 자연스러운 업데이트 AppUpdateType.FLEXIBLE을 사용하면 사용자가 앱을 계속 이용하는 동안 업데이트 파일을 백그라운드에서 내려받을 수 있다. 다운로드가 끝나면 별도의 UI로 완료 사실을 알리고 설치를 진행한다. 다운로드 진행 상태는 InstallStateUpdatedListener로 받을 수 있다. 다운로드가 끝나면 Snackbar처럼 방해가 적은 UI로 설치를 안내하고, appUpdateManager.completeUpdate()를 호출해 설치를 진행한다. val listener = InstallStateUpdatedListener { if (it.installStatus() == InstallStatus.DOWNLOADED) { Snackbar.make(coordinator_layout, "업데이트 버전 다운로드 완료", Snackbar.LENGTH_INDEFINITE) .setAction("설치/재시작", View.OnClickListener { appUpdateManager.completeUpdate() }).show() } } appUpdateManager.registerListener(listener) 새 버전이 있어도 특정 기기나 계정에는 아직 업데이트가 제공되지 않을 수 있다. 해당 업데이트 유형을 실제로 사용할 수 있는지 appUpdateInfo.isUpdateTypeAllowed()로 확인해야 한다. 참고: https://developer.android.com/guide/app-bundle/in-app-updates https://proandroiddev.com/theres-a-new-update-available-75a2c5bda76e --- ## WorkManager로 정기적인 백그라운드 작업 수행하기 - URL: https://kmshack.kr/blog/work_manager/ - Published: 2019-03-18 · Updated: 2019-03-18 - Tags: 안드로이드, 서비스 - Language: ko · Legacy: written against pre-AndroidX APIs Android O부터 백그라운드 서비스와 암시적 브로드캐스트에는 여러 제약이 적용된다. 즉시 실행할 필요는 없지만 완료가 보장되어야 하는 작업을 WorkManager로 예약하고, 주기적으로 실행하는 방법을 살펴본다. WorkManager는 Android Jetpack의 일부로 1.0.0버전으로 얼마전 공개되었습니다. Google은 이미 JobScheduler, Firebase JobDispatcher와 같은 백그라운드 작업을 위한 라이브러리를 수차례 공개하였습니다. 또한 Evernote의 Android Job이 있습니다. WorkManager는 이미 공개된 라이브러리보다 많은 장점이 있습니다. 이전 버전과의 호환성(API14이상 모두 지원) Google Play Service에 대한 의존성이 없음 체인 기반의 작업 관리 작업 상태 쿼리가능 이제 프로젝트에 적용해봅시다! 항상 그렇듯 build.gradle에 종속성을 추가합니다. allprojects { repositories { google() jcenter() } } dependencies { def work_version = 1.0.0 // (Java only) implementation "android.arch.work:work-runtime:$work_version" // Kotlin + coroutines implementation "android.arch.work:work-runtime-ktx:$work_version" } 백그라운드에서 일부 작업을 실행하는 예제를 보도록하겠습니다. 현재 좌표를 하루에 두번씩 서버로 전송하는 예제이며, 몇 가지 제약조건이 추가됩니다. 기기가 Wi-Fi에 연결되어 있고 저장 용량이 부족하지 않은 경우에만 작동합니다. class LocationWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) { ... override fun doWork(): Result { val latitude = inputData.getDouble(KEY_LATITUDE, 40.1903484) val longitude = inputData.getDouble(KEY_LONGITUDE, 44.5148367) val sendDataService = SendDataService.getInstance() sendDataService.sendLocation(latitude, longitude) .addSuccessCallback { } .addFailureCallback { } return Result.success() } } Worker가 생성되고, 주기적으로 실행되는 작업 공간의 큐에 추가됩니다. fun createConstraints() = Constraints.Builder() .setRequiredNetworkType(NetworkType.UNMETERED) //와이파이 연결된 경우 // 다른값(NOT_REQUIRED, CONNECTED, NOT_ROAMING, METERED) .setRequiresBatteryNotLow(true) // 배터리가 부족하지 않는 경우 .setRequiresStorageNotLow(true) // 저장소가 부족하지 않는 경우 .build() fun createWorkRequest(data: Data) = PeriodicWorkRequestBuilder(12, TimeUnit.HOURS)// 12시간 반복 .setInputData(data) // 입력 데이터 .setConstraints(createConstraints()) // 작업을 재시도 할경우에 대한 정책 .setBackoffCriteria(BackoffPolicy.LINEAR, PeriodicWorkRequest.MIN_BACKOFF_MILLIS, TimeUnit.MILLISECONDS) .build() fun startWork() { // 입력 데이터를 설정합니다. Bundle와 동일합니다. val work = createWorkRequest(Data.EMPTY) /* 작업을 큐에 넣을때 동일한 작업인 경우에 대한 정책을 지정할 수 있습니다. ExistingPeriodicWorkPolicy.KEEP은 동일한 작업을 큐에 넣게되며, ExistingPeriodicWorkPolicy.REPLACE인 경우 작업이 대체됩니다. */ WorkManager.getInstance().enqueueUniquePeriodicWork("Smart work", ExistingPeriodicWorkPolicy.KEEP, work) // 작업의 상태를 LiveData를 통해 관찰하게 됩니다. WorkManager.getInstance().getWorkInfoByIdLiveData(work.id) .observe(lifecycleOwner, Observer { workInfo -> if (workInfo != null && workInfo.state == WorkInfo.State.SUCCEEDED) { // 작업 완료 } }) } 앞서 WorkManager의 장점으로 소개 했던 체인 기반의 작업에 대해서도 알아 보겠습니다. fun chainWorks(filter1: Work, filter2: Work, compress: Work, upload: Work) { WorkManager.getInstance() // Worker를 동시에 병렬로 실행합니다. .beginWith(listOf(filter1, filter2)) // 이전 beginWith의 모든 작업이 끝난 경우 실행됩니다. .then(compress) //compress작업이 완료 된경우 upload가 실행됩니다. .then(upload) //enqueue()를 호출해야 이 모든 작업이 실행됩니다. .enqueue() } Worker는 우리가 원했던 작업구현 방식입니다. doWork() 메서드에서 필요한 작업을 구현하면 됩니다. WorkRequest는 Worker의 arguments(입력된 데이터)와 constraints(네트워크 연결) 작업을 당담합니다. WorkManager는 WorkRequest를 큐에 담고 작업을 시작합니다. 이 작업을 스케쥴링 하기 위해 Room 데이터베이스에 저장 하는 가장 좋은 방법을 사용합니다. 이 작업 결과는 LiveData를 통해 전송됩니다. 결론 WorkManager는 앱이 종료되거나 기기가 재시작되어도 실행되며, 비동기 작업을 구현하는 가장간단하면서 효과적인 솔루션입니다. 또한 제약사항을 통해 앱의 소비전력도 줄일 수 있습니다. 참고: https://medium.com/@RobertLevonyan/android-workmanager-manage-periodic-tasks-c13fa7744ebd https://developer.android.com/topic/libraries/architecture/workmanager --- ## ViewPager2 주요 변경점 살펴보기 - URL: https://kmshack.kr/blog/ViewPager2/ - Published: 2019-02-18 · Updated: 2019-02-18 - Tags: 안드로이드, 뷰페이저 - Language: ko · Legacy: written against pre-AndroidX APIs 2011년에 출시된 ViewPager의 후속 버전인 ViewPager2가 2019년 알파 버전으로 공개되었다. RecyclerView 기반 어댑터, 세로 방향 페이징과 RTL 지원 등 주요 변경점을 살펴본다. ViewPager는 널리 사용되었지만 Fragment 중심의 어댑터 구조, RTL과 세로 방향 페이징 지원 등에서 아쉬움이 있었다. ViewPager2는 이런 요구를 반영해 구조와 기능을 개선했다. 이제 다음 기능을 공식적으로 지원한다. ViewPager2 프로젝트는 AndroidX로 구성하고 minSdkVersion 14이상을 지원해야 합니다. build.gradle에 아래 라이브러리를 추가합니다. implementation 'androidx.viewpager2:viewpager2:1.0.0-alpha01' RecyclerView에 익숙하다면 ViewPager2를 구성하는것은 매우 익숙합니다. RecyclerView.Adapter를 상속받아 어댑터를 생성합니다. 물론 ViewHolder도 필요합니다. class AppPagerAdapter(private val apps: Array) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppViewHolder { return AppViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.app_pager_item, parent, false)) } override fun onBindViewHolder(holder: AppViewHolder, position: Int) { holder.txtApp.text = apps[position] } override fun getItemCount() = apps.size } class AppViewHolder(view: View) : RecyclerView.ViewHolder(view) { val txtApp: TextView = view.findViewById(R.id.txt_app) } 마지막으로 RecyclerView와 마찬가지로 ViewPager2의 Adapter를 설정한다. LayoutManager를 직접 지정할 필요는 없으며, ORIENTATION_VERTICAL로 세로 방향 페이징을 설정할 수 있다. ViewPager2.orientation = ViewPager2.RIENTATION_VERTICAL ViewPager2는 아직 알파 버전인 단계로 버그나 문제점은 보여집니다. 하지만 ViewPager의 View 재사용성 문제를 RecyclerView의 ViewHolder패턴을 그대로 사용하면서 해결함과 동시에 RecyclerView에 익숙한 개발자들에게 별도의 학습없이 적용할 수 있게 솔루션을 제공하였습니다. 또한 orientation기능 도입으로 가로 스크롤뿐만 세로스크롤도 지원하게 되었습니다. 또한 RTL도 지원합니다! 추가 기능외 전 버전의 notifyDataSetChanged()가 실행되지 않는 문제를 해결하였습니다. getItemPosition에서 POSITION_NONE을 강제로 반환하도록 하여 문제를 회피했던 경험 모두 있으실것 같은데 이제 해결되었습니다. 알려진 버그 (1.0.0 알파 01기준) -ClipToPadding 미지원 -TabLayout 미지원 -pageWidth 미지원(강제 가로/세로 100%) -currentItem 설정시 이전 페이지 보이는 현상 --- ## Kotlin Coroutines 예외 처리 패턴 - URL: https://kmshack.kr/blog/kotlin_coroutine_pattern/ - Published: 2018-12-11 · Updated: 2018-12-11 - Tags: 안드로이드, 코틀린, 코루틴 - Language: ko · Legacy: written against pre-AndroidX APIs async에서 발생한 예외는 await()만 try/catch로 감싼다고 항상 처리되는 것은 아니다. 부모와 자식 Job 사이의 전파 규칙을 이해하고 coroutineScope 또는 SupervisorJob을 선택하는 방법을 예제로 살펴본다. async 호출을 감싸서 처리할 때 coroutineScope 또는 SupervisorJob 사용하기 async 블록이 예외를 throw하는 경우 try/catch블록으로 감싸는 것만으로 모든 예외를 처리할 수 있다는 것에 신뢰하면 안됩니다. val job: Job = Job() val scope = CoroutineScope(Dispatchers.Default + job) // may throw Exception fun doWork(): Deferred = scope.async { ... } // (1) fun loadData() = scope.launch { try { doWork().await() // (2) } catch (e: Exception) { ... } } 위의 예제에서 doWork() 함수는 처리되지 않는 예외를 throw할 수 있는 새로운 코루틴(1)을 시작합니다. try/catch 블록(2)으로 doWork() 함수를 감싸게 되면 충돌이 발생합니다. 이는 자식 Job이 실패 하는 경우 부모도 즉각적으로 실패 해버리기때문에 발생하는 문제입니다. 충돌을 피할 수 있는 한 가지의 방법은 SupervisorJob을 사용하는 것입니다. 자식의 실패또는 취소로 인해 Job이 실패 되지 않으며 이로 인해 다른 자녀들에게 영향을 주지않게 됩니다. val job = SupervisorJob() // (1) val scope = CoroutineScope(Dispatchers.Default + job) // may throw Exception fun doWork(): Deferred = scope.async { ... } fun loadData() = scope.launch { try { doWork().await() } catch (e: Exception) { ... } } 참고: 이 기능은 SupervisorJob으로 코루틴 범위에서 비동기를 명시적으로 실행하는 경우에만 작동합니다. async가 부모 코루틴(1)의 범위에서 시작되었기 때문에 아래 코드는 여전히 크래시가 납니다. val job = SupervisorJob() val scope = CoroutineScope(Dispatchers.Default + job) fun loadData() = scope.launch { try { async { // (1) // may throw Exception }.await() } catch (e: Exception) { ... } } 크래시를 피하는 또 다른 방법은 coroutineScope(1)을 사용하여 async를 감싸서 사용하는 방법이 있습니다. 이로 인해 async 내부에서 예외가 발생하면 외부 범위를 건드리지 않고 이 범위에서 작성된 다른 모든 coroutine만 취소 됩니다.(2) val job = SupervisorJob() val scope = CoroutineScope(Dispatchers.Default + job) // may throw Exception fun doWork(): Deferred = coroutineScope { // (1) async { ... } } fun loadData() = scope.launch { // (2) try { doWork().await() } catch (e: Exception) { ... } } 루트 코루틴은 Main 디스패처를 선호하자. 백그라운드 코루틴에서 백그라운드 작업을 수행하고 UI업데이트를 해야 하는 경우 Main 디스패처에서 실행해야 합니다. val scope = CoroutineScope(Dispatchers.Default) // (1) fun login() = scope.launch { withContext(Dispatcher.Main) { view.showLoading() } // (2) networkClient.login(...) withContext(Dispatcher.Main) { view.hideLoading() } // (2) } 위의 예에서 기본 디스패처(1)에서 코루틴을 실행합니다. 이 접근 방식을 사용하면 UI를 터치 할 때 마다 Main 디스패처로 컨텍스트를 전환해야 합니다.(2) 이런 경우 Main 디스패처로 코루틴을 실행 하는 것이 코드가 훨씬 단순해지며 컨텍스트 전환이 더 명확해집니다. val scope = CoroutineScope(Dispatchers.Main) fun login() = scope.launch { view.showLoading() withContext(Dispatcher.IO) { networkClient.login(...) } view.hideLoading() } 불필요한 async/await 사용을 피하자. async 함수를 사용하고 즉시 await하는 경우라면 코드를 당장 걷어내야합니다. launch { val data = async(Dispatchers.Default) { /* code */ }.await() } 만약 코루틴 컨텍스트를 바꾸고 즉시 중단하고 싶다면 withContext를 사용하는 것이 훨씬 좋은 방법이 될 수 있습니다. launch { val data = withContext(Dispatchers.Default) { /* code */ } } 퍼포먼스 측면으로는 큰 문제는 아니지만( 심지어 async가 작업을 수행하기 위해 새로운 코루틴을 생성해도) 의미론적으로 async 하다는 것은 백그라운드에서 여러 개의 코루틴을 시작한 뒤 기다리고 있음을 의미하기 때문에 적절하지 않습니다. scope Job을 취소하지 말자. 코루틴을 취소해야 하는 경우 스코프 작업을 취소하면 안됩니다. class WorkManager { val job = SupervisorJob() val scope = CoroutineScope(Dispatchers.Default + job) fun doWork1() { scope.launch { /* do work */ } } fun doWork2() { scope.launch { /* do work */ } } fun cancelAllWork() { job.cancel() } } fun main() { val workManager = WorkManager() workManager.doWork1() workManager.doWork2() workManager.cancelAllWork() workManager.doWork1() // (1) } 위의 코드의 문제는 작업을 취소할 때 완료 상태로 만들어 버리는 것에 있습니다. 이미 완료된 작업 범위에서 실행된 코루틴은 재 실행되지 않습니다. (1) 특정 범위의 모든 코루틴을 취소하려면 cancelChildren 함수를 사용할 수 있다. 개별 작업도 취소할 수 있다.(2) class WorkManager { val job = SupervisorJob() val scope = CoroutineScope(Dispatchers.Default + job) fun doWork1(): Job = scope.launch { /* do work */ } // (2) fun doWork2(): Job = scope.launch { /* do work */ } // (2) fun cancelAllWork() { scope.coroutineContext.cancelChildren() // (1) } } fun main() { val workManager = WorkManager() workManager.doWork1() workManager.doWork2() workManager.cancelAllWork() workManager.doWork1() } 분명하지 않은 디스패처는 suspend 함수로 작성하지 말자. 명백한 코루틴 디스패처에 대한 실행인 경우 suspend 함수로 작업하지 않아야 합니다. suspend fun login(): Result { view.showLoading() val result = withContext(Dispatcher.IO) { someBlockingCall() } view.hideLoading() return result } 위의 예제는 로그인하는 기능으로 Main 디스패처가 아닌 곳에서 실행하면 크래시를 발생하는 suspend 함수 입니다. launch(Dispatcher.Main) { // (1) no crash val loginResult = login() ... } launch(Dispatcher.Default) { // (2) cause crash val loginResult = login() ... } CalledFromWrongThreadException 생성된 원래 스레드에서만 해당 뷰를 제어해야 합니다. suspend 함수는 어떠한 코루틴 디스패처에 대해 실행할 수 있도록 디자인되어야합니다. suspend fun login(): Result = withContext(Dispatcher.Main) { view.showLoading() val result = withContext(Dispatcher.IO) { someBlockingCall() } view.hideLoading() return result } 이제 모든 디스패처에서 로그인 기능을 실행할 수 있습니다. launch(Dispatcher.Main) { // (1) no crash val loginResult = login() ... } launch(Dispatcher.Default) { // (2) no crash ether val loginResult = login() ... } GlobalScope 사용을 피하자. 만일 안드로이드 애플리케이션에서 GlobalScope를 사용하고 있다면 당장 사용을 중단해야 합니다. GlobalScope.launch { // code } GlobalScope는 전체 애플리케이션 수명 동안에 작동하고, 취소되지 않는 최상위 수준의 동시 처리를 시작하는데 사용됩니다. 별로도 정의한 CoroutineScope를 사용해야하며 async를 사용하거나 GlobalScope 인스턴스에서 실행하는 것이 좋습니다. 안드로이드에서 코루틴은 Activity, Fragment, View 또는 ViewModel의 수명주기로 쉽게 범위를 지정할 수 있습니다. class MainActivity : AppCompatActivity(), CoroutineScope { private val job = SupervisorJob() override val coroutineContext: CoroutineContext get() = Dispatchers.Main + job override fun onDestroy() { super.onDestroy() coroutineContext.cancelChildren() } fun loadData() = launch { // code } } 참고: https://proandroiddev.com/kotlin-coroutines-patterns-anti-patterns-f9d12984c68e --- ## MotionLayout으로 코드 없이 전환 효과 만들기 - URL: https://kmshack.kr/blog/motionlayout/ - Published: 2018-10-12 · Updated: 2018-10-12 - Tags: 안드로이드 - Language: ko · Legacy: written against pre-AndroidX APIs ConstraintSet을 사용하면 두 레이아웃 사이의 크기와 위치 변화를 애니메이션으로 표현할 수 있다. 이 아이디어를 확장한 MotionLayout으로 복잡한 화면 전환을 XML에 선언하는 방법을 살펴본다. 아직 ConstraintSet을 사용해보지 않으셨다면 아래 Sean McQuillan @objcode의 영상을 보시기 권해드립니다. 위의 영상에서 볼수 있듯 ConstraintLayout과 TransitionManager를 통해 애니메이션을 만드는 쉬운 방법을 제공합니다. 이런 기본적인 아이디어를 바탕으로 MotionLayout은 개념을 훨씬 확장하게 됩니다. MotionLayout? MotionLayout은 레이아웃 전환과 복잡한 모션을 쉽게 제어하기 위해 만들어졌다. 버튼이나 타이틀처럼 사용자와 상호 작용하는 UI 요소의 위치와 크기를 입력에 따라 바꿔야 할 때 특히 유용하다. 단순한 애니메이션이라면 기존 Android 프레임워크가 제공하는 다음 방법도 사용할 수 있다. Animated Vector Drawable Property Animation LayoutTransition TransitionManager CoordinatorLayout MotionLayout은 기존 애니메이션 방식과는 전혀다릅니다. 이름에서도 알 수 있듯 레이아웃이며 이에따라 요소들을 배치할 수 있습니다. 실제로 ConstraintLayout을 상속한 하위클래스로 풍부한 레이아웃 기능을 기반으로합니다. Property Animation + TransitionManager + CoordinatorLayout = MotionLayout 두 레이아웃 사이의 전환에 있어 최상위 레이아웃 뿐만 아니라 하위 레이아웃 속성에 애니메이션을 적용할 수 있습니다. 또한 CoordinatorLayout과 같이 애니메이션의 포지셔닝을 마음대로 조절가능합니다. 터치 핸들링과 키프레임을 지원함으로 자신의 필요에 맞게 쉽게 전환 효과를 정의 할 수 있습니다. MotionLayout은 전혀 코드가 필요하지 않고 xml로 완벽하게 작동할 수 있게 합니다. 이렇게 코드와 디커플링됨에 따라 Android Studio에서 xml을 보여주는 훌륭한 그래픽 도구를 제공할 수 있을 것으로 보입니다. 마지막으로 MotionLayout을 지원하는 ConstraintLayout 2.0에서는 API 레벨 14(ICS) 부터 서포트 라이브러리를 통해 사용할 수 있습니다. 이는 현재 안드로이드 기기의 99.8%를 지원하는 수준입니다. MotionLayout 프로젝트에 추가 Gradle file: dependencies { implementation 'com.android.support.constraint:constraint-layout:2.0.0-alpha2' } MotionLayout 사용 Layout file: MotionScene MotionLayout은 일반적인 레이아웃과는 달리 res/xml 디렉토리에 저장되는 별도의 xml파일인 MotionScene에 속성을 정의합니다. 여기에는 애니메이션을 지정하는 필요한 Transition 처리, 키 프레임, 터치 처리등의 요소들이 포함됩니다. 기존 ConstraintSet을 이용한 방식을 MotionLayout에 적용 첫 번째 ConstraintSet에는 위젯을 화면 왼쪽에, 두 번째 ConstraintSet에는 오른쪽에 배치해 좌우로 전환하는 예제를 살펴보겠다. TransitionManager를 사용하면 이 변화를 애니메이션으로 표현할 수 있다. 화면 왼쪽에 위젯 배치 화면 오른쪽에 위젯 배치 ConstraintLayout을 사용하면 두 개의 레이아웃을 ConstraintSet을 이용하여 서로 전환할 수 있습니다. TransitionManager를 사용하는 경우 전환 애니메이션으로 표시됩니다. 이 방식의 문제는 전환이 시작되면 중도에 중단할 수 없다는 것에 있습니다. 전환시 특정 지점으로 이동하도록 지원하지 않기 때문에 사용자와의 상호작용할 수 없습니다. MotionLayout은 이런 모든 문제를 해결 할 수 있습니다. 이를 위해 기존의 2개의 레이아웃을 이용하여 MotionLayout을 통해 자동으로 초기화 하는 방식으로 바꿔보겠습니다. 레이아웃 inflate시 MotionScene은 @xml/scene_01파일을 참조하게 됩니다. scene_01은 전환에 사용할 ConstraintSet 시작(motion_01_cl_start)/종료(motion_01_cl_end) 레이아웃을 지정할 수 있습니다. 또한 핵심인 onSwipe는 전환에 있어 핸들러를 지정했음을 주목해야합니다. OnSwipe OnSwipe 핸들러는 사용자의 액션이 일치되는 경우 전환을 시작합니다. 주요 속성 touchAnchorId 추적해야 할 대상 (@+id/button) touchAnchorSide 손가락을 추적해야하는 물체의 측면 (right/left/top/bottom) dragDirection 추적중인 동작의 방향 (dragRight/dragLeft/dragUp/dragDown) 이제 코드 한 줄 없이 사용자의 스와이프 이벤트를 통해 네모박스가 자연스럽게 애니메이션되면서 끝에서 끝으로 이동 합니다. MotionLayout 자체로 MotionScene 구현 위의 예제 ConstraintSet을 사용하여 2개의 레이아웃으로 전환하였으나 이번에는 MotionLayout으로만 사용하여 빠르게 레이아웃을 수성하도록 해보겠습니다. MotionLayout은 res/xml 디렉토리에 있는 MotionScene 파일에서 ConstraintSet을 직접 설정하는 기능을 지원합니다. 이는 ConstraintSet을 사용하기 위한 다양한 레이아웃 생성을 하지 않아도 되는 장점이 있습니다. 이는 코드와 별도로 동작하기 때문에 나중에 Android Studio에서 모션레이아웃 뷰어를 지원할 가능성이 높습니다. MotionLayout의 속성 alpha visibility elevation rotation, rotation[X/Y] translation[X/ Y/Z] scaleX/Y 첫 번째 예제에서 사용한 ConstraintLayout 대신 MotionLayout으로 레이아웃을 다시 구성했다. 첫 번째 예제에서는 constraintSetStart와 constraintSetEnd에 @layout을 지정했지만, 여기서는 MotionScene 안에 정의한 ConstraintSet의 @id를 지정한다. 기존의 위젯은 Constraint로 모두 대체합니다. 속성 변경을 원하는 위젯만 Constraint를 지정해야 적용가능 합니다. 만약 10개의 위젯이 있는 레이아웃일 경우 2개만 애니메이션 처리하고 싶은 경우 MotionScene의 ConstraintSet은 해당 위젯에 대한 Constraint를 지정하면 됩니다. MotionLayout 속성 app:layoutDescription="reference" 예제에서 본것 처럼 MotionScene xml파일을 지정해야 합니다. app:applyMotionScene="boolean" MotionScene을 적용할지 여부를 지정합니다. app:showPaths="boolean" 동작 경로를 표시합니다. (기본값= false). 릴리즈 빌드시 끄는것을 잊지마세요. app:progress="float" 특정 위치의 진행 장면으로 이동합니다. (0~1) app:currentState="reference" 레이아웃 초기화시 특정 ConstraintSet을 강제로 실행합니다. 이제 한 줄의 코드 없이 사용자 이벤트를 통한 상호작용과 부드러운 애니메이션을 처리를 할 수 있습니다. 위의 모든 예제 코드는 GitHub를 통해 보실 수 있습니다. --- ## Android에서 키보드 전환을 자연스럽게 처리하기 - URL: https://kmshack.kr/blog/smoothkeyboard/ - Published: 2018-10-09 · Updated: 2018-10-09 - Tags: 안드로이드 - Language: ko · Legacy: written against pre-AndroidX APIs Dank 앱은 키보드가 나타나고 사라질 때 콘텐츠 크기와 스크롤을 자연스럽게 조절한다. 기본 동작에서 생기는 갑작스러운 레이아웃 변화를 줄이고 부드럽게 전환하는 원리를 살펴본다. 소프트키보드가 표시되면 안드로이드는 콘텐츠의 크기를 즉각 변경합니다. (windowSoftInputMode를 설정하지 않은경우) 이로 인해 레이아웃에 부자연스러운 변화가 생깁니다. 아쉽게도 플랫폼이나 대다수의 앱이 이를 처리하지 않기 때문에 사용자들은 정상적으로 작동한다는것으로 착각하고 있습니다. 역시 글로만 봐서 이해하기 힘들기때문에 차이점을 명확히 알아보기 위해 Google Keep과 Dank 앱을 영상으로 비교 해보겠습니다. Google Keep Dank 화면을 비교하면 Dank 앱은 키보드가 나타날 때 콘텐츠 크기와 스크롤 위치를 부드럽게 조절한다. 반면 당시 Google Keep은 키보드 전환에 맞춘 별도의 애니메이션이 없어 콘텐츠 영역이 갑자기 바뀌어 보인다. 좀 더 부드러운 화면으로 비교를 원한다면 영상으로 확인해보세요.( Google Keep, Dank) 눈치 빠른 분들은 Dank앱이 처리하는 작은 트릭을 발견하실 수 있습니다. 이 작은 트릭은 키보드가 표시되면 Activity의 전체 화면의 크기가 조정된다는 것에 있습니다. 이는 Activity를 구성하는 View 계층의 최상단 레이아웃(DecorView)에 아무런 영향을 받지 않고 처리 했다는 사실을 알 수 있습니다. 실제로 뷰의 크기가 저장되는 레이아웃ID는 콘텐츠 레이아웃(android.R.id.content)입니다. 이것은 DecorView내의 콘텐츠 레이아웃을 제어할 수 있다는 것을 의미합니다. Activity를 구성하는 View Tree는 일반적으로 다음과 같습니다. DecorView - LinearLayout -- FrameLayout <- android.R.id.content --- LinearLayout ---- Activity content 리사이즈 되는 부분을 제어하기 위해 콘텐츠 레이아웃(android.R.id.content)의 크기가 변경되는 것을 감지하는 유틸 클래스를 만들었습니다. View의 크기가 변경되면 콘텐츠의 전체 높이에서 변경된 높이만큼 변경되도록 애니메이션처리 합니다. val decorView = activity.window.decorView decorView.viewTreeObserver.addOnPreDrawListener { val contentHeight = contentViewFrame.height val sizeChanged = contentHeight != previousHeight if (sizeChanged) { animateSizeChange(from = previousHeight, to = contentHeight) } previousHeight = contentHeight } fun animateSizeChange(from: Int, to: Int) { // Immediately snap back to the original size. contentView.setHeight(from) ObjectAnimator.ofInt(from, to) .addUpdateListener { val h = it.animatedValue as Int contentView.setHeight(h) } .start() } 이와 관련된 모든 코드는 여기를 통해 확인하세요. 콘텐츠 뷰의 크기에 따라 애니메이션처리를 하여 높이를 변경하는 것은 손이 많이 가는일이지만 앱의 품질을 높이는 큰역할을 합니다. 다르게 구현할 수 있는 방법도 많고, 이것이 최상의 솔루션은 아니지만 여러달 테스트하면서 문제점을 발견하지는 못하였습니다. 참고: https://saket.me/smoothly-reacting-to-keyboard/ --- ## ProcessLifecycleOwner로 앱의 Background/Foreground 상태 감지하기 - URL: https://kmshack.kr/blog/ProcessLifecycleOwner%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%95%B1-Background,-Foreground-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EC%B2%98%EB%A6%AC/ - Published: 2018-06-13 · Updated: 2018-06-13 - Tags: 안드로이드, 아키텍처 - Language: ko · Legacy: written against pre-AndroidX APIs Android 앱 전체가 Background 또는 Foreground 상태로 전환되는 시점을 감지해야 할 때가 있다. 각 Activity의 Lifecycle을 따로 추적하는 대신 ProcessLifecycleOwner를 사용해 애플리케이션 수준의 상태 변화를 관찰하는 방법을 살펴본다. Architecture Components를 아직 사용하지 않고 있다면 build.gradle에 아래와 같이 라이브러리를 추가 합니다. dependencies{ kapt "android.arch.lifecycle:compiler:1.1.1" implementation "android.arch.lifecycle:extensions:1.1.1" } Activity(or Fragment)의 Lifecycle 상태를 모니터링할 때 사용하는 LifecycleObserver 인터페이스를 동일하게 하나 구현합니다. class AppLifecycleObserver : LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_START) fun onForeground() { //foreground } @OnLifecycleEvent(Lifecycle.Event.ON_STOP) fun onBackground() { //background } } 구현된 인터페이스를 Activity가 아닌 Application의 onCreate()에 ProcessLifecycleOwner를 이용하여 Observer하도록 합니다. ProcessLifecycleOwner는 내부적으로 Activity의 개수및 Lifecycle 상태를 모니터링 하는 역할을 합니다. class AppApplication : Application() { override fun onCreate() { super.onCreate() ProcessLifecycleOwner.get().lifecycle .addObserver(AppLifecycleObserver()) } } ON_START는 앱이 Foreground로 진입했음을, ON_STOP은 Background로 전환되었음을 의미한다. ProcessLifecycleOwner는 여러 Activity의 Lifecycle을 종합해 이벤트를 전달하므로 밀리초 단위의 즉각적인 상태 감지가 필요한 용도에는 적합하지 않을 수 있다. ProcessLifecycleOwner에 대한 더 많은 정보는 안드로이드 개발문서를 참고 해주세요. --- ## Kotlin Coroutines로 Retrofit2 요청 동시에 처리하기 - URL: https://kmshack.kr/blog/Kotlin-Coroutines-Retrofit2-+-Coroutines-%EB%8F%99%EC%8B%9C%EC%B2%98%EB%A6%AC/ - Published: 2018-05-12 · Updated: 2018-05-12 - Tags: 안드로이드, 코틀린 - Language: ko · Legacy: written against pre-AndroidX APIs Android의 UI 스레드에서 네트워크 요청, 데이터베이스 쿼리나 무거운 연산을 수행하면 화면이 멈추고 ANR이 발생할 수 있다. Kotlin Coroutines를 사용해 여러 Retrofit2 요청을 동시에 처리하고 결과를 조합하는 방법을 살펴본다. 우리는 이런 문제를 해결 하기 위해 아래와 같은 몇 가지 방식을 사용합니다. AsyncTask – 순서대로 짧은 연산 실행, 동시에 수행할 수 있음 Executors – 고정된 스레드 풀과 동시에 작업 실행 Intent Service – 순서대로 긴 연산을 실행, 큐잉 처리함 RxJava – 가장 인기 있으며, 안드로이드 프레임워크에서 지원하지 않음 Raw Threads 위의 방식은 훌륭하지만 올바르게 사용하기 위해선 미묘한 차이를 보입니다. 또, 디버깅과 취소처리가 간단하지 않습니다. Kotlin Coroutines 코틀린 코루틴는 순차적으로 일어나는 일을 비동기 처리하기 위한 방법입니다. 코루틴을 생성하는 것은 스레드를 만드는 것에 비해 비용이 적게듭니다. 그 이유는 코루틴은 컴파일러의 코드 변환으로 구현되므로 VM이나 OS의 별도 지원이 필요하지 않다. suspend 함수도 이 변환을 통해 동작한다. 코루틴은 여전히 실험단계에 있으며, 이는 API가 앞으로 변화될 가능성을 의미합니다. 하지만 JetBrains은 이전 버전과 같은 호환성을 제공할것 이라고 약속했습니다. (여기를 통해 확인 해보세요.) Code 코루틴을 사용하기 위해 함수를 suspend로 표시해야 모든 정상기능을 사용할 수 있습니다. 이러한 기능을 사용하기 위해 실행및 비동기화(Launch & Async)하는 코루틴 빌더가 필요합니다. Launch & Async Launch – 현재 스레드를 차단하지 않고 새로운 코루틴을 실행하고 코루틴을 제거하는데 사용할 수 있는 작업으로 코루틴에 대한 참조를 반환합니다. Async – 새로운 코루틴을 실행하고 지연후 결과를 반환합니다. public actual fun launch( context: CoroutineContext = DefaultDispatcher, start: CoroutineStart = CoroutineStart.DEFAULT, parent: Job? = null, block: suspend CoroutineScope.() -> Unit ): Job { val newContext = newCoroutineContext(context, parent) val coroutine = if (start.isLazy) LazyStandaloneCoroutine(newContext, block) else StandaloneCoroutine(newContext, active = true) coroutine.start(start, coroutine, block) return coroutine } public actual fun async( context: CoroutineContext = DefaultDispatcher, start: CoroutineStart = CoroutineStart.DEFAULT, parent: Job? = null, block: suspend CoroutineScope.() -> T ): Deferred { val newContext = newCoroutineContext(context, parent) val coroutine = if (start.isLazy) LazyDeferredCoroutine(newContext, block) else DeferredCoroutine(newContext, active = true) coroutine.start(start, coroutine, block) return coroutine } CoroutineContext – 코루틴 빌더가 실행되는 스레드를 정의합니다. DefaultDispatcher – CommonPool을 사용합니다. 개발자는 실행해야하는 스레드를 지정할 수 있는 모든 권한을가집니다. 다른 옵션은 UI 스레드에서 실행되는 UI입니다. CoroutineStart – 시작 방법 정의 DEFAULT - 즉시 실행을 시작 LAZY - 코루틴을 느리게 시작 ATOMIC - 자동으로 최적화된 방법으로 시작 UNDISPATCHED - 분산 처리방법으로 시작 예제 연속적으로 처리하는 작업 private lateinit var job1: Job suspend fun getThingsDone(myThings: Int): Int { // Heavy computation going on here :D return myThings + 99 } suspend fun getThingsDoneAgain(myThings: Int): Int { // Heavy computation going on here :D return myThings + 50 } fun toDoWorkSerially() { job1 = launch(UI) { try { val work1 = async { getThingsDone(23) } val work2 = async { getThingsDoneAgain(55) } val result1 = work1.await() tvResult1.text = result1.toString() val result2 = work2.await() tvResult2.text = result2.toString() } catch (exception: Exception) { exception.printStackTrace() } } } UI (Main Thread)는 Context를 사용하여 새로운 코루틴 Launch를 만듭니다. 이 과정에서 백그라운드에서 무거운 작업을 수행하고 UI 스레드에서 두 개의 다른 코루틴을 실행합니다. Async는 새로운 코루틴을 생성하고 지연된것을 리턴하여 기다릴 수 있게 됩니다. work1과 work2는 순서대로 실행된다. result1은 work1이 끝날 때까지 기다리고, 이어서 result2는 work2가 끝날 때까지 기다린다. 두 작업은 CommonPool을 Context로 사용하므로 백그라운드 스레드에서 실행되며, 예외는 try/catch 블록에서 처리한다. 동시에 처리 하는 작업 private lateinit var job2: Job fun toDoWorkConcurrent() { job2 = launch { try { val work1 = async { getThingsDone(43) } val work2 = async { getThingsDoneAgain(123) } val result = computeResult(work1.await(), work2.await()) withContext(UI) { tvResult1.text = result.toString() } } catch (exception: Exception) { exception.printStackTrace() } } } private fun computeResult(await: Int, await1: Int): Int { return await + await1 } 두 작업의 결과는 work1과 work2가 모두 끝난 뒤 사용할 수 있다. await()를 호출하면 각 Deferred의 완료를 기다린다. 코루틴 취소는 Job에서 cancel()을 통해 간단히 호출할 수 있습니다. Retrofit2 + Coroutines 코루틴을 Retrofit2와 함께 사용하기 위해서는 Retrofit2 Kotlin Coroutines Adapter 라이브러리를 이용하여 Service 메서드의 반환 유형으로 Deferred 타입을 사용할 수 있습니다. class LoginRepository @Inject constructor(private val apiService: APIService, private val sharedPreferences: SharedPreferences) { fun getOTPForNumber(phoneNumber: String): LiveData { val loginResult = MutableLiveData() launch { try { val request = apiService.getOTPForNumber(phoneNumber) val response = request.await() if (response.isSuccessful) { loginResult.postValue(OTPResult.Success(response.message())) } else { loginResult.postValue(OTPResult.Error(response.code(), response.message())) } } catch (exception: Exception) { loginResult.postValue(OTPResult.Exception(exception)) } } return loginResult } fun getMyThings(date: String, meal: Triple): LiveData { val dispatchResult = MutableLiveData() launch { try { val task1 = apiService.getDispatches(id, date, meal.first) val task2 = apiService.getDispatches(id, date, meal.second) val task3 = apiService.checkAppDetails() checkResult(task1.await(), task2.await(), task3.await(), dispatchResult) } catch (exception: Exception) { dispatchResult.postValue(DispatchesResult.Exception(exception)) } } return dispatchResult } private fun checkResult() { if (task1.isSuccessful && task2.isSuccessful && (task3.isSuccessful && task3.body()?.code == 1000)) { } } } apiService 인터페이스가 기존 Call대신 Deferred를 반환하기 때문에 이를 통해 이전에 다룬 코루틴을 그대로 사용할 수 있습니다. 결론 코루틴을 사용하면 코드를 쉽게 읽을 수 있는 비동기 코드 작성이 가능하며, 오류및 유지 보수를 줄일 수 있습니다. 또한 코드의 경량화는 말할것도 없습니다. --- ## Fragment 전환 애니메이션 쉽게 적용하기 - URL: https://kmshack.kr/blog/Fragment-%ED%8A%B8%EB%9E%9C%EC%A7%80%EC%85%98-%EC%89%BD%EA%B2%8C-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0/ - Published: 2018-03-19 · Updated: 2018-03-19 - Tags: 안드로이드 - Language: ko · Legacy: written against pre-AndroidX APIs Fragment 전환에 공유 요소 애니메이션을 적용하는 방법을 살펴본다. 기본적인 sharedElementViews 구성부터 Chris Banes의 ColumnedChangeBounds를 활용한 확장 효과까지 다룬다. 일반적으로 Fragment 전환 시 아무런 전환 효과를 주지 않습니다. commit과 동시에 콘텐츠 화면이 심심하게 바뀝니다. Fragment도 Activity에서처럼 전환 효과를 주기위해서는 sharedElementViews를 이용하여 전환하고 싶은 View를 추가하면 됩니다. 그리고 트랜잭션 최적화 작업을 위해 setReorderingAllowed() 속성을 허용합니다. supportFragmentManager.beginTransaction() .setReorderingAllowed(true) .replace(R.id.container, GridFragment.newInstance(count)) .addToBackStack("detail") .apply { val topFragment: Int = supportFragmentManager.fragments.size val fragment: GridFragment = supportFragmentManager.fragments[topFragment - 1] as GridFragment val recyclerView: RecyclerView = fragment.recyclerview var viewCount = 0 for (itemView in recyclerView.children) { addSharedElement(itemView, viewCount.toString()) viewCount++ } } .commit() 위의 코드에서 addSharedElement를 통해 현재 보여지고 있는 View에 각각 전환될 이름을 부여 해줍니다. 그럼 Fragment가 전환된 View에 해당 전환된 이름의 View를 찾아 트랜지션 됩니다. 문제점 새로운 Fragment가 생성되면 데이터를 가져오거나 이미지를 불러오고 레이아웃을 구성하고 그리는 시간이 걸리기 때문에 전환 효과는 자연스럽지 않습니다. 이런 구성을 하는 동안 애니메이션이 끝나게 되는 경우 아무런 효과를 보지 못할 수 있습니다. 그렇기 때문에 장면 효과를 데이터나 뷰를 구성하기 전까지 지연할 필요가 있습니다. postponeEnterTransition(), startPostponedEnterTransition()을 이용하면 장면 효과를 지연하고 시작할 수 있습니다. 우리는 뷰가 그려진다는 사실을 OnPreDrawListener를 통해 인지 할 수 있습니다. 최근에 지원된 코틀린 라이브러리 KTX를 이용한다면 OnPreDrawListener 대신 doOnPreDraw를 통해 쉽게 콜백을 받을 수 있습니다. postponeEnterTransition() recyclerView.doOnPreDraw { startPostponedEnterTransition() 이제 Fragment 전환 효과를 위한 준비는 모두 끝났습니다. 어떻게 트랜지션이 이루어 질지에 대한 정의만 해주면 되는데 기본적으로 몇 가지를 지원하는데 Bounds가 변경되기 때문에 ChangeBounds() 트랜지션을 설정 하도록 하겠습니다. override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) sharedElementEnterTransition = ChangeBounds() } 좀 더 멋진 효과를 보기 위해는 Chris Banes과 같이 ColumnedChangeBounds를 직접 구현하면 됩니다. 이 글에서 구현된 모든 코드는 https://github.com/kmshack/FragmentTransitions에 공개됩니다. --- ## Support Library 28.0.0 alpha1의 BottomAppBar 살펴보기 - URL: https://kmshack.kr/blog/Support-Library-28.0.0-alpha1%EC%97%90-%EC%B6%94%EA%B0%80%EB%90%9C-BottomAppBar/ - Published: 2018-03-16 · Updated: 2018-03-16 - Tags: 안드로이드 - Language: ko · Legacy: written against pre-AndroidX APIs Android P 프리뷰와 함께 Design Support Library 28.0.0 alpha1이 공개되었다. 이 버전에 처음 포함된 BottomAppBar의 주요 속성과 FloatingActionButton을 배치하는 방법을 살펴본다. com.android.support:design:28.0.0-alpha1 이 버전에는 MaterialButton, MaterialCardView, Chip과 BottomAppBar가 포함되어 있다. 화면 아래쪽에 주요 액션을 배치하면 큰 화면에서도 엄지손가락으로 접근하기 쉽다. FloatingActionButton의 app:fabCradleVerticalOffset 속성으로 세로 오프셋을 조절할 수 있다. app:fabAlignmentMode를 사용하면 위치를 CENTER 또는 END로 바꿀 수 있으며, 런타임에 값을 변경하면 전환 애니메이션이 적용된다. CoordinatorLayout에 BottomAppBar와 FloatingActionButton을 배치하고, FloatingActionButton의 layout_anchor 속성에 BottomAppBar의 ID를 지정하면 된다. 몇 가지 주의 사항 Toolbar를 확장하여 BottomAppBar를 구현했지만 setSupportActionBar()를 호출하면 중단됩니다. 배경식을 변경하기 위해 android:background대신 app:backgroundTint를 호출 해야 합니다. setTitle, setSubTitle은 재정의되고 비어있기 때문에 아무런 작동을 하지 않습니다. Android P 프리뷰와 함께 공개된 초기 알파 버전이므로 API와 디자인은 이후 변경될 수 있다. 큰 화면에서 주요 액션의 접근성을 높이려는 방향을 확인할 수 있다. 그리고 알파 버전에 들어 있는 추가 레이아웃에 대해 간단하게 언급 하자면.. Chip, ChipGroup 키워드나 태그를 하나씩 보여주는 태그뷰이며 그룹으로 관리가 가능하며 다양한 스타일(Action, Filfer, Choice)를 기본으로 제공합니다. ingleSelection속성을 이용하면 그룹중에 하나만 선택 할 수 있는 기능도 지원합니다. MaterialCardView CardView의 확장버전이며 strokeColor와 strokeWidth 속성이 추가되었습니다. MaterialButton 기본 Button에서 cornerRadius를 지원하여 라운드 처리가 가능하며 아이콘을 추가 할 수 있는 속성이 추가되었습니다. --- ## ConstraintLayout으로 전환 애니메이션 만들기 - URL: https://kmshack.kr/blog/ConstraintLayout%EC%9C%BC%EB%A1%9C-%EC%95%84%EB%A6%84%EB%8B%A4%EC%9A%B4-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98%ED%95%98%EA%B8%B0/ - Published: 2017-06-30 · Updated: 2017-06-30 - Tags: 안드로이드 - Language: ko · Legacy: written against pre-AndroidX APIs ConstraintLayout은 복잡한 뷰 계층을 줄이는 데 유용할 뿐 아니라, 서로 다른 Constraint 집합 사이를 전환하는 애니메이션에도 활용할 수 있다. ConstraintSet과 TransitionManager를 조합해 적은 코드로 레이아웃 변화를 애니메이션으로 표현하는 방법을 살펴본다. 방법 ConstraintLayout의 기본 사항을 알고 있다고 가정합니다 (예: app:layout_constraintLeft_toLeftOf 및 다른 속성). 대부분의 문서 또는 사이트에서는 새로 개선 된 Android Studio 레이아웃 디자인 패널을 사용하여 다양한 Constraint를 드래그/드롭/시각화 하는 방법만 소개되어 있습니다. 애니메이션의 목적을 위해서는 Constraint를 정확하게 이해하고 있어야만 조작이 가능합니다. 가장 간단한 형식인 TransitionManager(API 19 이상 또는 서포트 라이브러리에서 사용 가능)를 통해 두 가지 Constraint 집합 간에 애니메이션을 적용할 수 있습니다. 긴 설명 보다 간단한 예제를 살펴 보겠습니다. 예제 Activity 실행시 초기화되는 XML 레이아웃부터 살펴 보겠습니다.