๐ŸŒ™

iOS ์›น๋ทฐ ์ƒ๋‹จ ๋…ธ์น˜ ๋Œ€์‘ํ•˜๊ธฐ

yeggrrr๐Ÿผ 2025. 11. 7. 16:24
728x90

 


 

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 ์ดํ›„ ๋ชจ๋“  ๊ธฐ๊ธฐ์—์„œ ์•ˆ์ •์ ์ธ ๋ ˆ์ด์•„์›ƒ์„ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ๋‹ค.
 


 

728x90