블로그 목록
techIoT 플랫폼하이브리드 앱BLE앱 셸서버 주도 UI

제품이 늘어도 앱은 한 벌: 하이브리드 앱 셸 설계기

고객사마다 제품이 다르고 같은 고객사도 모델을 계속 추가하는데, 그때마다 앱을 새로 만들거나 다시 스토어 심사를 받으면 IoT 사업은 앱 개발 일정에 묶입니다. 앱을 한 벌만 만들어 놓고 제품이 늘어나는 것을 받아내는 구조를 어떻게 짰는지 정리했습니다.

OpenIoT

OpenIoT 플랫폼팀

조회 8
제품이 늘어도 앱은 한 벌: 하이브리드 앱 셸 설계기

하드웨어는 양산 준비가 끝났는데 출시일이 스토어 심사 대기열에 걸리는 장면이 모델 하나 늘 때마다 반복됩니다. 이걸 없애려고 앱은 한 벌만 만들어 두고 제품이 늘어나는 쪽을 서버가 받아내게 했는데, 그 구조를 처음부터 적었습니다.


모델 하나 추가했는데 앱을 다시 심사받아야 합니다

IoT 제품을 만드는 회사에서 흔히 벌어지는 장면입니다. 센서 신제품이 나와 하드웨어는 이미 양산 준비가 끝났는데, 앱 담당자가 앱을 다시 빌드해서 스토어에 올려야 하고 심사가 며칠 걸릴지는 모른다고 말합니다.

하드웨어가 다 준비된 뒤에도 출시가 남의 일정에 묶인다는 점을 봐주세요. 심사 기간은 우리가 정할 수 없고, 통과한 뒤에도 사용자가 직접 업데이트하기 전까지는 새 제품이 보이지 않습니다.

기존 사내 구현들을 조사하면서 확인한 증상은 넷이었습니다. 로그인·약관·푸시·마이페이지는 어느 앱에나 있는데 고객사가 바뀔 때마다 매번 처음부터 다시 만들고, 기기 통신 규격이 앱 안에 박혀 있으면 새 모델은 재빌드 없이 인식되지 않습니다. 로그인 세션이 끊기는 버그를 발견하면 앱이 8개일 때 8번 고치고 8번 심사받고, 화면 문구 한 줄을 급히 바꿔야 해도 스토어를 거치는데 서버라면 5분이면 끝날 일입니다.

앱이 "제품 하나짜리 프로그램"으로 만들어지기 때문입니다

원인은 대체로 같은데 기기에 대한 지식이 앱 코드 안에 들어 있는 것입니다. 이 모델의 블루투스 통신 주소가 무엇인지, 화면에 스위치를 몇 개 그려야 하는지, 온도 범위가 몇 도부터 몇 도인지 같은 정보가 앱 안에 상수로 적혀 있으면 그 앱은 그 제품 전용 프로그램이라 제품이 늘면 앱도 늘어납니다.

아래로 갈수록 일이 곱해진다는 점이 이 그림의 요점입니다. 제품 수만큼 앱이 생기고 어느 앱에나 똑같이 들어가는 로그인·약관·푸시를 매번 다시 만들며, 버그 하나를 고칠 때도 앱 개수만큼 반복합니다.


기기 등록 화면 하나가 선택지를 정리해 줬습니다

세 가지 방식을 놓고 비교했습니다.

방식어떤 구조인가장점결정적 한계
① 완전 네이티브iOS·Android 각각 그 OS의 언어로 전부 제작하드웨어 접근이 자유롭고 성능이 가장 좋음화면 하나 고쳐도 스토어 심사. 두 OS를 따로 만들어 개발 비용이 두 배
② 완전 웹앱브라우저에서 도는 웹페이지. 앱을 설치하지 않음배포가 즉시. 코드 한 벌블루투스·푸시·보안 저장소를 못 씀. IoT 기기 등록 자체가 불가능
③ 하이브리드 셸얇은 네이티브 앱이 껍데기 역할을 하고 그 안에서 웹 화면이 돎하드웨어도 되고 화면은 즉시 배포껍데기와 웹 사이의 통신 규격을 잘 정해야 함. 여기가 깨지면 앱 전체가 멈춤

