모바일 최적화¶
라이믹스 레이아웃에서 모바일 환경을 체크하고 최적화하는 방법을 학습합니다.
모바일 체크¶
디바이스 감지¶
<!-- 모바일 체크하기 -->
{@
// 라이믹스 모바일 체크 함수
$is_mobile = Mobile::isMobile();
$is_tablet = Mobile::isTablet();
$is_desktop = !$is_mobile && !$is_tablet;
// 디바이스별 클래스
$device_class = '';
if($is_mobile) {
$device_class = 'device-mobile';
} elseif($is_tablet) {
$device_class = 'device-tablet';
} else {
$device_class = 'device-desktop';
}
// 특정 모바일 기기 체크
$user_agent = $_SERVER['HTTP_USER_AGENT'];
$is_ios = preg_match('/iPhone|iPad|iPod/i', $user_agent);
$is_android = preg_match('/Android/i', $user_agent);
$is_samsung = preg_match('/SAMSUNG|SM-/i', $user_agent);
}
<!-- body 태그에 디바이스 클래스 추가 -->
<body class="{$device_class} os-{$is_ios ? 'ios' : ($is_android ? 'android' : 'other')}">
<!-- 모바일 전용 콘텐츠 -->
<div cond="$is_mobile" class="mobile-only">
<!-- 모바일 전용 네비게이션 -->
<nav class="mobile-nav">
<button type="button" class="nav-toggle">
<i class="xi-bars"></i>
</button>
</nav>
</div>
<!-- 데스크탑 전용 콘텐츠 -->
<div cond="$is_desktop" class="desktop-only">
<!-- 데스크탑 사이드바 -->
<aside class="desktop-sidebar">
<!-- 사이드바 콘텐츠 -->
</aside>
</div>
<!-- 반응형 콘텐츠 -->
<div class="responsive-content">
<!-- 모든 디바이스에서 표시 -->
</div>
</body>
웹뷰 체크¶
앱 내장 브라우저 감지¶
<!-- 웹뷰 여부 체크하기 (PHP) -->
{@
// User-Agent로 웹뷰 체크
$user_agent = $_SERVER['HTTP_USER_AGENT'];
// 일반적인 웹뷰 패턴
$webview_patterns = array(
'/wv/', // Android WebView
'/WebView/',
'/Version\/[\d\.]+.*Safari/', // iOS WebView
'/Line/', // Line 앱
'/FB/', // Facebook 앱
'/Instagram/', // Instagram 앱
'/KAKAO/', // 카카오톡
'/NAVER/', // 네이버 앱
);
$is_webview = false;
foreach($webview_patterns as $pattern) {
if(preg_match($pattern, $user_agent)) {
$is_webview = true;
break;
}
}
// 안드로이드 웹뷰 상세 체크
$is_android_webview = false;
if(preg_match('/Android/', $user_agent)) {
if(preg_match('/wv/', $user_agent) ||
(preg_match('/Version\/[\d\.]+/', $user_agent) && !preg_match('/Chrome\/[\d\.]+/', $user_agent))) {
$is_android_webview = true;
}
}
}
<!-- 웹뷰 전용 처리 -->
<div cond="$is_webview" class="webview-notice">
<p>앱에서 보고 계십니다. 더 나은 경험을 위해 브라우저에서 열어보세요.</p>
<button onclick="openInBrowser()">브라우저에서 열기</button>
</div>
<script>
// JavaScript로 웹뷰 체크
function isWebView() {
var userAgent = navigator.userAgent || navigator.vendor || window.opera;
// iOS 웹뷰
if(/iPhone|iPod|iPad/.test(userAgent)) {
// Safari 브라우저가 아닌 경우
if(!/Safari/.test(userAgent) || /CriOS/.test(userAgent)) {
return true;
}
// standalone 모드
if(window.navigator.standalone) {
return true;
}
}
// Android 웹뷰
if(/Android/.test(userAgent)) {
if(/wv/.test(userAgent) ||
(/Version\/[\d\.]+/.test(userAgent) && !/Chrome\/[\d\.]+/.test(userAgent))) {
return true;
}
}
// 기타 앱 내장 브라우저
if(/FBAN|FBAV|Instagram|Line|KAKAO|NAVER/.test(userAgent)) {
return true;
}
return false;
}
// 브라우저에서 열기
function openInBrowser() {
var currentUrl = window.location.href;
// iOS
if(/iPhone|iPod|iPad/.test(navigator.userAgent)) {
window.location = 'x-web-search://?url=' + encodeURIComponent(currentUrl);
}
// Android
else if(/Android/.test(navigator.userAgent)) {
window.location = 'intent:' + currentUrl + '#Intent;action=android.intent.action.VIEW;category=android.intent.category.BROWSABLE;end';
}
// 기타
else {
alert('브라우저에서 직접 열어주세요: ' + currentUrl);
}
}
</script>
안드로이드 푸시앱 모듈 변수 활용¶
<!-- 웹뷰 여부 체크하기(안드로이드푸시앱모듈 변수 활용) -->
{@
// 안드로이드 푸시앱 모듈이 설치된 경우
if(getModel('module')->getModuleConfig('androidpushapp')) {
$androidpushapp_config = getModel('module')->getModuleConfig('androidpushapp');
$is_pushapp = Context::get('is_pushapp') == 'Y';
// 푸시앱 버전 정보
$pushapp_version = Context::get('pushapp_version');
$pushapp_os = Context::get('pushapp_os'); // android, ios
}
}
<!-- 푸시앱 전용 기능 -->
<div cond="$is_pushapp" class="pushapp-features">
<!-- 푸시앱 전용 헤더 -->
<header class="pushapp-header">
<button onclick="pushappBack()">
<i class="xi-arrow-left"></i>
</button>
<h1>{$layout_info->site_title}</h1>
<button onclick="pushappShare()">
<i class="xi-share-alt"></i>
</button>
</header>
<!-- 푸시앱 전용 하단 메뉴 -->
<nav class="pushapp-bottom-nav">
<a href="#" onclick="pushappGoHome()">
<i class="xi-home"></i>
<span>홈</span>
</a>
<a href="#" onclick="pushappOpenMenu()">
<i class="xi-apps"></i>
<span>메뉴</span>
</a>
<a href="#" onclick="pushappOpenNotification()">
<i class="xi-bell"></i>
<span>알림</span>
</a>
<a href="#" onclick="pushappOpenMypage()">
<i class="xi-user"></i>
<span>MY</span>
</a>
</nav>
</div>
<script>
// 푸시앱 전용 함수들
function pushappBack() {
if(window.AndroidPushApp) {
window.AndroidPushApp.goBack();
} else if(window.webkit && window.webkit.messageHandlers.goBack) {
window.webkit.messageHandlers.goBack.postMessage('');
} else {
history.back();
}
}
function pushappShare() {
var shareData = {
title: document.title,
text: document.querySelector('meta[name="description"]').content,
url: window.location.href
};
if(window.AndroidPushApp) {
window.AndroidPushApp.share(JSON.stringify(shareData));
} else if(navigator.share) {
navigator.share(shareData);
}
}
function pushappGoHome() {
if(window.AndroidPushApp) {
window.AndroidPushApp.goHome();
} else {
location.href = '/';
}
}
</script>
반응형 레이아웃¶
모바일 우선 접근¶
<!-- 반응형 네비게이션 -->
<header class="site-header">
<!-- 모바일 헤더 -->
<div class="mobile-header">
<button type="button" class="menu-toggle" aria-label="메뉴 열기">
<span class="hamburger">
<span></span>
<span></span>
<span></span>
</span>
</button>
<h1 class="mobile-logo">
<a href="{getUrl('')}">{$layout_info->site_title}</a>
</h1>
<button type="button" class="search-toggle" aria-label="검색">
<i class="xi-search"></i>
</button>
</div>
<!-- 데스크탑/모바일 공통 네비게이션 -->
<nav class="main-navigation" id="navigation">
<ul class="nav-menu">
<li loop="$main_menu->list=>$key,$val" class="menu-item">
<a href="{$val['url']}" target="_blank"|cond="$val['open_window']=='Y'">
{$val['text']}
</a>
<!-- 서브메뉴 -->
<ul cond="$val['list']" class="sub-menu">
<li loop="$val['list']=>$key2,$val2">
<a href="{$val2['url']}">{$val2['text']}</a>
</li>
</ul>
</li>
</ul>
</nav>
<!-- 모바일 오버레이 -->
<div class="mobile-overlay"></div>
</header>
<style>
/* 모바일 우선 CSS */
.site-header {
position: relative;
}
.mobile-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
background: #fff;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.main-navigation {
position: fixed;
top: 0;
left: -280px;
width: 280px;
height: 100vh;
background: #fff;
transition: left 0.3s;
overflow-y: auto;
z-index: 1000;
}
.main-navigation.active {
left: 0;
}
.mobile-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
display: none;
z-index: 999;
}
.mobile-overlay.active {
display: block;
}
/* 태블릿 이상 */
@media (min-width: 768px) {
.mobile-header {
display: none;
}
.main-navigation {
position: static;
width: auto;
height: auto;
background: none;
}
.nav-menu {
display: flex;
list-style: none;
margin: 0;
padding: 0;
}
.menu-item {
position: relative;
}
.sub-menu {
position: absolute;
top: 100%;
left: 0;
background: #fff;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
display: none;
}
.menu-item:hover .sub-menu {
display: block;
}
}
/* 데스크탑 */
@media (min-width: 1200px) {
.site-header {
max-width: 1200px;
margin: 0 auto;
}
}
</style>
<script>
// 모바일 메뉴 토글
document.addEventListener('DOMContentLoaded', function() {
var menuToggle = document.querySelector('.menu-toggle');
var navigation = document.querySelector('.main-navigation');
var overlay = document.querySelector('.mobile-overlay');
menuToggle.addEventListener('click', function() {
navigation.classList.toggle('active');
overlay.classList.toggle('active');
this.classList.toggle('active');
});
overlay.addEventListener('click', function() {
navigation.classList.remove('active');
overlay.classList.remove('active');
menuToggle.classList.remove('active');
});
// 화면 크기 변경시 메뉴 닫기
var resizeTimer;
window.addEventListener('resize', function() {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(function() {
if(window.innerWidth >= 768) {
navigation.classList.remove('active');
overlay.classList.remove('active');
menuToggle.classList.remove('active');
}
}, 250);
});
});
</script>
터치 최적화¶
터치 이벤트 처리¶
<!-- 터치 친화적인 UI -->
<style>
/* 터치 타겟 크기 최적화 */
.touch-target {
min-width: 44px;
min-height: 44px;
display: inline-flex;
align-items: center;
justify-content: center;
}
/* 터치 피드백 */
.touchable {
-webkit-tap-highlight-color: rgba(0,0,0,0.1);
touch-action: manipulation; /* 더블탭 줌 방지 */
}
.touchable:active {
transform: scale(0.95);
opacity: 0.8;
}
/* 스와이프 가능한 영역 */
.swipeable {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scroll-snap-type: x mandatory;
}
.swipeable::-webkit-scrollbar {
display: none;
}
.swipe-item {
scroll-snap-align: start;
}
</style>
<script>
// 터치 스와이프 감지
class TouchSwipe {
constructor(element, options = {}) {
this.element = element;
this.options = Object.assign({
threshold: 50,
onSwipeLeft: null,
onSwipeRight: null,
onSwipeUp: null,
onSwipeDown: null
}, options);
this.touchStartX = 0;
this.touchStartY = 0;
this.touchEndX = 0;
this.touchEndY = 0;
this.init();
}
init() {
this.element.addEventListener('touchstart', this.handleTouchStart.bind(this), {passive: true});
this.element.addEventListener('touchmove', this.handleTouchMove.bind(this), {passive: true});
this.element.addEventListener('touchend', this.handleTouchEnd.bind(this), {passive: true});
}
handleTouchStart(e) {
this.touchStartX = e.changedTouches[0].screenX;
this.touchStartY = e.changedTouches[0].screenY;
}
handleTouchMove(e) {
this.touchEndX = e.changedTouches[0].screenX;
this.touchEndY = e.changedTouches[0].screenY;
}
handleTouchEnd() {
this.handleSwipe();
}
handleSwipe() {
const deltaX = this.touchEndX - this.touchStartX;
const deltaY = this.touchEndY - this.touchStartY;
if(Math.abs(deltaX) > Math.abs(deltaY)) {
// 좌우 스와이프
if(deltaX > this.options.threshold && this.options.onSwipeRight) {
this.options.onSwipeRight();
} else if(deltaX < -this.options.threshold && this.options.onSwipeLeft) {
this.options.onSwipeLeft();
}
} else {
// 상하 스와이프
if(deltaY > this.options.threshold && this.options.onSwipeDown) {
this.options.onSwipeDown();
} else if(deltaY < -this.options.threshold && this.options.onSwipeUp) {
this.options.onSwipeUp();
}
}
}
}
// 사용 예시
document.addEventListener('DOMContentLoaded', function() {
// 이미지 갤러리 스와이프
var gallery = document.querySelector('.image-gallery');
if(gallery) {
new TouchSwipe(gallery, {
onSwipeLeft: function() {
// 다음 이미지
nextImage();
},
onSwipeRight: function() {
// 이전 이미지
prevImage();
}
});
}
// 모바일 메뉴 스와이프
var menuSwipe = new TouchSwipe(document.body, {
threshold: 100,
onSwipeRight: function() {
// 메뉴 열기
if(window.innerWidth < 768) {
document.querySelector('.main-navigation').classList.add('active');
}
},
onSwipeLeft: function() {
// 메뉴 닫기
if(window.innerWidth < 768) {
document.querySelector('.main-navigation').classList.remove('active');
}
}
});
});
</script>
성능 최적화¶
모바일 리소스 최적화¶
<!-- 조건부 리소스 로딩 -->
{@
// 디바이스별 리소스 설정
if($is_mobile) {
$css_files = array('/layouts/my_layout/css/mobile.min.css');
$js_files = array('/layouts/my_layout/js/mobile.min.js');
$image_quality = 70;
} else {
$css_files = array('/layouts/my_layout/css/desktop.min.css');
$js_files = array('/layouts/my_layout/js/desktop.min.js');
$image_quality = 90;
}
}
<!-- CSS 로딩 -->
<link loop="$css_files=>$css_file" rel="stylesheet" href="{$css_file}" />
<!-- 크리티컬 CSS 인라인 -->
<style>
/* 초기 렌더링에 필요한 최소 CSS */
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
.site-header { background: #fff; box-shadow: 0 2px 5px rgba(0,0,0,0.1); }
</style>
<!-- 나머지 CSS 비동기 로딩 -->
<link rel="preload" href="{$css_files[0]}" as="style" onload="this.onload=null;this.rel='stylesheet'">
<!-- JavaScript 지연 로딩 -->
<script>
// 필수 스크립트만 먼저 로드
document.addEventListener('DOMContentLoaded', function() {
// 비필수 스크립트 지연 로딩
setTimeout(function() {
var scripts = {$js_files|@json_encode};
scripts.forEach(function(src) {
var script = document.createElement('script');
script.src = src;
script.async = true;
document.body.appendChild(script);
});
}, 1000);
});
</script>
<!-- 이미지 최적화 -->
{@
// 반응형 이미지 생성
function getResponsiveImage($src, $alt = '') {
$srcset = array();
$sizes = array(320, 640, 768, 1024, 1200);
foreach($sizes as $size) {
$srcset[] = getThumbnail($src, $size) . ' ' . $size . 'w';
}
return sprintf(
'<img src="%s" srcset="%s" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" alt="%s" loading="lazy" />',
getThumbnail($src, 768),
implode(', ', $srcset),
$alt
);
}
}
모범 사례¶
- 터치 타겟: 최소 44x44px 크기 유지
- 폰트 크기: 모바일에서 최소 16px
- 뷰포트: 확대/축소 가능하게 설정
- 성능: 불필요한 리소스 제거
- 테스트: 실제 기기에서 테스트
다음 단계¶
모바일 최적화를 완료했다면, 고급 레이아웃 기법을 학습하세요.