Web for Duo.

iPhone Duo에서 달라진 여백과 바, 스크롤 끝, 그리고 Glass.
프런트엔드 개발자가 알아둘 것들을 짧게 정리했습니다.

01 Safe Area는 네 방향입니다.

네이티브 앱의 Safe Area와 웹의 인셋은 서로 다른 값입니다. 웹에서는 현재 브라우저가 반환하는 네 방향의 env()를 사용하세요. Duo용 숫자를 정해 넣거나 UIKit의 pt 값을 CSS px로 옮기지 않습니다. [2]

현재 브라우저의 Safe AreaCSS px
위 top
측정 전
오른쪽 right
측정 전
아래 bottom
측정 전
왼쪽 left
측정 전
Layout viewport
측정 전
Visual viewport
측정 전
확대 · 이동
측정 전
Display mode
측정 전
JavaScript로 현재 환경의 값을 읽습니다.

기기 모형이나 인셋 시뮬레이션을 사용하지 않습니다. Duo에서 이 페이지를 열고 Safari·Chrome 각각 접기, 회전, 바 펼침·접힘, 키보드 표시 전후를 비교하세요. 측정값 복사에는 시각·UA·뷰포트가 포함되며 서버로 전송하지 않습니다.

오른쪽이 0px이어도 기기에 우측바가 없다는 뜻은 아닙니다. 브라우저가 이미 웹 뷰포트 바깥에 예약한 공간은 인셋으로 다시 나타나지 않을 수 있습니다. viewport-fit=cover만으로 Safari의 우측바까지 웹 영역이 넓어진다고 가정하지 마세요.

index.html + layout.css
<meta name="viewport"
  content="width=device-width, initial-scale=1, viewport-fit=cover">

/* Apply insets to content, not the full-bleed background. */
.content {
  padding-top:    max(20px, env(safe-area-inset-top, 0px));
  padding-right:  max(20px, env(safe-area-inset-right, 0px));
  padding-bottom: max(20px, env(safe-area-inset-bottom, 0px));
  padding-left:   max(20px, env(safe-area-inset-left, 0px));
}

max()는 기본 여백과 안전 여백 중 큰 값을 선택합니다. 안전 영역에서 추가로 20px을 띄우려면 calc(20px + env(…))를 쓰세요. 20px은 사이트의 디자인 여백이며 Duo의 기기 수치가 아닙니다. 이 페이지도 실제 env() 값으로 본문 여백을 계산합니다. [2]

접히는 부분도 Safe Area로 피할 수 있나요?

가장자리 인셋만으로 화면 가운데의 접힘을 설명할 수는 없습니다. Apple은 카메라처럼 가려지는 영역과 화면을 나누는 접힘 영역을 Reserved Region으로 구분합니다. SwiftUI·UIKit의 네이티브 API와 CSS의 env()는 같은 API가 아닙니다. [3]

웹에는 Viewport Segments와 Device Posture API가 있지만, 아래 WebKit 도입 커밋의 기본값은 꺼져 있었습니다. 현재 설치 버전에서의 노출 여부는 별도로 확인해야 합니다. 지원 여부를 확인한 뒤 보조 기능으로 추가하고, 기본 레이아웃은 화면 크기가 바뀌어도 유지되게 만드세요. [4] [5]

02 바는 떠 있고, 배경은 이어집니다.

사이트 안의 우측 내비게이션은 만들 수 있습니다. Safari·Chrome의 주소창이나 우측 시스템 영역에 웹 버튼을 넣는 공개 API는 없습니다. Apple이 안내하는 세로 툴바와 ReservedRegion은 SwiftUI·UIKit용 기능입니다. [6]

어느 우측바를 사용할 수 있나요?
영역웹에서 할 수 있는 일
Safari의 바배치·크기·버튼은 Safari가 관리합니다. 웹 콘텐츠는 Safari가 제공한 뷰포트에 배치합니다.
iOS용 Chrome의 바Chrome 앱이 관리합니다. WebKit을 공유해도 Safari의 바·인셋·배경 샘플링과 같다고 볼 수 없습니다.
사이트의 내비게이션제공된 뷰포트 안에서 우측·하단 배치를 구현하고, 실제 인셋과 자체 바 크기를 각각 반영합니다.
Safari·Chrome의 WebKit 정책과 Duo 확인 범위

App Review 2.5.6의 기본 원칙은 WebKit 사용입니다. 한국 배포의 일반적인 iOS 브라우저에도 이 원칙이 적용됩니다. 단, EU는 iOS 17.4·iPadOS 18 이상, 일본은 iOS 26.2 이상에서 자격 요건과 entitlement를 충족하면 대체 엔진을 사용할 수 있습니다. 따라서 “전 세계의 모든 iOS 브라우저가 항상 WebKit”이라고 단정하지 않습니다. [13] [14] [15]