①과 ②가 각자 반쪽만 가졌다는 점을 봐주세요. ①은 하드웨어를 얻고 배포 속도를 잃었고 ②는 배포 속도를 얻고 하드웨어를 잃었으며, ③만 둘 다 갖는 대신 껍데기와 웹 사이의 규격이라는 숙제를 떠안습니다.

②가 왜 탈락인지가 중요합니다. IoT 앱의 핵심 작업은 기기를 처음 등록하는 일인데 이건 블루투스나 와이파이로 기기와 직접 대화해야 하고 브라우저는 이 일을 제대로 하지 못합니다. ①은 가능하지만 비싸고, 이 글의 문제인 "제품이 늘 때마다 심사"를 전혀 풀지 못합니다. 같은 세 갈래를 저울질하고 계신다면 기기 등록 화면부터 먼저 그려보시길 권하는데, 저희는 그 화면 하나에서 선택지가 정리됐습니다.

이 구조는 저희가 처음 생각해낸 것도 아닙니다. 대형 상용 앱들이 오래전부터 쓰는 방식이고, 상용 IoT 플랫폼이 기기 화면을 서버에서 정의해 내려주는 것도 같은 발상입니다. 사내에도 앞서 만들어 본 구현 두 개가 이미 하이브리드 방식을 택하고 있었는데, 같은 결론에 독립적으로 두 번 도달했다는 것이 판단에 도움이 됐습니다.


네이티브는 하드웨어만, 화면은 전부 웹으로 보냈습니다

③을 택하고 경계선을 아주 분명하게 그었습니다.

가운데 브리지가 위아래를 잇는 유일한 통로라는 점을 봐주세요. 네이티브와 웹은 서로를 직접 만지지 않고 정해진 규격의 메시지로만 대화하는데, 이 선을 분명히 그어야 아래쪽 전체를 스토어 심사에서 떼어낼 수 있습니다. 네이티브 셸은 웹에서 불가능한 것만 맡고 웹 화면은 전 화면과 서버 호출과 비즈니스 로직을 맡으며, 웹 쪽은 관리자 웹과 코드를 공유합니다.

둘 사이는 우편함에 규격 봉투를 넣는 것처럼 정해진 형식의 메시지로만 대화합니다. 봉투에는 요청 종류와 내용물과 답장을 짝지을 번호가 들어가고, 답장이 30초 안에 오지 않으면 웹은 시간 초과로 처리하고 넘어가서 네이티브 쪽이 멈춰도 화면은 영원히 기다리지 않습니다. 같은 구조를 검토 중이시라면 이 경계선을 어디에 그을지부터 보시길 권하는데, 나중에 옮기기가 가장 어려운 선이었습니다.

하드웨어를 만지는 화면은 생각보다 적습니다

앱 전체를 화면 단위로 세어봤더니 총 60개 화면 중 하드웨어를 직접 만지는 것은 기기 연결 6화면뿐이었습니다. 나머지 54개인 로그인·대시보드·제어·데이터 그래프·자동화·알림·마이페이지는 전부 그냥 화면이라, 앱의 90%는 웹으로 만들어도 아무 차이가 없고 그 90%를 스토어 심사에서 떼어낼 수 있다면 떼어내는 게 맞습니다.

바꾸는 것완전 네이티브우리 구조
화면 문구 · 레이아웃재빌드 + 심사 + 사용자 업데이트즉시 반영
새 기능 화면 추가재빌드 + 심사즉시 반영
서버 연동 로직 수정재빌드 + 심사즉시 반영
계산 오류 · 표시 버그 수정재빌드 + 심사즉시 반영
블루투스 라이브러리 교체재빌드 + 심사재빌드 + 심사

빨간 줄 가운데의 재빌드·심사·업데이트 대기 칸이 초록 줄에는 아예 없다는 점을 봐주세요. 아래 주황 줄은 그래도 심사를 거쳐야 하는 경우인데 위 표의 마지막 줄 하나뿐이고 그 일은 자주 일어나지 않습니다. 고객사 입장에서는 "화면에 잘못된 값이 표시된다"는 신고가 들어왔을 때 며칠이 아니라 그날 안에 고칠 수 있다는 뜻이라 사고 대응 속도가 다른 등급이 됩니다. 부수 효과로 관리자 웹과 앱 화면이 같은 부품과 같은 서버 통신 코드를 써서 같은 기능을 두 번 만들지 않습니다.

