홈페이지를 아이폰 홈 화면에 웹앱으로 추가하는 방법

  • 22nd August 2026
  • 7 min read

사내 그룹웨어를 모바일 대응하고 나면 마지막 한 걸음이 남는다. 폰 홈 화면에 아이콘으로 올려서 앱처럼 쓰게 하는 것이다.

앱스토어에 올릴 것도 아니고, 네이티브 앱을 만들 것도 아니다. 사파리에서 공유 → 홈 화면에 추가만 하면 끝난다. 아이콘이 생기고, 탭하면 주소창 없이 전체화면으로 뜬다. 여기까지는 아무것도 안 해도 된다.

문제는 그다음이다.

링크를 한 번 눌렀더니 브라우저가 됐다

첫 화면은 분명 앱처럼 떴다. 그런데 메뉴에서 결재함으로 이동하는 순간 위쪽에 주소창이 생기고, 아래에 사파리 도구막대가 붙는다. 앱이었던 것이 갑자기 브라우저가 된다.

로그인 세션은 그대로다. 화면도 정상이다. 그냥 앱처럼 보이지 않을 뿐이다. 그런데 이게 생각보다 거슬린다. 홈 화면에 아이콘까지 만들어놓고 실제로는 브라우저를 쓰는 셈이 되니까.

홈 화면 아이콘으로 실행 화면 안의 링크를 한 번 탭 manifest 없음 (홈 화면에 추가한 그 주소 하나만 범위) ☰ 그룹웨어 결재 대기 3 메일 12 일정 오늘 2건 gw.example.com ☰ 그룹웨어 결재 대기 3 메일 12 일정 오늘 2건 주소창이 나타난다 웹앱이 아니라 브라우저 scope: "/" (사이트 전체가 웹앱 범위) ☰ 그룹웨어 결재 대기 3 메일 12 일정 오늘 2건 ☰ 그룹웨어 결재 대기 3 메일 12 일정 오늘 2건 그대로 웹앱 주소창 없음 iOS 는 scope 밖 주소를 주소창 달린 인앱 브라우저로 연다. manifest 가 없으면 그 범위가 딱 한 페이지다.
manifest 가 없으면 홈 화면에 추가한 그 주소 하나만 웹앱 범위가 된다. 링크 한 번이면 인앱 브라우저다.

원인은 manifest가 없었던 것

iOS는 홈 화면 웹앱에 범위(scope)라는 개념을 둔다. 그 범위 안의 주소는 웹앱 안에서 열고, 범위 밖의 주소는 주소창이 달린 인앱 브라우저로 연다. 밖으로 나가는 링크를 눌렀을 때 "여기서부터는 웹앱이 아니라 웹입니다"라고 알려주는 장치다.

그 범위를 알려주는 것이 manifest다. 그리고 manifest가 없으면 iOS는 홈 화면에 추가한 그 주소 하나만 범위로 잡는다.

여기까지 오면 증상이 정확히 설명된다. 홈 화면에 추가한 주소가 /였으니 /만 웹앱이고, 결재함 링크는 범위 밖이라 인앱 브라우저로 열린 것이다. 버그가 아니라 명세대로 동작한 것이었다.

manifest 한 장

{
    "name": "A사 그룹웨어",
    "short_name": "그룹웨어",
    "lang": "ko",
    "start_url": "/",
    "scope": "/",
    "display": "standalone",
    "background_color": "#f1f2f7",
    "theme_color": "#f1f2f7",
    "icons": [
        {
            "src": "/cms/images/icon.png",
            "sizes": "180x180",
            "type": "image/png",
            "purpose": "any"
        }
    ]
}

핵심은 "scope": "/" 한 줄이다. 사이트 전체를 웹앱 범위로 선언하면, 어느 메뉴로 이동하든 주소창이 나타나지 않는다.

나머지 항목도 각자 역할이 있다.

  • start_url — 아이콘을 탭했을 때 처음 열 주소. scope와 다르게 줄 수 있다(예: 대시보드로 바로 열기).
  • display: "standalone" — 브라우저 UI 없이 띄운다. fullscreen은 상태바까지 먹어서 시계와 배터리가 사라지므로, 업무용 화면에는 standalone이 맞다.
  • background_color — 앱이 뜨는 순간 화면이 채워지기 전까지 보이는 색. 여기가 흰색이면 다크 테마에서 켤 때 한 번 번쩍인다.
  • icons — iOS는 apple-touch-icon을 먼저 보므로 사실상 안드로이드·데스크톱용이다. 180×180 하나로 충분하다.