확인한 Chromium의 iOS 웹 레이어는 Blink 대신 WKWebView를 감쌉니다. 정책상 대체 엔진이 허용된다는 사실과 Chrome이 해당 엔진을 배포했다는 사실은 별개입니다. 엔진 이름만으로 Duo의 실제 Chrome UI나 인셋 수치를 추정할 수 없습니다. [16]

Duo Safari의 관찰 자료: Mike Piontek은 2026년 10월 7일, Xcode 27.1 RC의 Device Hub에서 우측 툴바를 Safe Area로 취급하지 않는 현상을 직접 보고했습니다. 이는 해당 시뮬레이터의 관찰이며 Apple의 지원 보장이나 모든 출시 버전의 결과가 아닙니다. 우측 공간만큼 CSS 상수를 더하는 우회 방법을 예제로 제공하지 않습니다. [17]

이 문서는 실제 Duo 기기와 Duo Safari·Chrome에서 직접 측정하지 않았습니다. 개발 환경에는 iOS 26.5 시뮬레이터만 설치되어 있습니다. 위 측정기는 접속한 브라우저의 값만 표시하며 기종을 추정하거나 Duo 인셋으로 바꾸지 않습니다.

인터랙티브 예제를 불러오고 있습니다.

일정은 세로로, 세계시계는 가로로 스크롤할 수 있습니다. 이미지 모드에서는 사진이 바 아래까지 이어집니다. 일정·시간은 예시 데이터이며 사이트 안에서 만든 바입니다. 이 예제의 박스 크기와 바 여백은 웹 컴포넌트의 디자인 값이며 Duo의 치수나 Safe Area가 아닙니다.

  • 배경html과 body에 기본색을 지정하고, 배경 이미지도 가장자리까지 이어지게 만드세요. 사진은 사이트의 바 아래까지 이어집니다. Safari가 웹 뷰포트 밖에 둔 영역까지 그릴 수 있다는 뜻은 아닙니다.
  • 콘텐츠본문과 버튼에 인셋을 적용하세요. 고정된 웹 바의 크기는 별도로 예약해 마지막 내용이 가려지지 않게 합니다.
  • 내비게이션폭이 좁으면 하단에, 충분하면 옆에 둘 수 있습니다. 텍스트가 긴 버튼은 가로로 유지하고, 보조 동작은 더보기로 묶으세요.