기기 정의를 앱이 아니라 서버에 뒀습니다

여기가 이 설계의 중심입니다. 블루투스로 기기와 대화하려면 "이 기기의 어느 창구에 말을 걸어야 하는가"에 해당하는 식별자가 필요한데 보통 이 값은 앱 코드에 상수로 박혀 있습니다. 저희는 이걸 기기 타입 정보로 서버에 저장하고 앱이 페어링 직전에 내려받도록 했습니다.

서버에 두는 것내용
블루투스 서비스 식별자스캔할 때 걸러낼 기준값
특성 목록시리얼을 읽는 창구 · 와이파이 정보를 쓰는 창구 · 진행 상태를 받는 창구
광고에 실린 시리얼 위치몇 번째 바이트부터 몇 바이트인지
제어 UI 구성이 모델에 스위치·슬라이더·선택·실행 중 무엇을 몇 개 그릴지, 범위는 얼마인지

파란 상자에서 두 갈래가 갈라져 나온다는 점을 봐주세요. 스캔 필터와 연결할 창구가 한 갈래이고 제어 화면 구성이 다른 갈래인데 둘 다 앱 코드가 아니라 서버가 내려준 값에서 나오므로, 새 제품 모델을 추가해도 앱은 그대로이고 관리자 화면에서 기기 타입을 하나 등록하면 끝입니다.

이게 단순한 편의 문제가 아니라 기술적으로 강제되는 선택이었다는 근거가 하나 더 있습니다. iOS는 앱이 화면 밖에서 블루투스를 스캔할 때 무엇을 찾을지 식별자를 미리 지정하도록 요구하므로, 앱은 스캔을 시작하기 전에 그 값을 알고 있어야 합니다. 이 값을 앱 안에 박아두면 모델이 하나 추가될 때마다 재빌드가 구조적으로 강제되어서, 서버에서 내려받는 방식이 아니면 이 문제를 피할 방법이 없습니다.

닭과 달걀 문제도 없습니다. 페어링 시점에 그 기기는 아직 등록 전이지만 그 모델은 이미 고객사 계정에 등록돼 있기 때문입니다. 여기에 조회 결과를 앱에 캐시하는 것을 더했는데 공장이나 설치 현장은 인터넷이 없는 경우가 많고 그런 곳에서도 페어링이 되어야 하기 때문이고, 부수적으로 외주 업체가 만든 펌웨어가 자체 식별자를 쓰더라도 기기 타입에 그 값을 적어두면 앱을 고치지 않고 받아들일 수 있습니다.

접속할 때 서로의 "능력"을 협상합니다

같은 코드베이스라도 앱은 인터넷에 직접 붙는 기기 한 대를 다루는 단독 기기형, 허브 하나에 자식 기기 여러 대가 붙는 허브형, 폰과 기기가 직접 통신하는 근거리 전용형 셋으로 갈립니다. 여기에 OS별 차이와 앱 버전 차이까지 겹치면 구버전 앱에는 없는 기능을 신버전 웹 화면이 호출해 오류가 나는데, 웹만 먼저 배포되는 구조라 이 어긋남은 반드시 발생합니다.

그래서 앱이 켜질 때 인사를 주고받게 했습니다. 웹이 "안녕, 나는 이 버전이야"라고 보내면 네이티브가 "내 버전은 이거고 내가 할 수 있는 건 이 목록이야" 라고 답하는데, 목록은 블루투스·광고 송출·와이파이·카메라·사진첩·위치·지오펜스·생체인증·NFC·푸시·보안저장소·파일·캘린더·무결성검사 14종입니다.

웹은 이 목록을 보고 없는 기능의 버튼을 아예 그리지 않고, 네이티브는 모르는 요청을 받으면 오류로 죽는 대신 "그건 지원하지 않음"이라고 정중히 답합니다. 이 두 규칙 덕분에 구버전 앱과 신버전 화면 조합에서도 앱이 멈추지 않고, 사용자는 자기 앱이 지원하는 만큼만 보며 나머지는 조용히 감춰집니다.