메타 태그 — 특히 이름 붙이기

<link rel="apple-touch-icon" href="/cms/images/icon.png">
<link rel="manifest" href="/cms/manifest.json">

<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="{{SiteName}}">

<meta name="theme-color" content="#f1f2f7" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#17181e" media="(prefers-color-scheme: dark)">

여기서 실무적으로 중요한 건 apple-mobile-web-app-title이다.

홈 화면 아이콘 아래에 붙는 이름인데, iOS에서는 이 값이 manifestname보다 우선한다. 한 코드베이스로 여러 사이트를 굴리는 경우 — 그룹웨어와 헬프데스크가 같은 레이아웃을 쓰는 식이라면 — manifest는 한 장을 공유하면서 아이콘 이름만 사이트별로 다르게 붙일 수 있다. 템플릿 변수 하나면 된다.

theme-color를 두 벌 주는 것도 작지만 티가 난다. 상태바 주변 색이 화면 배경과 맞아떨어져야 앱처럼 보인다. 라이트에서만 맞춰두면 다크 테마에서 위쪽에 밝은 띠가 남는다.

구형 iOS를 위한 폴백

manifest를 홈 화면 웹앱에 반영하기 시작한 건 iOS 16.4부터다. 그 아래 버전은 manifest를 아예 보지 않고 예전처럼 동작한다. 사내 시스템은 기기 교체 주기가 길어서 구형이 남아 있기 마련이다.

그래서 자바스크립트로 같은 일을 한 번 더 한다. standalone으로 떠 있을 때, 같은 사이트로 가는 평범한 링크만 가로채서 직접 이동시킨다.

(function () {
    // navigator.standalone 은 iOS 사파리에만 있는 값이다.
    // 홈 화면 아이콘으로 띄운 창일 때만 true.
    if (!window.navigator.standalone) return;

    document.addEventListener('click', function (e) {
        var link = e.target.closest ? e.target.closest('a') : null;
        if (!link || !link.href) return;

        // 새 창으로 여는 것이 의도인 링크들은 그대로 둔다
        if (link.target && link.target !== '_self') return;
        if (link.hasAttribute('download')) return;
        if (link.classList.contains('popup')) return;

        // 다른 스킴(tel:, mailto:)과 외부 도메인은 밖으로 나가는 것이 맞다
        if (link.protocol !== 'http:' && link.protocol !== 'https:') return;
        if (link.host !== window.location.host) return;

        // 같은 페이지 앵커는 브라우저에 맡긴다
        var raw = link.getAttribute('href') || '';
        if (raw === '' || raw.charAt(0) === '#') return;

        e.preventDefault();
        window.location.href = link.href;
    }, false);
})();

코드에서 실제로 중요한 건 location.href 한 줄이 아니라 그 앞의 return 여섯 개다. 이런 가로채기는 무엇을 가로채는가보다 무엇을 건드리지 않는가로 품질이 갈린다.

건드리지 않는 것 이유
target이 지정된 링크새 창이 의도다
download 속성이동이 아니라 저장이다
팝업 관례 클래스프로젝트가 팝업으로 쓰는 링크다
tel: · mailto:전화·메일 앱으로 넘어가야 한다
외부 도메인밖으로 나가는 게 맞다
# 앵커드롭다운·탭 토글이 대부분이다

특히 마지막 항목을 빠뜨리면 드롭다운 메뉴가 전부 페이지 이동으로 바뀐다. 부트스트랩 계열 UI는 토글을 <a href="#">로 만드는 경우가 많아서, 이 한 줄이 없으면 화면 곳곳이 깨진다.

폼 전송은 손대지 않았다. POST 제출은 원래 standalone 안에서 유지되기 때문이다. 필요 없는 곳까지 가로채면 버그만 늘어난다.

알아두면 하루를 아끼는 함정

iOS는 홈 화면에 추가하던 시점의 설정을 저장한다.