background.css
:root {
  color-scheme: light dark;
  --page-bg: light-dark(#fafafa, #191919);
}

html, body {
  margin: 0;
  background: var(--page-bg);
}

.page {
  min-height: 100svh;
  /* Keep artwork behind the inset content. */
  background: var(--page-bg);
}

브라우저 바의 색과 투명도는 Safari가 결정합니다. theme-color나 배경색 하나로 결과를 고정한다고 가정하지 마세요. WebKit에는 고정 요소의 backdrop-filter가 바의 색상 샘플링에 영향을 주는 보고도 있습니다. [7]

우측바·하단바의 전체 HTML / CSS 보기

아래 48rem은 예제 콘텐츠에 맞춘 전환점입니다. 기종이나 접힘을 감지하는 값이 아닙니다. 바의 동작 순서는 유지하고, 콘텐츠가 사용할 공간만 바꿉니다.

navigation.html + navigation.css
<div class="app">
  <main class="app-content">…</main>
  <nav class="app-nav" aria-label="주 내비게이션">
    <a href="/" aria-current="page">Home</a>
    <a href="/search">Search</a>
    <a href="/saved">Saved</a>
  </nav>
</div>

.app {
  container-type: inline-size;
  --bar-size: 64px;
}

.app-content {
  padding: 20px;
  padding-top: max(20px, env(safe-area-inset-top, 0px));
  padding-left: max(20px, env(safe-area-inset-left, 0px));
  padding-right: max(20px, env(safe-area-inset-right, 0px));
  padding-bottom: calc(var(--bar-size) + 32px
    + env(safe-area-inset-bottom, 0px));
}

.app-nav {
  position: fixed;
  z-index: 10;
  display: flex;
  align-items: center;
  justify-content: space-around;
  gap: 4px;
  left: max(16px, env(safe-area-inset-left, 0px));
  right: max(16px, env(safe-area-inset-right, 0px));
  bottom: max(16px, env(safe-area-inset-bottom, 0px));
  height: var(--bar-size);
  border: 1px solid #d8d8d8;
  border-radius: 24px;
  color: #202020;
  background: #f7f7f7;
}

.app-nav a {
  display: grid;
  place-items: center;
  min-width: 44px;
  min-height: 44px;
  color: inherit;
}

/* A content breakpoint, not an iPhone Duo detector. */
@container (min-width: 48rem) {
  .app-content {
    padding-right: calc(var(--bar-size) + 32px
      + env(safe-area-inset-right, 0px));
    padding-bottom: max(20px, env(safe-area-inset-bottom, 0px));
  }

  .app-nav {
    flex-direction: column;
    justify-content: center;
    top: max(16px, env(safe-area-inset-top, 0px));
    left: auto;
    width: var(--bar-size);
    height: auto;
  }
}

03 스크롤 끝까지, 가려지지 않게.

첨부 예시의 캘린더처럼 내용은 바 뒤로 지나가도 됩니다. 다만 마지막 항목은 끝까지 스크롤했을 때 바 바깥에 온전히 나와야 합니다. 끝 여백은 공간을 만들고, 가장자리 효과는 읽기 쉬운 경계를 만듭니다. 두 가지를 따로 처리하세요. [11]

위 예제에서 ‘끝 여백’을 끄고 ‘끝까지 스크롤’을 누른 다음, 하단바로 바꿔 비교해 보세요. 세계시계의 마지막 도시에는 같은 원리를 가로 방향으로 적용했습니다.

scroll-edge.css
.scene { position: relative; }

.scroller {
  overflow: auto;
  /* Actual space after the last item. */
  padding-bottom: calc(64px + 24px
    + env(safe-area-inset-bottom, 0px));
  /* Alignment for anchors / scrollIntoView(), not extra space. */
  scroll-padding-bottom: calc(64px + 24px
    + env(safe-area-inset-bottom, 0px));
}

.scroll-edge {
  position: absolute;
  inset: auto 0 0;
  height: 96px;
  pointer-events: none;
  /* A web fade; separate from the refractive glass. */
  background: linear-gradient(to top, #fafafae6, #fafafa00);
}

/* Horizontal list + right-side bar: reserve right-end space. */
.horizontal-scroller {
  padding-right: calc(64px + 24px
    + env(safe-area-inset-right, 0px));
  scroll-padding-right: calc(64px + 24px
    + env(safe-area-inset-right, 0px));
}
scroll-padding, 끝 여백, 가장자리 페이드의 차이

padding-bottom은 마지막 항목 뒤에 실제 스크롤 공간을 만듭니다. scroll-padding-bottom은 앵커 이동이나 scrollIntoView()의 정렬 기준만 조정하므로 그 공간을 대신하지 못합니다. 가로 목록에서는 각각 padding-right, scroll-padding-right에 해당합니다.

Apple의 Scroll Edge Effect는 콘텐츠와 컨트롤을 시각적으로 분리합니다. 여기의 CSS 그라디언트는 그 원칙을 웹에 적용한 가벼운 페이드이며, 네이티브의 가변 blur 재현이나 Glass 굴절 자체가 아닙니다. 본문 전체에 마스크를 걸면 스크롤 끝의 콘텐츠도 사라질 수 있습니다.

배경 확장은 backgroundExtensionEffect()의 디자인 의도를 참고하되, 웹에서는 배경·스크롤 콘텐츠·컨트롤을 서로 다른 레이어로 만듭니다. 네이티브 API가 CSS에서 호출되는 것은 아닙니다. [12]

04 웹에서도, 굴절하는 Glass.

WebKit 소스에는 -apple-visual-effect와 -apple-system-glass-material이 있습니다. 단순한 blur 대신 시스템 재질을 요청하는 속성이지만, useSystemAppearance 설정에 묶인 비표준 기능입니다. 일반 Safari 웹사이트나 기본 WKWebView에서 사용할 수 있는 공개 API로 간주하면 안 됩니다. [8] [9]

일반 브라우저에는 @samasante/liquid-glass의 refract 모드를 사용했습니다. 같은 장면의 복사본에 SVG feDisplacementMap을 적용하고, 실제 배경과 스크롤 좌표를 맞춥니다. 버튼과 텍스트는 굴절 레이어 위에 또렷하게 남깁니다.

React · 설치
npm install react react-dom @samasante/liquid-glass@0.1.1
React 컴포넌트 전체 코드 보기
GlassButton.tsx
import { Glass } from '@samasante/liquid-glass';
import type { ReactNode } from 'react';

type Props = {
  scene: ReactNode;
  view: { width: number; height: number };
  x: number; y: number;
  scrollX?: number; scrollY?: number;
  label: string;
  onClick?: () => void;
  children: ReactNode;
};

export function GlassButton({ scene, view, x, y,
  scrollX = 0, scrollY = 0, label, onClick, children }: Props) {
  return (
    <div style={{ position: 'absolute', left: x, top: y,
      width: 44, height: 44 }}>
      <Glass width={44} height={44} radius={22}
        optics={{ strength: 0.14, frost: 0.65, bend: 0.55 }}
        style={{ width: 44, height: 44 }}
        refract={
          <div aria-hidden inert style={{ position: 'absolute',
            left: -x, top: -y, width: view.width,
            height: view.height, overflow: 'hidden' }}>
            <div style={{ position: 'absolute',
              left: -scrollX, top: -scrollY,
              width: view.width, height: view.height }}>
              {scene}
            </div>
          </div>
        }
      />
      <button type="button" aria-label={label} onClick={onClick}
        style={{ position: 'absolute',
        inset: 0, border: 0, borderRadius: 22,
        background: 'transparent', color: '#163644' }}>
        {children}
      </button>
    </div>
  );
}

/* Measure the viewport with ResizeObserver; pass its scroll offsets.
   scene is the same non-interactive backdrop used by the original. */

같은 Scene을 두 번 그리는 방식입니다. 복사본에는 aria-hidden과 inert를 적용하고 원본만 조작 가능하게 만드세요. 스크롤·크기·데이터가 바뀌면 복사본도 갱신해야 합니다. 영상·캔버스에는 라이브러리의 src / draw WebGL 경로처럼 별도의 렌더러가 필요합니다.

상황에 맞는 라이브러리
선택적합한 용도와 제약
@samasante/liquid-glass이 예제 · 0.1.1React에서 직접 관리하는 장면. Safari·Firefox의 굴절에는 refract로 복사본을 전달합니다. 단순히 <Glass>로 감싸는 것만으로는 충분하지 않습니다.
simple-liquid-glassReact 또는 Web Component. LiquidGlassScene / backdropRef를 연결해 WebGL 경로를 사용합니다. DOM 캡처 비용과 CORS·영상 제약을 확인하세요.
liquid-glass-reactChromium 중심의 간단한 효과. 저장소에서 Safari·Firefox의 굴절 제한을 명시하므로, 세 엔진의 굴절이 필요한 이번 예제의 기본값으로 사용하지 않았습니다.

문서는 정적 HTML로 유지하고 조작 예제만 React 19로 올렸습니다. 긴 문서라면 Astro의 React 아일랜드, 기존 React 서비스라면 독립된 컴포넌트로 같은 장면을 분리하면 됩니다. 아래에는 WebKit 내부 속성의 지원 여부도 별도로 표시합니다. 브라우저별로 굴절을 구현하는 것이 목표이며, Apple 네이티브 재질과 픽셀 단위로 동일한 효과를 보장하는 것은 아닙니다.

검증: Chromium 151, Playwright WebKit 26.5, Firefox 153에서 굴절 변화·스크롤 끝 여백·390px 화면을 확인했습니다. 실제 iPhone Duo Safari의 기기별 성능과 네이티브 재질은 별도 확인이 필요합니다.

WebKit material
불투명 폴백을 기본으로 사용합니다.

JavaScript가 켜져 있으면 이 환경의 CSS 지원 여부를 표시합니다.

WebKit 비공개 속성 예제 보기
glass.css
.toolbar {
  color: #202020;
  background: #f7f7f7; /* Opaque fallback. */
  border: 1px solid #dedede;
  border-radius: 24px;
}

/* Private WebKit material; gated by the host setting. */
@supports (-apple-visual-effect: -apple-system-glass-material) {
  .toolbar {
    background: transparent;
    -apple-visual-effect: -apple-system-glass-material;
  }
}

@media (prefers-reduced-transparency: reduce) {
  .toolbar {
    -apple-visual-effect: none;
    background: #f7f7f7;
  }
}

위 데모는 지원 환경에서는 WebKit 재질을, 그 외 환경에서는 라이브러리의 SVG 굴절을 선택합니다. 투명도 감소 설정이나 ‘불투명 비교’를 켜면 효과를 줄입니다. @supports는 속성·값의 허용 여부를 검사하며, 실제 시스템 재질 렌더링까지 보장하지는 않습니다.

지원하는 것처럼 보이는 데모가 있는 이유

WKWebView를 호스팅하는 앱이 비공개 설정을 켠 실험과, 일반 Safari 페이지를 구분해야 합니다. WebKit 구현의 기본값은 false입니다. 웹의 CSS나 JavaScript로 이 호스트 설정을 켤 수는 없습니다. [9]

디자인 원칙은 참고할 수 있습니다. 재질은 본문 전체보다 조작·내비게이션 레이어에 한정하고, 투명도 감소 설정과 다양한 배경에서도 글자가 읽히는지 확인하세요. 이 문서는 private 설정을 켜는 앱 코드를 배포 예제로 제공하지 않습니다. [10]

05 직접 확인할 문서들.

이미지 예제 사진: ArtHouse Studio / Pexels. 첨부하신 Apple 화면은 패턴 분석에만 참고했습니다.

2026년 10월 23일에 공식 문서·라이브러리 문서와 WebKit 소스를 확인했습니다. 디자인 예제는 문서의 원칙을 웹에 적용한 해석이며, Apple의 공식 웹 컴포넌트가 아닙니다.