31바이트 안에 무엇을 넣을지가 가장 오래 걸렸습니다

라벨 QR을 찍으면 시리얼 번호를 알 수 있지만 그 시리얼을 가진 기기가 눈앞의 어느 것인지 알 방법이 문제였습니다. 보통은 기기의 고유 주소로 맞추는데 iOS는 이 주소를 앱에 알려주지 않고 폰마다 다르고 주기적으로 바뀌는 임시 번호를 주기 때문에, 물리적으로 같은 기기인지 판별이 불가능합니다. 그러면 주변 기기에 하나씩 전부 연결해서 시리얼을 읽어봐야 하는데 매장이나 사무실처럼 기기가 여러 대인 곳에서는 사실상 페어링이 안 됩니다.

그래서 기기가 블루투스로 내보내는 광고 신호에 시리얼 일부를 실었습니다. 광고 신호에 담긴 제조사 데이터는 iOS도 그대로 앱에 전달하기 때문인데, 문제는 이 신호가 전체 31바이트밖에 되지 않는다는 것이었고 돌아보면 이 배치를 맞추는 데 가장 오래 붙들려 있었습니다.

초록 칸 안의 시리얼 6바이트가 우리가 쓸 수 있는 전부라는 점을 봐주세요. 오른쪽 끝에 여유 칸이 없어서 무엇을 더 넣으려면 반드시 무엇을 빼야 하고, 시리얼이 6바이트보다 긴 제품은 뒤쪽 6바이트만 싣습니다. 6바이트면 48비트라 현실적 규모에서 겹칠 일이 없습니다.

중요한 것은 이 값의 역할입니다. 광고에 실린 시리얼은 후보를 좁히는 필터일 뿐이고 확정은 연결한 다음 정식 창구에서 전체 시리얼을 읽어서 하는데, 자리가 부족해 잘라 넣은 값에 최종 판단을 맡기지 않습니다.

판단이 한 번에 끝나지 않고 세 번에 걸쳐 좁혀진다는 점이 요점입니다. 앞의 두 단계는 연결하지 않고도 가능해서 빠르고 마지막 확정만 연결 후에 정확한 값으로 하며, 사용자에게 보여줄 이름이나 배터리 같은 나머지 정보는 폰이 되물었을 때만 오는 두 번째 응답에 실었습니다.

소유권을 증명하는 값은 광고에 실을 수 없습니다

기기를 등록하려면 "이 기기가 정말 내 손에 있다"는 증명이 필요한데, 초기 검토안 중에는 이 증명값을 QR에 인쇄하거나 광고에 싣자는 안이 있었습니다. 둘 다 위험한데 블루투스 광고는 주변 아무나 수신할 수 있고 QR 인쇄도 라벨 사진 한 장이면 유출되기 때문입니다.

그래서 역할을 분리했습니다. QR 라벨에는 모델 코드와 시리얼만, 블루투스 광고에는 서비스 식별자와 시리얼 6바이트만 담는데 둘 다 알려져도 무해한 식별 정보입니다. 소유권 증명값은 공장에서 기기 내부 저장소에 심어두고 앱이 실제로 그 기기에 연결한 뒤에만 받아오므로 라벨 사진이 유출돼도 등록에 쓸 수 없고, 최종 판정은 서버가 해서 증명값을 알고 있어도 이미 소유자가 있으면 등록이 거부됩니다.

기기 와이파이에 붙어 있는 동안은 인터넷이 끊깁니다

와이파이 기기를 등록할 때는 기기가 잠깐 만들어내는 임시 와이파이에 폰을 접속시켜야 하는데, iOS는 앱이 사용자 몰래 와이파이를 갈아타는 것을 허용하지 않습니다. 그래서 기본은 "설정 앱에서 이 네트워크를 선택해 주세요"라는 안내 화면을 제대로 만드는 것으로 잡았습니다. 안내 없이 자동 전환에만 기대면 전환이 막힌 순간 사용자는 아무 설명 없이 멈춘 화면을 보게 되기 때문입니다.

더 까다로운 건 그 다음이었습니다. 폰이 기기의 임시 와이파이에 접속한 동안에는 인터넷이 없어서 이 구간에서 서버를 호출하는 코드가 있으면 전부 실패하고, 기기가 집 와이파이에 무사히 붙었는지도 앱은 알 수 없습니다. 폰이 이미 다른 네트워크에 있기 때문입니다.