manifest를 고치고 배포해도 이미 홈 화면에 있는 아이콘에는 반영되지 않는다. 아이콘을 지우고 다시 추가해야 한다. 이걸 모르면 "분명 고쳤는데 그대로다"를 반복하며 코드를 계속 의심하게 된다. 캐시 문제로 착각하기 딱 좋다.

테스트할 때는 아예 습관을 만들어두는 편이 낫다. 고쳤으면 아이콘부터 지운다.

브라우저를 없애면, 브라우저가 하던 일이 남는다

여기까지 하면 웹앱은 완성된 것처럼 보인다. 그런데 실제로 써보니 예상 못 한 숙제가 하나 나왔다.

이 시스템은 상세·수정 화면 상당수를 팝업으로 띄운다. 데스크톱에서는 아무 문제가 없다. 볼 일이 끝나면 창의 X를 누르면 되니까. 그래서 팝업 화면 48개 중 화면 안에 닫기 버튼이 있는 것은 4개뿐이었다. 나머지 44개는 닫는 방법이 브라우저에만 있었다.

홈 화면 웹앱에서는 그 X가 없다.

데스크톱 브라우저 결재선 지정 팝업 창의 X 로 닫으면 그만이다. 닫기 버튼이 없어도 아무도 불편하지 않았다. 48개 팝업 중 닫기 수단 4개 홈 화면 웹앱 gw.example.com/…/frame 팝업이 Safari 로 열린다. 창을 닫는 X 가 없고, 뒤로 갈 곳도 없다. 화면에 닫기 버튼이 없으면 갇힌다 브라우저 크롬을 없앤다는 것은, 브라우저가 대신 해주던 일을 화면이 직접 해야 한다는 뜻이다.
데스크톱에서는 창의 X가 닫기 버튼을 대신하고 있었다. 그 X가 사라지면 닫을 방법이 남지 않는다.

팝업은 사파리로 열리는데, 거기엔 닫을 창도 없고 돌아갈 뒤로도 없다. 사용자는 그 화면에 갇힌다. 앱 전환으로 빠져나오는 수밖에 없다.

결국 44개 화면에 닫기 버튼을 넣었다. 기계적인 작업 같지만 실제로는 화면마다 사정이 달랐다.

  • @if/@else로 [등록]과 [수정] 버튼이 갈리는 폼이 많았다. 마지막 버튼 뒤에 넣으면 한쪽 화면에만 나온다. @endif 뒤에 넣어야 양쪽 다 나온다.
  • "하단 버튼 영역의 첫 자식이 버튼 그룹"이라는 가정이 통하지 않는 화면이 있었다. 안내 문구가 먼저 오는 경우다.
  • 어떤 화면은 하단 막대 전체가 권한 조건문 안에 들어 있어서, 권한이 없으면 막대째 사라졌다. 막대는 항상 그리고 조건부 버튼만 조건 안에 두도록 구조를 바꿔야 했다.
  • 복원 폼 안에 버튼 영역이 들어가 있어 폼 밖으로 꺼내야 하는 화면도 있었다.

템플릿 46개를 고쳤으니 눈으로 훑는 것만으로는 부족했다. 컴파일 결과를 php -l로 검사하고 태그 여닫이 균형을 파일마다 확인했다.

정리 — 홈 화면 웹앱 체크리스트

  • manifest.jsonscope를 준다. 이게 링크 이동의 핵심이다.
  • apple-touch-iconapple-mobile-web-app-title을 넣는다. 멀티사이트라면 title은 사이트별로.
  • theme-color는 라이트·다크 두 벌.
  • 구형 iOS를 지원해야 한다면 navigator.standalone 폴백을 둔다. 가로채지 말아야 할 것들을 먼저 정하고 시작한다.
  • 고쳤으면 홈 화면 아이콘을 지우고 다시 추가한다.
  • 브라우저 UI에 의존하던 동작을 찾아낸다. 닫기, 뒤로 가기, 새로고침 — 데스크톱에서는 공짜였던 것들이다.

웹앱으로 만든다는 건 화면을 전체화면으로 띄우는 일이 아니다. 브라우저가 대신 해주던 일을 화면이 직접 떠맡는 일에 가깝다. 주소창을 없앤 대가로 44개 화면에 닫기 버튼을 넣게 될 줄은 시작할 때 몰랐다.