iPhone X ์ดํ ์ถ์๋ ๊ธฐ๊ธฐ๋ค์ ๋ชจ๋ ๋
ธ์น(Notch) ์์ญ์ ๊ฐ์ง๊ณ ์๋ค.
์ด ์์ญ์ ์ํ๋ฐ(Status Bar)์ ์ผ์ ํ์ฐ์ง ๋๋ฌธ์ ํ๋ฉด์ ์์ ํ์ ์์ญ(SafeArea)์ด ๋ฌ๋ผ์ก์์ ์๋ฏธํ๋ค.
์ฑ์ ์น๋ทฐ ๊ธฐ๋ฐ์ผ๋ก ๊ตฌ์ฑํ๋ค๋ฉด,
์ด ์ฐจ์ด๋ฅผ ๋ช
ํํ ์ฒ๋ฆฌํ์ง ์์ผ๋ฉด ์น ์ฝํ
์ธ ๊ฐ ๋
ธ์น์ ๊ฐ๋ ค์ง๊ฑฐ๋ ์๋จ์ด ์๋ฆฌ๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์๋ค.
์ด๋ฒ ๊ธ์์๋ WKWebView ๊ธฐ๋ฐ ์ฑ์์ ์๋จ ๋
ธ์น ์์ ์์ญ์ ์ฌ๋ฐ๋ฅด๊ฒ ์ฒ๋ฆฌํ๋ ๋ฐฉ๋ฒ์ ์ ๋ฆฌํ๋ ค๊ณ ํ๋ค.
๋ฌธ์ ์ํฉ - ์น๋ทฐ๋ฅผ ์ ์ฒด ํ๋ฉด์ผ๋ก ์ฑ์ฐ๊ธฐ
webView.snp.makeConstraints {
$0.edges.equalToSuperview()
}์ด๋ ๊ฒ ํ๋ฉด iPhone 15 Pro์ ๊ฐ์ ๊ธฐ๊ธฐ์์ ์น ์ฝํ
์ธ ๊ฐ ๋
ธ์น ์์ญ ๋ฐ์ผ๋ก ๋ค์ด๊ฐ ๋ฒ๋ฆฐ๋ค.
๊ฒฐ๊ณผ์ ์ผ๋ก HTML์ <header>์์๊ฐ ์๋ฆฌ๊ฑฐ๋, ์๋จ ๋ฒํผ์ด ๋๋ฆฌ์ง ์๋ ๋ฌธ์ ๊ฐ ์๊ธด๋ค.
์ฌ๋ฐ๋ฅธ SafeArea ์ ์ฉ
๊ฐ์ฅ ๊ฐ๋จํ ๋ฐฉ๋ฒ์ SafeAreaLayoutGuide๋ฅผ ๊ธฐ์ค์ผ๋ก ์ ์ฝ์ ์ค์ ํ๋ ๊ฒ์ด๋ค.
webView.snp.makeConstraints {
$0.top.equalTo(view.safeAreaLayoutGuide)
$0.horizontalEdges.equalToSuperview()
$0.bottom.equalToSuperview()
}์ด๋ ๊ฒ ํ๋ฉด ์๋จ ๋
ธ์น, ํ๋จ ํ ์ธ๋์ผ์ดํฐ๋ฅผ ๋ชจ๋ ํผํด์ ์น๋ทฐ๊ฐ ์์ ์์ญ ์์ ๋ฐฐ์น๋๋ค.
ํ์ง๋ง, ์น์ฑ์ด ์์ฒด์ ์ผ๋ก safeArea๋ฅผ ์ฒ๋ฆฌํ๋ ๊ตฌ์กฐ๋ผ๋ฉด,
์ด ์ค์ ์ ์คํ๋ ค ์ด์ค ์ฌ๋ฐฑ์ ๋ง๋ค์ด ๋ ์ด์์์ด ์ด์ํด์ง ์ ์๋ค.
์น์ฑ์ด SafeArea๋ฅผ ์ง์ ์ฒ๋ฆฌํ๋ ๊ฒฝ์ฐ
์์ฆ SPA(React, Next.js ๊ธฐ๋ฐ) ๊ตฌ์กฐ์ ์น์ฑ๋ค์ CSS ํ๊ฒฝ์์ ์ด๋ฏธ safeArea ๋์์ด ๋์ด์๋ค.
์๋ฅผ ๋ค์ด, ์น CSS์ ๋ค์๊ณผ ๊ฐ์ด ์ ์๋์ด ์๋ค๋ฉด!
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);์ด ๊ฒฝ์ฐ ๋ค์ดํฐ๋ธ ์ชฝ์์ ๊ตณ์ด safeArea๋ฅผ ๊ณ ๋ คํ ํ์๊ฐ ์๋ค.
์คํ๋ ค safeAreaLayoutGuide๋ฅผ ์ ์ฉํ๋ฉด ์๋จ ์ฌ๋ฐฑ์ด ๋ ๋ฐฐ๋ก ์ฆ๊ฐํ๊ฒ ๋๋ค.
๋ฐ๋ผ์ ์ด๋ฐ ๊ฒฝ์ฐ์๋ ์๋์ ๊ฐ์ด ์ ์ฒด ํ๋ฉด์ ์น๋ทฐ๊ฐ ๋ฎ๋๋ก ๊ตฌ์ฑํ๋๊ฒ ๋ง๋ค.
webView.snp.makeConstraints {
$0.edges.equalToSuperview()
}์ฆ, ์น์ฑ์ด SafeArea๋ฅผ ๋ด๋นํ ๋๋ ๋ค์ดํฐ๋ธ๊ฐ ๊ฐ์ญํ์ง ์๋๋ค๋ ์์น์ ์ง์ผ์ผ ํ๋ค.
+ ์น์์ ํ๋ iOS ๋
ธ์น ๋์ ๊ฐ์ด๋
1) ํ์ ์ ์ : viewport-fit=cover
iOS Safari์์ ๋
ธ์นโํ ์ธ๋์ผ์ดํฐ๊น์ง ์ ์ฒด ํ๋ฉด์ ์น์ด ์ฐจ์งํ๋ ค๋ฉด ๋ทฐํฌํธ ์ ์ธ์ด ํ์ํ๋ค.
<meta name="viewport"
content="width=device-width, initial-scale=1, viewport-fit=cover" />์ด ์ค์ ์ด ์์ผ๋ฉด env(safe-area-inset-*)๊ฐ ๊ธฐ๋ํ๋๋ก ๋์ํ์ง ์๊ฑฐ๋,
์๋จ/ํ๋จ์ด ๋ธ๋ผ์ฐ์ UI์ ์ํด ์๋ฆด ์ ์๋ค.
WKWebView์์๋ ๋์ผํ๊ฒ ๋ฃ์ด๋๋ ํธ์ด ์์ ํ๋ค.
2) CSS ๊ธฐ๋ณธ ํจํด (๊ถ์ฅ)
๊ธฐ๋ณธ์ ์ผ๋ก ์โํ ์์ ์์ญ์ ํจ๋ฉ์ผ๋ก ํก์ํ๋ค.
๋
ธ์น ์๋ ๊ธฐ๊ธฐ๋ 0์ด ๋ฐํ๋๋ฏ๋ก ๋์ผ CSS๋ฅผ ๊ณต์ฉ์ผ๋ก ์จ๋ ๋ฌด๋ฐฉํ๋ค.
/* iOS 11.x ๊ตฌ๋ฒ์ (WebKit ํ๋ฆฌํฝ์ค)๊น์ง ํธํ */
:root {
/* ์ ํ: ์ปค์คํ
๋ณ์๋ก ์ฌ๋
ธ์ถํด๋๋ฉด ์ฌ์ฌ์ฉ์ด ํธํจ */
--sat: env(safe-area-inset-top);
--sab: env(safe-area-inset-bottom);
--sal: env(safe-area-inset-left);
--sar: env(safe-area-inset-right);
}
/* ํ์ด์ง ์ ์ฒด๋ฅผ ๊ฐ์ธ๋ ์ต์์ ๋ํผ(๋๋ body)์ ์ ์ฉ */
.safe-area {
padding-top: env(safe-area-inset-top);
padding-right: env(safe-area-inset-right);
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
/* ๊ตฌํ iOS ๋๋น์ฉ(์์ด๋ ๋ฌดํด) */
padding-top: constant(safe-area-inset-top);
padding-right: constant(safe-area-inset-right);
padding-bottom: constant(safe-area-inset-bottom);
padding-left: constant(safe-area-inset-left);
}์ด๋์ ๋ถ์ฌ์ผํ๋๋?
์น์ด ์ ์ฒดํ๋ฉด์ ์ง์ ๋ด๋นํ๋ ๊ตฌ์กฐ๋ผ๋ฉด,
body ๋๋ ์ต์์ ๋ ์ด์์ ์ปดํฌ๋ํธ(ex. #root, <App/>๋ํผ)์ .safe-area๋ฅผ ๋ถ์ฌํ๋ค.
์๋จ ๊ณ ์ ๋ฐ(AppBar)โํ๋จ ํญ๋ฐ๊ฐ ์น ๋ด๋ถ์ ์๋ค๋ฉด,
ํด๋น ์ปดํฌ๋ํธ์๋ง ์ ํ์ ์ผ๋ก padding-top/bottom์ ์ ์ฉํด๋ ๋๋ค.
(ex. .app-bar { padding-top: var(--sat); }, .tabbar { padding-bottom: var(--sab); }
3) ์๋จ/ํ๋จ ๊ณ ์ ์์(position: fixed)์ฒ๋ฆฌ
๋
ธ์นโํ ์ธ๋์ผ์ดํฐ์ ๊ฒน์น์ง ์๋๋ก ๊ณ ์ ๋ฐ์ inset์ ๋ํ๋ค.
.app-bar {
position: sticky;
top: 0;
padding-top: var(--sat);
height: calc(56px + var(--sat));
}
.bottom-tab {
position: sticky;
bottom: 0;
padding-bottom: var(--sab);
height: calc(64px + var(--sab));
}fixed๋ฅผ ์จ์ผ ํ๋ค๋ฉด?
.app-bar {
position: fixed; top: 0; left: 0; right: 0;
padding-top: var(--sat);
height: calc(56px + var(--sat));
}
4) ๋ฐฐ๊ฒฝ์โ์คํฌ๋กค ๋ฐ์ด์ค ๋๋น
iOS์์ ์ค๋ฒ์คํฌ๋กค(๋ฐ์ด์ค) ์ ์์ ์์ญ ๋ฐ๊น์ง ๋ฐฐ๊ฒฝ์ด ๋น์ณ๋ณด์ผ ์ ์๋ค.
body์ ๋ฃจํธ ์ปจํ
์ด๋์ ๋์ผํ ๋ฐฐ๊ฒฝ์์ ์ง์ ํด ํ์ด ๋ณด์ด์ง ์๊ฒ ํด์ผํ๋ค.
html, body, #root {
background: #fff; /* ์ฑ ๋ฐฐ๊ฒฝ์๊ณผ ๋์ผ */
min-height: 100%;
}
5) vh ์ด์: ๋ชจ๋ฐ์ผ Safari ํด๋ฐ ๋์ด ๋ณํ
iOS Safari๋ ์โํ๋จ ํด๋ฐ ๋
ธ์ถ์ ๋ฐ๋ผ 100vh๊ฐ ํ๋ค๋ฆด ์ ์๋ค.
iOS 15+๋ถํฐ๋ svh/dvh/lvh ๋จ์๋ฅผ ํ์ฉํ๋ฉด ์์ ์ ์ด๋ค.
/* ๊ถ์ฅ: 100dvh ๋๋ 100svh */
.main-full {
min-height: 100dvh; /* ๋์ ๋ทฐํฌํธ, ํด๋ฐ ๋ณํ์ ๋ฐ์ */
}
/* ๊ตฌ๋ฒ์ ํด๋ฐฑ */
@supports not (height: 100dvh) {
.main-full { min-height: 100vh; }
}
6) React/SPA ๋ ์ด์์ ์์
์น์ด SafeArea๋ฅผ ์ฑ
์์ง๋ ์ผ์ด์ค(๋ค์ดํฐ๋ธ์์๋ ์ ์ฒดํ๋ฉด์ผ๋ก๋ง)
// AppLayout.tsx
export function AppLayout({ children }: { children: React.ReactNode }) {
return (
<div className="safe-area app-shell">
<header className="app-bar">
{/* ์ข/์ฐ ๋
ธ์น ๊ณ ๋ ค ํ์ ์ padding-left/right ๋ ๊ฐ๋ฅ */}
<h1>Title</h1>
</header>
<main className="content">
{children}
</main>
<nav className="bottom-tab">
{/* ํญ ์์ดํ
*/}
</nav>
</div>
);
}.app-shell {
min-height: 100dvh;
display: grid;
grid-template-rows: auto 1fr auto;
}
.app-bar {
padding-top: var(--sat);
height: calc(56px + var(--sat));
display: flex; align-items: center;
border-bottom: 1px solid #eee;
background: #fff;
}
.bottom-tab {
padding-bottom: var(--sab);
height: calc(64px + var(--sab));
border-top: 1px solid #eee;
background: #fff;
}
.content {
/* ์ข์ฐ ๋
ธ์น ๋์ ํ์ํ๋ค๋ฉด ์๋ ์ฃผ์ ํด์
padding-left: var(--sal);
padding-right: var(--sar);
*/
overflow: auto;
}
7) ๋์์โ๊ฐ๋ก๋ชจ๋ ํน์ ํ๋ฉด
์ ์ฒดํ๋ฉด(Fullscreen) ๋น๋์ค๋ ๋ธ๋ผ์ฐ์ /OS๊ฐ ๋ณ๋ ๋ ์ด์ด๋ก ์ฒ๋ฆฌํ๋ฏ๋ก ๋๊ฐ ์ถ๊ฐ๋ก SafeArea ๋ณด์ ์ด ๋ถํ์ํ๋ค.
๊ฐ๋ก๋ชจ๋๋ก ์ ํ๋๋ ์ ์ฉ ํ๋ฉด์ด๋ผ๋ฉด, ์ข/์ฐ inset(--sal, --sar)๊น์ง ๊ณ ๋ คํด ์กฐ์ ๋ฒํผ์ด ๋
ธ์น์ ๊ฒน์น์ง ์๊ฒ ํ๋ค.
@media (orientation: landscape) {
.player-controls {
padding-left: var(--sal);
padding-right: var(--sar);
}
}
์๋จ ๋ค์ดํฐ๋ธ UI๊ฐ ์๋ ๊ฒฝ์ฐ
์๋จ์ ๋ฐฑ๋ฒํผ, ์ ๋ชฉ๋ฐ, ํญ๋ฐ ๋ฑ์ ๋ค์ดํฐ๋ธ UI๊ฐ ์กด์ฌํ๋ค๋ฉด,
ํด๋น UI๋ฅผ ๊ธฐ์ค์ผ๋ก ์น๋ทฐ์ top์ ๋ง์ถฐ์ผ ํ๋ค.
webView.snp.makeConstraints {
$0.top.equalTo(backButtonSuperView.snp.bottom)
$0.horizontalEdges.equalToSuperview()
$0.bottom.equalToSuperview()
}์ด๋ ๊ฒ ํ๋ฉด ๋
ธ์น ์์ญ์ backButtonSuperView๊ฐ ์ฐจ์งํ๊ณ ,
์น๋ทฐ๋ ๊ทธ ์๋๋ถํฐ ์ฝํ
์ธ ๋ฅผ ํ์ํ๊ฒ ๋๋ค.
์์ ์์ญ ๋์ด ์ง์ ๊ณ์ฐํ๊ธฐ (ํน์ ์ผ์ด์ค)
๊ธฐ๊ธฐ๋ณ ์๋จ ์์ ์์ญ ๋์ด๋ฅผ ์ง์ ๊ณ์ฐํด์ผ ํ๋ ๊ฒฝ์ฐ๋ ์๋ค.
์๋ฅผ ๋ค์ด, ๋ฐฑ๋ฒํผ ์์ญ์ safeArea ๋์ด + 50pt๋ก ๋ง์ถฐ์ผ ํ๋ค๋ฉด ์๋์ ๊ฐ์ด ๊ตฌํํ ์ ์๋ค.
let window = UIApplication.shared.windows.first
let topInset = window?.safeAreaInsets.top ?? 0
let backBarHeight = topInset + 50์ด ๊ฐ์ ์ ์ฝ ์กฐ๊ฑด์ ๋ฐ์ํ๋ฉด, ๊ธฐ๊ธฐ๋ณ ๋
ธ์น ์ฐจ์ด์ ๋ฐ๋ผ ์ ์ฐํ๊ฒ ๋์ํ ์ ์๋ค.
ํต์ฌ์ ํ๋๋ค.
'์น๋ทฐ์ safeArea์ฒ๋ฆฌ๋ "์น์ด ํ๋๋, ์ฑ์ด ํ๋๋"๋ฅผ ๋จผ์ ๊ฒฐ์ ํ๋ ๊ฒ.'
์ด ์์น๋ง ๋ช
ํํ ํด๋๋ฉด, iPhone X ์ดํ ๋ชจ๋ ๊ธฐ๊ธฐ์์ ์์ ์ ์ธ ๋ ์ด์์์ ์ ์งํ ์ ์๋ค.