두 번째 칸인 인터넷 없음 구간의 앞뒤로 서버 통신을 몰아둔 것이 이 그림의 요점입니다. 필요한 값은 들어가기 전에 미리 받고 결과 확인은 나온 뒤에 되묻는데, 되묻기에 끝을 60초로 정해 둔 덕분에 실패해도 화면이 멈추지 않고 안내로 넘어갑니다.

큰 파일을 규격 봉투에 그대로 넣으면 화면이 멈춥니다

네이티브와 웹 사이의 메시지는 문자열이라, 사진 한 장을 문자열로 바꿔 넘기면 수 MB짜리 문자열이 되고 그 순간 화면이 얼어붙습니다. 문의 게시판에 사진을 첨부하는 흔한 기능에서 바로 터지는데, 그래서 큰 데이터는 내용물을 넣지 않고 "보관함 번호"만 넘기도록 규칙으로 못 박았습니다. 파일은 기기 저장소에 두고 봉투에는 그 위치만 적으며, 업로드 전 이미지 압축도 네이티브가 처리하고 결과 위치만 돌려줍니다.

함께 웹 화면이 네이티브를 직접 부르는 것도 금지하고 공용 라이브러리를 거치게 했습니다. 이 브리지는 단일 장애점이라 규약이 깨지면 앱 전체가 멈추므로 편의보다 엄격함을 택했는데, 이 부분은 지금도 더 나은 방법이 있을 것 같습니다. 다만 브리지만큼은 느슨하게 두지 않는 편이 낫다는 것이 지금의 판단입니다.


지금 손에 있는 것을 그대로 적습니다

현재 이 구조는 설계 확정 + 동작하는 화면 프로토타입 단계이고 성능 측정치나 사용자 반응은 아직 없습니다. 지금까지 이야기한 것을 한 장으로 모으면 이렇습니다.

앱으로 들어오는 화살표가 전부 서버에서 출발한다는 점을 봐주세요. 새 모델이 늘어도 바뀌는 것은 위쪽 기기 타입 정의이고 화면이 바뀌어도 바뀌는 것은 웹 배포이며, 아래쪽 네이티브 셸만 그대로 남아 있는 것이 앱을 한 벌로 유지할 수 있는 이유입니다.

구현이 따라야 하는 계약으로 고정한 값들입니다.

규약내용
브리지 메시지 형식요청 종류 · 내용물 · 짝맞춤 번호. 요청-응답형과 일방 통보형 2종
필수 규칙 3가지짝맞춤 번호 필수 · 기본 30초 타임아웃 · 시작 시 버전 협상
능력 목록 14종앱이 선언하고 화면이 그에 맞춰 기능을 노출
오류 코드 7종권한 거부 · 미지원 · 하드웨어 없음 · 사용자 취소 · 타임아웃 · 형식 위반 · 그 외
구현 규칙 5조모르는 요청은 "미지원"으로 응답 · 큰 데이터는 참조로 전달 · 직접 호출 금지 등
기기 정의 스키마서비스 식별자 · 제조사 번호 · 시리얼 위치 · 창구 목록 · 제어 UI 구성
광고 패킷 배치31바이트 정확히 소진
QR 페이로드버전 · 모델 · 시리얼 3개 필드. 증명값 없음

프로토타입은 모바일 14화면을 클릭되는 형태로 만들었습니다. 스플래시에서 저장된 토큰을 읽어 홈·로그인·온보딩으로 갈리는 분기, 기기 추가의 5단계 흐름, 제어·데이터·정보 3개 탭의 기기 상세, 일·주·월 구간에 따라 집계 단위가 바뀌는 그래프까지 확인했습니다.

특히 유용했던 장치가 둘 있습니다. 앱 타입 전환기는 화면 옆에 앱 타입 3종을 바꾸는 버튼을 둔 것인데, 누르면 하단 메뉴와 홈 화면 구성과 기기 등록 방식 선택지가 실제로 바뀌어서 서버가 내려주는 설정값이 들어갈 자리를 그대로 재현합니다. 덕분에 "앱 한 벌로 여러 제품군을 대응한다"는 말이 화면에서 검증됩니다. 기기 정의 기반 제어 화면은 컨트롤을 그려 넣은 것이 아니라 기기 타입의 제어 UI 정의를 읽어서 그리는데, 스위치·슬라이더·선택·실행 4종이 정의대로 나타나고 범위와 단위도 정의에서 옵니다.

