한국의 개발자들을 위한 Google for Developers 국문 블로그입니다.
Google I/O를 위한 Google 어시스턴트 액션 제작기 (2)
2019년 3월 28일 목요일
Google I/O를 위한 Google 어시스턴트 액션 제작기 (2)
요약 정리
Dialogflow
여기
서 Dialogflow 에이전트를 살펴보세요 (디렉토리를 압축해 Dialogflow에서 임포트 할 수 있습니다).
찾아보기 흐름과 같이 하위 메뉴에 대한 후속 인텐트(Followup Intent)와 컨텍스트를 사용하세요. 자세한 내용은
여기
를 참조하세요.
이 작업을 수행할 때 컨텍스트가 뒤섞여 예기치 않은 동작이 발생하지 않도록 주의하세요. 안전을 보장하려면 하위 메뉴로 들어갈 때 관계없는 컨텍스트는 비활성화하세요.
Dialogflow 에이전트의 압축을 해제한 후 버전 관리 시스템에 푸시합니다. 이를 통해 코드 검토와 책임 문제를 개선할 수 있습니다.
웹훅(Webhook)
프롬프트용으로 재사용 가능한 데이터 구조를 만들고 인텐트 기반으로 이를 구조화 합니다 (예:
예제 디렉토리
, 인텐트를
라우팅
한 방식).
Cloud Functions를 사용할 때는
전역 범위 캐싱
에 대해 충분히 파악해두세요. 이 기능을 원할 때는 더없이 유용하지만 그렇지 않으면 고통의 원인이 될 수도 있기 때문입니다.
액션은 다른 소프트웨어와 유사합니다. 꿈은 크게 가지시더라도 작게 시작하고 계속 반복하세요.
이 게시물의
1부
에서는 Google I/O ’18 액션의 설계 과정을 다루었습니다. 1부에는 사용 사례에 대한 브레인스토밍 그리고 샘플 대화 및 상위 레벨 흐름을 만들어내는 페르소나에 대한 내용이 포함되었습니다.
남
은 문제는 Dialogflow에서 이를 구현하는 방법입니다.
Dialogflow 에이전트 구현
과제는 전체 대화 설계(즉, 서로 주고받을 가능성이 있는 모든 대화와 이런 대화 사이의 전환)를 Dialogflow 에이전트로 구현하는 문제였습니다. 이 시점에는 대화가 가장 일반적이고 공통된 경로에서 벗어날 수 있는
수없이 많은 방식
을 포괄할 뿐 아니라
오류
그리고 발생 가능성이 거의 없거나 흔치 않은 다른 상황을 처리하는 문제까지 포함하는 자세한 설계 사양이 있었습니다. 이를 위해 간단한 흐름도 이상의 것이 필요했습니다. 흐름도에서 화살표가 모든 경로를 반영할 필요는 없습니다. 흐름도는 특정 대화 인스턴스에서 자주 발생하는 경로를 나타냅니다. 해당 대화에서 말할 차례는 특정 순서에 따라 진행되겠지만 사용자마다 대화할 때 따라가는 경로가 제각기 다를 수 있고, 이는 흐름도에는 표시되지 않습니다. 게다가 컨텍스트가 여러 개의 대화 경로를 생성하는 방식이 흐름도에 항상 명확히 드러나는 것은 아닙니다.
이 사양을 Dialogflow에 포팅할 때는 Dialogflow '
인텐트
'가 실제로는 무엇인지 잘 생각해보는 것이 중요합니다. 인텐트는 사용자가 트리거한 ‘특정 상태’입니다. 사용자의 입력이
훈련 문구와 매치
되었고 에이전트에서 응답을 반환해야합니다. 각각의 인텐트는 한 번의 대화 차례를 나타냅니다. Dialogflow 에이전트에 있는 인텐트의 모음이 유한 상태 기계(Finite State Machine)를 형성하는데, 여기서 몇몇 상태의 모음이 결합하여 완전한 대화를 형성합니다. 특정 인텐트에서 이어질 수 있는 가능한 후속 인텐트는 이전 인텐트에서 활성화된 컨텍스트가
입력 컨텍스트
로 지정되어 있거나, 아니면 아예 입력 컨텍스트가 없는 인텐트입니다. 가능한 상태 전환 방식 중에서, Dialogflow는 사용자의 입력이특정 임계값 이상으로 훈련 문구와 매치되는 전환을 고릅니다.
인텐트 식별
그렇다면, 이 모든게 어떤 의미였을까요? 먼저, 전체 대화 흐름을 상위 레벨의 인텐트 또는 중첩된(즉, 후속) 인텐트로 분류하는 걸 의미했습니다. 상위 레벨 인텐트는 대화의 어느 시점에서든 매치될 수 있습니다. 예를 들어 사용자는 대화 중 아무 시점에서나 특정 세션 장소로 가능 방법을 물어볼 수 있습니다. 세션 장소까지 가는 방향을 제시하기 위해 추가로 필요한 입력 컨텍스트가 없습니다. 대부분의 인텐트가 상위 레벨의 인텐트일 수 있고, 사용자는 좀 더 유연한 방법으로 에이전트와 대화할 수 있습니다.
큰 체계 내에서 다양한 하위 대화에 대해
후속 인텐트
가 필요했습니다. 더 자세히 들어보고 싶은 세션 주제를 찾는 사용 사례를 예로 들어보겠습니다. 이 상호 작용은 사용자가 다음과 같은 내용을 말하기를 기다리는 찾아보기-주제(browse-topics) 인텐트로 시작됩니다.
“세션을 찾아줘”
“주제”
“올해 I/O에선 어떤 강좌가 열리지?”
이런 문구가 나올 경우 우리는 사용자가 결국은 같은 기본적인 목표(인텐트)를 표현하는 것으로 간주합니다. 즉, 사용자는 이벤트에서 열리는 세션의 범주를 알고 싶어합니다. 하지만 사용자에게 이 목록을 제공하면 오히려 더 복잡해질 뿐입니다. 스피커 기기에서는 사용자가 질려버리지 않도록 하기 위해 주제 목록을 몇 부분으로 나누어서 제시합니다. 이 때문에, 우리는 사용자가 다음 옵션 집합을 원한다는 의사를 밝히도록 하기 위한 인텐트가 필요했습니다. 사용자는 또한 설계상 필요에 따라 주어지는 옵션 집합을 반복해달라고 요청할 방법이 필요합니다. 이런 상황에서는 찾아보기-주제 인텐트에 대해 적어도 두 개 이상의 후속 인텐트가 필요합니다(다음 옵션 집합을 요청하기 위한 인텐트와 현재 옵션 집합을 반복하기 위한 인텐트).
찾아보기 흐름 만들기
액션에 여러 개의 찾아보기 흐름이 있는 경우(예: 주제 찾아보기, 한 주제 내에서 세션 찾아보기 등), 한 가지 해결책은 이러한 흐름이 공유할 상위 레벨의 '다음 옵션' 인텐트를 만드는 것이었을 겁니다. 우리는 다음과 같은 몇 가지 이유로 이에 반하는 결정을 내렸습니다. 첫째, 이러한 흐름을 여러 개의 후속 인텐트로 나누면 더욱 개념적으로 구분됩니다. 둘째, '다음 옵션' 인텐트 하나를 처리하려면 찾아보기 흐름 내에서 올바른 '다음' 옵션 집합을 가져오기 위해 Dialogflow에서 특수한 컨텍스트의 사용과 추가적인 로직이 필요했을 것입니다. 그 대신, 우리는 각각의 찾아보기 흐름에 필요한 후속 인텐트를 복제했고, 그 결과 다음과 같은 인텐트 집합을 생성했습니다.
컨텍스트 충돌 처리
후속 인텐트(Followup Intent)는 어떻게 작동할까요? 후속 인텐트는
컨텍스트
를 통해 인텐트 사이의 방향 관계를 정의합니다. 후속 인텐트는 Dialogflow가 해당 인텐트와 매치하기 위해 활성화해야 하는 컨텍스트(입력 컨텍스트)를 정의합니다. 상위 레벨 인텐트는 매치되었을 때 이 동일한 컨텍스트(출력 컨텍스트)를 활성화합니다. 우리가 다룬 사례에서는 찾아보기-주제-다음(browse-topics-next) 인텐트와 찾아보기-주제-반복(browse-topics-repeat) 인텐트가 매치되도록 활성 상태가 되려면 찾아보기-주제-후속 컨텍스트(browse-topics-followup context)가 필요합니다. 찾아보기-주제 인텐트는 사용자가 세션에서 다루는 주제를 찾아보고 싶다고 할 때 이 컨텍스트를 활성화합니다.
이 때 주의깊게 살펴 볼 부분이 있습니다. 다른 찾아보기 컨텍스트를 삭제한 부분입니다(컨텍스트의 수명을 0으로 설정). 이는 각각의 상위 레벨 찾아보기 인텐트에 사용되는 패턴입니다(찾아보기-세션 및 표시-예약). 이건 왜 필요한 걸까요? 이런 것이 없는 상태에서 사용자가 다음과 같은 대화에 참여한다고 상상해 봅시다(의역된 대화).
다음으로 무슨 일이 일어날까요? 다른 찾아보기 흐름의 컨텍스트를 지우는 각각의 상위 레벨 인텐트가 없다면 사용자의 마지막 쿼리가 찾아보기-세션-다음 인텐트와 매치되리란 보장이 없습니다. 대신에, 사용자의 마지막 쿼리가 찾아보기-주제-다음 인텐트와 매치되어 사용자가 이미 주제를 선택한 후에도 사용자에게 다음 주제 집합을 제시하는 일이 벌어질 수 있습니다.
이는 찾아보기-주제-후속 컨텍스트와 찾아보기-세션-후속 컨텍스트가 둘 다 활성 상태이기 때문입니다. 각 상위 레벨 찾아보기 인텐트에서 다른 찾아보기 흐름의 후속 컨텍스트를 지우는 방법으로 이를 방지합니다.
에이전트를 개발하면서 상위 레벨 인텐트를 확장하는 작업부터 시작했습니다. 계속 추적할 컨텍스트가 없으므로 가장 쉽게 만들고 테스트할 수 있는 방법입니다.
에이전트 버전 관리 및 협업 환경 구축
처음부터 이 에이전트를 개발하려면 방대한 규모의 협업이 필요할 것이라고 예상할 수 있었습니다. 그래서 백엔드 로직 외에도 에이전트도 버전 관리 대상으로 포함하기로 판단했습니다. 더욱이, 에이전트는 압축 해제된 에이전트 디렉토리로 Git 저장소에 상주시키는 것으로 결정했습니다. 그 덕분에 코드 검토를 통해 새로 추가되는 인텐트나 컨텍스트에서 문제를 포착할 수 있었습니다. 이는 에이전트 내부에서 이루어지는 작업에 대한 지식을 공유하고 문제를 미연에 방지하는 데 중요한 단계였습니다.
웹훅 구현
물론, 웹훅으로 이 전체 에이전트를 구동했습니다. 다른 모든
샘플
에서 그렇게 하듯이,
Firebase용 Cloud 함수
를 사용하기로 했습니다. 이 기능을 사용하면 구현하려는 서비스를 위해 매우 비용 효율적이고 간단한 웹훅을 구현할 수 있습니다. 수요가 극단적으로 몰리는 시간에 서비스를 확장하거나 트래픽이 잠잠한 시간에 유휴 시간에 대한 요금을 지불해야 할 걱정을 할 필요가 없습니다. 또한
Firestore
,
Auth
등 다른 Firebase 서비스와 최고 수준의 통합도 가능합니다.
대화 구성
로직에서 먼저 해결해야 할 문제 중 하나는 현재 시간을 식별할 방법을 찾는 것이었습니다. 현재 날짜에 따라(이벤트 이전, 도중 또는 이후) 사용자에게 약간 다른 대화 흐름이 제시되었습니다. 이를 해결하기 위해 최초
커밋
중 하나에서
Actions on Google Node.js Client Library
미들웨어
기능을 사용하여 매번 대화 차례가 바뀌기 전에 현재 사용자 '문구'를 미리 처리했습니다. 주어진 인텐트를 처리할 때 이 문구의 문자열 값('pre', 'during' 또는 'post')이 전역
conv
객체에 첨부되었습니다. 그래서 그 값을 기준으로 응답 대화를 조건부로 선택할 수 있었습니다.
하지만 주어진 문구에 대해 어떻게 적절한 응답을 선택할까요? 해당 문구를 매핑하기 위해 각 인텐트 내에서 거대한 'if/else'를 사용하는 건 확실히 현명하지 못한 일이었습니다. 게다가, 해당 문구가 어떤 인텐트로 매핑될지 기준이 되는 유일한 조건도 아니었습니다. 다음과 같은 다른 조건이 포함되었습니다.
기존 사용자의 대화인지 신규 사용자의 대화인지 여부
화면을 사용 중인지, 스피커 기기를 사용 중인지 여부
이들 각 조건에 대해 다음을 선택해야 했고 때로는 무작위로 선택해야 했습니다.
응답의
Simple Response
/
Basic Card
/
List
요소
응답 뒤에 표시되는
Suggestion
칩
다음에 이어지는 사용자 음성이 Dialogflow 인텐트와 매치되지 않을 경우 사용할 대체 응답
사용자가 스피커 기기에 침묵으로 반응할 경우에 사용할 무입력 응답
이 모든 변동 사항을 고려하기 위해서는 응답 대화를 제시하기 위한 유연한 전용 데이터 구조가 필요하다는 사실이 분명했습니다. 소스 코드의
prompts
디렉토리에서 액션의 각 주요 사용 사례(최상위 레벨 질문과 각각의 찾아보기 흐름)에 대해 표시되는 이 데이터 구조를 찾을 수 있습니다. 데이터 구조는 다음과 같이 구성되었습니다.
모든 응답은 prompts 디렉토리에 저장했고 공통 파싱 로직은 단일 자바스크립트
파일
에 넣어두고 사용했습니다.
로직과 응답 문자열 데이터를 구분한 후, 다음 3가지 주요 흐름에 걸쳐 로직을 분할했습니다.
대부분 최상위 레벨 Dialogflow 인텐트에 상응하는 '정적' 질문
찾아보기를 위한 메뉴(주제 또는 주제 내 세션)
일정 액세스 및 찾아보기
이들은 각각 prompts 디렉토리에서 별개의 하위 디렉토리가 되었는데, 서로에 대해 고유한 애플리케이션 로직을 포함한 utils.js 파일이 들어 있습니다. 루트 app.js 파일의 드라이버 로직은 인텐트 처리를 이러한 utils.js 파일 중 하나로 라우팅합니다.
컨퍼런스 데이터 가져오기: Cloud Function 캐싱에 대한 강좌
찾아보기 흐름 중 하나를 처리하기 위해, 컨퍼런스 데이터를 가져오기 위한 전용 모듈이 필요했습니다. 이벤트의 컨퍼런스 데이터는 클라우드에 호스팅된 JSON 파일에 저장됩니다. 그럼, 이를 가장 수월하게 처리하는 방법은 무엇일까요? 한 가지 옵션은 절전 모드 해제 시 JSON 데이터를 Cloud Function으로 가져와서 전역 범위 변수에 저장하고 미리 처리한 다음,
이후에 호출
할 때 해당 데이터에 액세스하는 것이었습니다. 여기서 주요 문제는 JSON 파일의 콘텐츠가 꽤 많이 변경되고(개발 중 세션 추가, 이벤트 중 라이브스트림 링크 추가 등), Cloud Function이 절전모드 해제 후 상당한 시간 동안 해제 상태로 유지될 수 있다는 점입니다. 그래서 실시간 솔루션이 필요했습니다.
이 문제를 해결하기 위해 취한 첫 번째 접근 방법은 JSON 데이터를 가져와서 미리 처리하고 반환하는 전역 비동기 함수(Promise)를 만든 다음, 인텐트 처리 로직에서 이 함수를 호출하는 것이었습니다. 즉, 이 함수를 재사용 가능한 함수로 선언하여 모든 대화에서 새로운 데이터를 끌어올 수 있도록 하자는 아이디어였습니다. 여기서 흥미로운 문제는 전역적으로 선언한 함수가 절전 모드 해제 시 실행되었고, 결정된 값이 다음 호출을 위해 Promise로서 캐시된다는 점이었습니다. 이게 왜 문제냐고요? 이 문제로 인해 테스트 중에 새 데이터가 표시되지 않는 이유에 관해 많은 혼동이 있었기 때문입니다.
그래서 생각해낸 해결책은 대화의 각 차례에 대해
새로 인스턴스화
되는,
ConferenceData
라는 클래스를 만드는 방법이었습니다. 이 클래스는 처음 필요할 때 해당 데이터를 가져와서 미리 처리하고(정리, 중복 제거 등 포함), 해당 함수 실행의 나머지 부분을 위해 이를 캐시합니다. 이런 방법으로 항상 새로운 데이터를 보장했고, 단일 함수 실행으로 여러 차례 데이터에 액세스하는 경우에 사용자에 대한 응답 시간을 훨씬 더 단축할 수 있었습니다.
여기서 예리한 관찰자라면 아마 뭔가 잘못된 점을 알아차릴 수도 있을 것입니다. 이 해결책이 의미하는 바는, 어쩌면 대화 차례가 바뀔 때마다 컨퍼런스 데이터를 가져와서 미리 처리하게 된다는 사실이었습니다. 이러한 작업의 상대적 비용을 생각해 볼 때 이상적인 해결책은 아닙니다. 한 가지 쉽게 떠올릴 수 있는 수정 방법은 대화를 시작할 때마다 JSON 데이터를
conv.data
에 저장하는 것이겠지만, 이 문자열 데이터의 크기를 고려할 때 합당한 방법은 아니었습니다.
그래서 숙고 끝에 최종적인 해결책으로 내놨지만 시간 제약 조건 때문에 포기해야 했던 방법은 컨퍼런스 데이터를 주기적으로 가져와서 미리 처리하고 캐시하는 백엔드 시스템을 빌드하는 것이었습니다. 이 방법을 쓰면 애플리케이션 로직을 파싱 로직과 분리함으로써 실시간에 가까운 데이터 정확도를 제공하고 지연 시간을 대폭 줄일 수 있었을 것입니다. 필자는 향후 프로젝트에서 이 문제의 우선순위를 훨씬 더 높이 설정할 생각입니다. 사용자 환경에 직접적인 영향을 주는 문제이기 때문이죠. 우리가 다루는 사례에서는 실제로 효과를 발휘한 방법이 있었습니다.
범위 제한
자, 이제 다른 무슨 문제가 남았을까요? 어떤 소프트웨어 프로젝트에서나 그러하듯이, 우리는 온갖 종류의 제한 사항에 맞닥뜨렸고 이로 인해 범위가 제한되었습니다. 예를 들어 시간 문제로 범위를 줄인 한 가지 기능은 사용자가 특정 옵션 집합이 흥미를 돋우는지 말할 수 있도록 하는 기능이었습니다. 초기 설계 요구 사항에는 다음과 같은 대화가 있었습니다.
사용자: “Android 강좌엔 뭐가 있는지 전부 알려줄래?”Action: “먼저 Android 1번 강좌가 진행된 후에 Android 2번 강좌가 진행됩니다. 둘 중에서 흥미를 끄는 강좌가 있으세요?”사용자: “응”Action: “그래요? 어떤 거죠?”사용자: “...에 대해 들어보자”
그리고 이런 대화도 있었습니다.
사용자: “Cloud 강좌엔 뭐가 있는지 전부 알려줄래?”Action: “Cloud에 관한 강좌는 하나밖에 없군요. 들어보실래요?”사용자: “응”Action: “좋아요, 그건...”
두 가지 이상의 옵션을 제시하는 경우 사용자는 주제를 선택하거나 위와 같은 옵션에 만족하는지 여부를 나타낼 수 있습니다. 한 가지 옵션만 제시하는 경우 사용자는 그 옵션에 대해 듣고 싶은지 간단히 확인해 줄 수 있습니다. 하지만 우린 결국 이 흐름을 지원하지 않기로 했습니다. 왜냐고요? 사용자가 "응"과 같이 긍정의 답을 하면 충돌이 생기기 때문입니다. 사용자는 주어진 단일 세션에 대해 듣고 싶을 때나 두 가지 옵션 중에서 고를지 물을 때 모두 "응"이라 답할 수 있을 겁니다. 이 때문에 "응", "그래" 등과 같은 문구로 훈련한 단일 Dialogflow 인텐트를 만들고 마지막으로 제시되는 항목 집합의 크기를 기준으로 어떻게 응답할지 결정하는 애플리케이션 로직을 빌드해야 했을 것입니다.
이런 종류의 기능은 엔지니어링 시간을 늘리고 버그가 발생할 가능성도 있으므로, 더 완전한 액션을 만들기 위해 제거해야 했습니다. 따라서 특히 엔지니어링 시간과 잠재적 UX 결과의 균형을 맞추는 데 있어 우선순위를 정하고 추정을 할 필요가 있었습니다.
QA 및 출시
초기 개발 기간이 지난 후, 우리는 액션에 대한 기본적인 QA 테스트를 실시하기 시작했습니다. 여기에는 포괄적인 대화 집합을 실행해나가면서 사용자 경험이 타당한지 확인하는 작업이 수반되었습니다. 사용자 경험이 타당한 수준일 때 예/아니요 질문을 묻고 있는 것인가? 음성의 SSML 제공이 적당한가? 말하는 속도가 너무 빠르거나 느리지는 않은가? 이 시점에서 이루어진 변경 사항은 대부분 응답 내용을 적절히 조정하는 내용이었습니다.
또한 이 시간을
암시적 호출 인텐트
를 추가하는 데도 활용했는데, 이를 통해 이벤트에 대해 더 자세히 알아보려는 Google 어시스턴트 사용자를 위해 인텐트의 검색 능력을 강화하기 위한 조치였습니다.
버그 발생 상황
이 시기는 개발 과정에서는 발견하지 못했던 버그를 다수 포착한 중요한 때이기도 했습니다. 이때 잡아낸 버그 중 하나는 응답 데이터 구조 형식을 따르지 못하는 문제였는데, 이로 인해 파싱 로직이 장애를 일으켰습니다. 이 버그가 사용자에게는 어떤 문제를 일으켰을까요? 첫 대화 차례 중 하나에서 액션이 사용자에게 인사를 건넨 후에 사용자가 Dialogflow 에이전트가 인식하지 못하는 대답을 한 경우 정적 대체 메시지를 반복해서 받는 문제였습니다. 원래 의도했던 동작은 설계 지침에 따라 사용자의 말(입력)을 3차례 인식하지 못하면 대화를 종료하도록 하는 것이었습니다. 그렇다면 이 문제가 왜 발생한 것일까요? 일부 응답에서
RichResponse 메시지를 배열로 둘러싸야 한다는 점
을 깜빡 잊었기 때문입니다. 이는 데이터 구조와 그에 상응하는 파싱 로직의 상대적 불안정성을 잘 보여주는 사례였습니다.
불안정한 프로세스를 통해 발생하는 다른 중대한 버그도 있었고 약간 바보스럽게 느껴지게 만드는 버그도 있었습니다. Git 저장소에 압축하지 않은 Dialogflow 에이전트를 보관한 것은 코드 검토를 위한 훌륭한 아이디어였지만, 그 때문에 배포에 몇 가지 문제가 있었습니다. 배포 프로세스는 다음과 같았습니다.
중앙 관리되는 Git 저장소에서 로컬 코드베이스로 최신 코드와 Dialogflow 에이전트를 가져오기
Cloud Functions 코드 배포
Dialogflow 에이전트 디렉토리 압축
에이전트 .zip 파일을 프로덕션 Dialogflow 콘솔로 가져오기
어디서 문제가 생겼을까요? Dialogflow 에이전트의 로컬 개발 복사본에 중앙에서 관리되는 버전(개발 초기에 도입되었지만 이후에 삭제됨)보다 많은 인텐트가 있는 경우 이처럼 불필요한 인텐트를 추적하지 않아서 Git 가져오기 단계에서 이런 인텐트를 정리하지 못했기 때문입니다. 즉, Dialogflow에 배포하면서 때로는 관계없는 인텐트가 포함되었다는 뜻입니다. 우리는 프로덕션 에이전트에 너무 많은 인텐트가 나타나는 것을 보고서야 이 문제를 눈치채기 시작했으며, 배포하기 전에 항상 dialogflow-agent 디렉토리를 새로 다운로드하는 과정부터 시작하도록 함으로써 이 문제를 해결했습니다.
마침내, 그 모든 문제와 제한 사항을 극복하고서 만들어낸 액션에 자부심을 느낍니다. 오늘 현재, Action은 300여 개의 평가를 통해
어시스턴트 디렉토리
에서 4.7점의 높은 별점을 받고 있습니다. 이번 블로그 게시물 시리즈가 멋진 액션을 만든다는 목표를 달성하는 데 있어 훌륭한 안내자 역할을 할 수 있기를 바랍니다.
우리가 이 글에서 설명한 모든 내용을 직접 눈으로 확인하고 여러분도 참여해 일조하고 싶으세요? 코드는
여기
서 확인하세요.
Contents
ML/Tensorflow
Android
Flutter
Web/Chrome
Cloud
Google Play
Community
Game
Firebase
검색
Tag
인디게임페스티벌
정책 세미나
창구프로그램
AdMob
AI
Android
Android 12
Android 12L
Android 13
Android 14
Android Assistant
Android Auto
Android Games
Android Jetpack
Android Machine Learning
Android Privacy
Android Studio
Android TV
Android Wear
App Bundle
bootcamp
Business
Chrome
Cloud
Community
compose
Firebase
Flutter
Foldables
Game
gdg
GDSC
google
Google Developer Student Clubs
Google Play
Google Play Games
Interview
Jetpack
Jetpack Compose
kotlin
Large Screens
Library
ma
Material Design
Material You
ML/Tensorflow
mobile games
Now in Android
PC
Play Console
Policy
priva
wa
wear
Wearables
Web
Web/Chrome
Weeklyupdates
WorkManager
Archive
2024
12월
11월
10월
9월
8월
7월
6월
5월
4월
3월
2월
1월
2023
12월
11월
10월
9월
8월
7월
6월
5월
4월
3월
2월
1월
2022
12월
11월
10월
9월
8월
7월
6월
5월
4월
3월
2월
1월
2021
12월
11월
10월
9월
8월
7월
6월
5월
4월
3월
2월
1월
2020
12월
11월
10월
9월
8월
7월
6월
5월
4월
3월
2월
1월
2019
12월
11월
10월
9월
8월
7월
6월
5월
4월
3월
2월
1월
2018
12월
11월
10월
9월
8월
7월
6월
5월
4월
3월
2월
1월
2017
12월
11월
10월
9월
8월
7월
6월
5월
4월
3월
2월
1월
2016
12월
11월
10월
9월
8월
7월
6월
5월
4월
3월
2월
1월
2015
12월
11월
10월
9월
8월
7월
6월
5월
4월
3월
2월
1월
2014
12월
11월
10월
9월
8월
7월
6월
5월
4월
3월
2월
1월
2013
12월
11월
10월
9월
8월
7월
6월
5월
4월
3월
2월
1월
2012
12월
11월
10월
9월
8월
7월
6월
5월
3월
2월
1월
2011
12월
11월
Feed