그리고 이 프로토타입은 버려지지 않습니다. 화면과 로직이 웹인 구조라 이 웹 화면이 그대로 이행 대상입니다. 검증 산출물로는 기존 앱 구현 8건 조사, 앱 13개 그룹 78항목의 화면-기능 전수 대조, 브리지 메시지 목록화, 그리고 도메인 확보나 제조사 번호 등록처럼 사람이 실행해야 하는 항목을 분리한 남은 결정 목록이 남았습니다.


앱을 재배포하게 만드는 것은 코드가 아니라 지식이었습니다

가장 크게 배운 점입니다. 앱을 다시 심사받게 만드는 원인은 대개 새 코드가 아니라 앱 안에 박혀 있던 정보인데, 통신 식별자나 화면 구성이나 기능 목록이 상수로 들어 있으면 값 하나 바꾸는 데도 스토어를 거칩니다. 바뀔 것 같은 정보는 코드에서 데이터로 옮긴다는 원칙 하나로 "모델 추가마다 재배포"가 사라졌습니다.

플랫폼의 제약이 설계를 결정하기도 한다는 것도 배웠습니다. 기기 정의를 서버에 두기로 한 것은 처음에는 그게 더 깔끔해서였는데, 파고들어 보니 iOS의 백그라운드 스캔 규칙 때문에 사실상 선택지가 없었습니다. 취향으로 정한 줄 알았던 결정에 기술적 필연이 있었던 셈이고, 근거를 찾아보지 않았다면 나중에 "왜 이렇게 했더라"로 되돌렸을지도 모릅니다.

31바이트 같은 물리적 한도는 타협 대상이 아니라는 것도 확인했습니다. 넣고 싶은 정보는 많은데 한도는 고정이라 여기서 할 일은 무엇을 뺄지 정하는 것이 아니라 각 정보가 어느 단계에서 필요한지 나누는 것이었습니다. 스캔 단계에 필요한 것은 광고에, 되물을 수 있는 것은 두 번째 응답에, 연결 후면 되는 것은 연결 후에, 그리고 비밀이어야 하는 것은 반드시 연결 후에 둡니다. 마지막 줄이 특히 중요한데 자리가 남았어도 증명값은 광고에 실으면 안 되고, 공간 문제와 보안 문제를 같은 표에 놓고 저울질하지 않는 것이 규칙이 됐습니다.

고객이 얻는 것근거
새 제품 모델 추가에 앱 재배포가 필요 없음통신 규격과 제어 화면 구성을 서버의 기기 타입에 저장
화면·로직 수정이 스토어 심사 없이 당일 반영60화면 중 54화면이 웹. 사고 대응 속도가 달라짐
제품군이 달라도 앱은 한 벌앱 타입 3종을 서버 설정으로 분기
버그 수정이 한 번으로 끝남관리자 웹과 앱이 같은 화면 부품·통신 코드를 공유
구버전 앱 사용자도 앱이 멈추지 않음능력 협상 + 미지원 요청의 안전한 거절
라벨 사진이 유출돼도 남이 등록할 수 없음소유권 증명값은 기기에 연결한 뒤에만 전달
인터넷이 없는 설치 현장에서도 페어링 가능기기 정의를 앱에 캐시

두 번째 줄이 이 설계에서 가장 신경 쓴 부분입니다. "고칠 수 있다"와 "오늘 고칠 수 있다"는 운영에서 완전히 다른 이야기입니다.

작업을 끝내고 보니 앱을 묶어두고 있던 것은 코드가 아니라 앱 안에 박아 넣은 지식이었고, 그 지식을 서버 쪽으로 옮기고 나서야 앱 한 벌이 제품 개수를 따라가지 않게 됐습니다.

읽어주셔서 감사합니다.


IoT 서비스,
지금 바로 시작하세요

디바이스 연결부터 대시보드까지 올인원

무료 플랜으로 부담 없이 체험