«   2026/04   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
Recent Posts
Today
Total
관리 메뉴

푸른들소프트

웹앱에서 네이티브 앱 패키징 시 성능 병목 찾아내기 본문

개발 노트/INSIGHT

웹앱에서 네이티브 앱 패키징 시 성능 병목 찾아내기

푸른들소프트 2025. 12. 2. 10:30

 

 

 

들어가며

 

React로 개발한 웹앱을 Kotlin이나 Swift로

네이티브 앱에 웹뷰로 감싸서 배포하는 방식은

많은 팀들이 선택하고 있습니다.

 

개발 속도도 빠르고 유지보수도 효율적입니다.

 

하지만 여기에는 한 가지 중요한 과제가 따라옵니다.

 

"로딩 속도가 느립니다"

 

푸른들소프트 역시 마찬가지였습니다.

React 앱을 웹뷰로 패키징했는데,

사용자가 앱 아이콘을 터치하고 나서

실제로 사용 가능한 화면이 나오기까지

5초가 넘게 걸리는 경우가 빈번했습니다.

 

이는 사용자들이

"앱이 정상 작동하지 않는가?" 하고

의심할 만한 시간입니다.

 

푸른들소프트가 이 문제를

네이티브와 웹 양쪽에서 체계적으로 해결해나간

과정을 공유해드리고자 합니다.

 


 

성능 문제 이해하기

 

성능 문제를 해결하려면

우선 전체 로딩 과정을 정확히 파악해야 합니다.

 

네이티브 앱에서 React 웹앱이 실행되기까지는

다음 단계들을 거칩니다:

[네이티브] 앱 실행 → 웹뷰 초기화 → HTML 요청
    ↓
[웹뷰] HTML 파싱 → 리소스 다운로드
    ↓
[JavaScript] React 앱 시작 → 첫 화면 렌더링 → 초기 데이터 로딩
 

 

 

각 단계별로 병목이 어디서 발생하는지

네이티브와 웹 양쪽에서 측정해야 정확한 진단이 가능합니다.

 

 

 

 

성능 측정하기

 

네이티브 쪽 성능 측정

먼저 네이티브 앱에서 웹뷰가 초기화되고 페이지가 로드되는 시간을 측정해보겠습니다.

 

Android (Kotlin)

class WebViewPerformanceTracker(private val context: Context) {
    private var appStartTime = System.currentTimeMillis()
    private var webViewInitStartTime = 0L

    fun setupPerformanceTracking(webView: WebView) {
        webViewInitStartTime = System.currentTimeMillis()

        webView.webViewClient = object : WebViewClient() {
            override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
                val webViewInitTime = System.currentTimeMillis() - webViewInitStartTime
                val totalTime = System.currentTimeMillis() - appStartTime

                Log.d("Performance", "앱 시작 → 웹뷰 초기화: ${webViewInitTime}ms")
                Log.d("Performance", "앱 시작 → HTML 로딩 시작: ${totalTime}ms")

                // JavaScript로 데이터 전달
                view?.evaluateJavascript("""
                    window.nativePerformance = {
                        appStart: $appStartTime,
                        webViewInit: $webViewInitTime,
                        htmlLoadStart: ${System.currentTimeMillis()}
                    };
                """.trimIndent(), null)
            }

            override fun onPageFinished(view: WebView?, url: String?) {
                val htmlLoadTime = System.currentTimeMillis() - appStartTime
                Log.d("Performance", "앱 시작 → HTML 로딩 완료: ${htmlLoadTime}ms")

                view?.evaluateJavascript("""
                    window.nativePerformance.htmlLoadEnd = ${System.currentTimeMillis()};
                """.trimIndent(), null)
            }
        }

        // JavaScript에서 호출할 수 있는 인터페이스 추가
        webView.addJavascriptInterface(PerformanceInterface(), "Android")
    }

    inner class PerformanceInterface {
        @JavascriptInterface
        fun receivePerformanceData(data: String) {
            Log.d("Performance", "웹앱 성능 데이터: $data")
            // 서버로 전송하거나 로컬에 저장
        }
    }
}
 

iOS (Swift)

class WebViewPerformanceTracker: NSObject {
    private let appStartTime = CFAbsoluteTimeGetCurrent()
    private var webViewInitStartTime: CFAbsoluteTime = 0

    func setupPerformanceTracking(webView: WKWebView) {
        webViewInitStartTime = CFAbsoluteTimeGetCurrent()
        webView.navigationDelegate = self

        // JavaScript 메시지 핸들러 추가
        let contentController = webView.configuration.userContentController
        contentController.add(self, name: "performanceData")
    }
}

extension WebViewPerformanceTracker: WKNavigationDelegate {
    func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
        let webViewInitTime = (CFAbsoluteTimeGetCurrent() - webViewInitStartTime) * 1000
        let totalTime = (CFAbsoluteTimeGetCurrent() - appStartTime) * 1000

        print("앱 시작 → 웹뷰 초기화: \(webViewInitTime)ms")
        print("앱 시작 → HTML 로딩 시작: \(totalTime)ms")

        // JavaScript로 데이터 전달
        let script = """
            window.nativePerformance = {
                appStart: \(appStartTime * 1000),
                webViewInit: \(webViewInitTime),
                htmlLoadStart: \(CFAbsoluteTimeGetCurrent() * 1000)
            };
        """
        webView.evaluateJavaScript(script, completionHandler: nil)
    }

    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        let htmlLoadTime = (CFAbsoluteTimeGetCurrent() - appStartTime) * 1000
        print("앱 시작 → HTML 로딩 완료: \(htmlLoadTime)ms")

        let script = """
            window.nativePerformance.htmlLoadEnd = \(CFAbsoluteTimeGetCurrent() * 1000);
        """
        webView.evaluateJavaScript(script, completionHandler: nil)
    }
}

extension WebViewPerformanceTracker: WKScriptMessageHandler {
    func userContentController(_ userContentController: WKUserContentController,
                              didReceive message: WKScriptMessage) {
        if message.name == "performanceData" {
            print("웹앱 성능 데이터: \(message.body)")
            // 서버로 전송하거나 로컬에 저장
        }
    }
}
 

 

 

웹앱 쪽 성능 측정

이제 React 앱에서 JavaScript 실행부터 첫 화면 렌더링까지의 시간을 측정해보겠습니다.

// 통합 성능 측정 도구
class FullStackPerformanceTracker {
    constructor() {
        this.webStartTime = performance.now();
        this.checkpoints = {};
        this.nativeData = window.nativePerformance || {};
    }

    checkpoint(name) {
        const timestamp = performance.now();
        this.checkpoints[name] = timestamp;
        console.log(`${name}: ${timestamp.toFixed(2)}ms (웹앱 시작 기준)`);

        // 네이티브 앱 시작 기준으로도 계산
        if (this.nativeData.appStart) {
            const totalTime = Date.now() - this.nativeData.appStart;
            console.log(`${name}: ${totalTime.toFixed(2)}ms (네이티브 앱 시작 기준)`);
        }
    }

    getFullReport() {
        const webTimes = this.checkpoints;
        const report = {
            // 네이티브 구간
            native: {
                webViewInit: this.nativeData.webViewInit || 0,
                htmlLoad: (this.nativeData.htmlLoadEnd - this.nativeData.htmlLoadStart) || 0
            },
            // 웹 구간
            web: webTimes,
            // 전체 시간
            total: Date.now() - (this.nativeData.appStart || Date.now())
        };

        console.table(report);
        this.sendToNative(report);
        return report;
    }

    sendToNative(data) {
        try {
            if (window.Android) {
                // Android
                window.Android.receivePerformanceData(JSON.stringify(data));
            } else if (window.webkit) {
                // iOS
                window.webkit.messageHandlers.performanceData.postMessage(data);
            }
        } catch (error) {
            console.log('네이티브 통신 실패:', error);
        }
    }
}

// App.js에서 사용
const perfTracker = new FullStackPerformanceTracker();

function App() {
    useEffect(() => {
        perfTracker.checkpoint('React 앱 마운트');

        // 초기 데이터 로딩
        loadInitialData().then(() => {
            perfTracker.checkpoint('초기 데이터 로딩 완료');
            perfTracker.getFullReport();
        });
    }, []);

    return <div>...</div>;
}

// public/index.html에 추가
window.addEventListener('DOMContentLoaded', () => {
    window.webPerformanceTracker = new FullStackPerformanceTracker();
    window.webPerformanceTracker.checkpoint('DOM 준비 완료');
});

window.addEventListener('load', () => {
    window.webPerformanceTracker.checkpoint('리소스 로딩 완료');
});
 

 

 

실제 측정 결과

네이티브와 웹을 모두 측정한 결과는 다음과 같습니다:

 

전체 로딩 시간 분포

구간
시간
비율
측정 위치
앱 실행 → 웹뷰 초기화
150ms
3%
네이티브
HTML 요청 및 파싱
200ms
4%
네이티브 + 웹뷰
리소스 다운로드
2,800ms
56%
웹뷰
React 앱 시작
1,500ms
30%
JavaScript
초기 데이터 로딩
350ms
7%
JavaScript

 

 

총 로딩 시간: 5,000ms

가장 큰 병목은 리소스 다운로드(56%)React 앱 시작(30%)으로 확인되었습니다.

 

 

 


 

 

 

성능 최적화 방안

 

수행하려는 최적화 작업을

"효율적인 주방 운영"에 비유해보겠습니다.

 

현재 상황은 손님이 주문하면

  • 리소스 다운로드 병목: 필요한 모든 재료를 한 번에 가져옵니다.
  • React 앱 시작 병목: 모든 요리를 동시에 준비하려고 합니다.

당연히 서비스가 지연될 수밖에 없습니다.

 

개선 방향은 다음과 같습니다:

  • 리소스 다운로드 최적화: 필요한 리소스만 단계적으로 로드
  • React 앱 시작 최적화: 핵심 화면부터 우선 렌더링
  • 네이티브 최적화: 웹뷰를 사전에 준비하여 초기화 시간 단축

 

 

필요한 것만 그때그때 가져오기
중요한 것부터 먼저 내보내도록 순서바꾸기
적절한 준비 상태까지

 

 

 

 

리소스 다운로드 최적화

 

번들 크기 분석

# 번들 분석 도구 설치
npm install --save-dev webpack-bundle-analyzer

# 분석 실행
npm run build && npx webpack-bundle-analyzer build/static/js/*.js
 

 

코드 스플리팅 적용

// 라우트 기반 코드 스플리팅
const HomePage = React.lazy(() => import('./pages/HomePage'));
const ProfilePage = React.lazy(() => import('./pages/ProfilePage'));
const AdminPage = React.lazy(() =>
    import(/* webpackChunkName: "admin" */ './pages/AdminPage')
);

function App() {
    return (
        <Router>
            <Suspense fallback={<LoadingScreen />}>
                <Routes>
                    <Route path="/" element={<HomePage />} />
                    <Route path="/profile" element={<ProfilePage />} />
                    <Route path="/admin" element={<AdminPage />} />
                </Routes>
            </Suspense>
        </Router>
    );
}

// 컴포넌트 기반 코드 스플리팅
function Dashboard() {
    const [showChart, setShowChart] = useState(false);
    const ChartComponent = useMemo(() => {
        if (!showChart) return null;
        return React.lazy(() => import('./HeavyChart'));
    }, [showChart]);

    return (
        <div>
            <button onClick={() => setShowChart(true)}>
                차트 보기
            </button>
            {ChartComponent && (
                <Suspense fallback={<div>차트 로딩중...</div>}>
                    <ChartComponent />
                </Suspense>
            )}
        </div>
    );
}
 

 

리소스 프리로딩

네이티브 앱의 장점을 활용하여 리소스를 미리 캐시할 수 있습니다.

// Android: 앱 설치/업데이트 시 리소스 프리로딩
class ResourcePreloader(private val context: Context) {
    fun preloadCriticalResources() {
        val webView = WebView(context)
        val criticalUrls = listOf(
            "https://myapp.com/static/js/main.js",
            "https://myapp.com/static/css/main.css"
        )

        criticalUrls.forEach { url ->
            // 백그라운드에서 미리 로딩
            webView.loadUrl(url)
        }
    }
}
 

 

React 앱 시작 최적화

 

우선순위 기반 초기화

// 단계별 초기화 전략
function useStaggeredInitialization() {
    const [phase, setPhase] = useState('loading');
    const [criticalData, setCriticalData] = useState(null);

    useEffect(() => {
        const initializeApp = async () => {
            // 1단계: 필수 데이터만 로딩
            perfTracker.checkpoint('필수 데이터 로딩 시작');
            const essential = await loadEssentialData();
            setCriticalData(essential);
            setPhase('ready');
            perfTracker.checkpoint('필수 데이터 로딩 완료');

            // 2단계: 나머지 데이터는 백그라운드에서
            setTimeout(async () => {
                perfTracker.checkpoint('보조 데이터 로딩 시작');
                await loadSecondaryData();
                setPhase('fully-loaded');
                perfTracker.checkpoint('보조 데이터 로딩 완료');
            }, 100);
        };

        initializeApp();
    }, []);

    return { phase, criticalData };
}

function App() {
    const { phase, criticalData } = useStaggeredInitialization();

    if (phase === 'loading') {
        return <SplashScreen />;
    }

    return (
        <div>
            <MainContent data={criticalData} />
            {phase === 'fully-loaded' && <SecondaryFeatures />}
        </div>
    );
}
 

 

렌더링 최적화

// 가상 스크롤링으로 대량 데이터 처리
function VirtualizedList({ items, itemHeight = 60 }) {
    const [scrollTop, setScrollTop] = useState(0);
    const containerHeight = 400;

    const startIndex = Math.floor(scrollTop / itemHeight);
    const endIndex = Math.min(
        startIndex + Math.ceil(containerHeight / itemHeight) + 2,
        items.length
    );

    const visibleItems = items.slice(startIndex, endIndex);
    const totalHeight = items.length * itemHeight;
    const offsetY = startIndex * itemHeight;

    return (
        <div
            style={{ height: containerHeight, overflow: 'auto' }}
            onScroll={e => setScrollTop(e.target.scrollTop)}
        >
            <div style={{ height: totalHeight, position: 'relative' }}>
                <div style={{ transform: `translateY(${offsetY}px)` }}>
                    {visibleItems.map((item, index) => (
                        <div key={startIndex + index} style={{ height: itemHeight }}>
                            <ListItem data={item} />
                        </div>
                    ))}
                </div>
            </div>
        </div>
    );
}
 

 

네이티브 최적화 기법

 

웹뷰 사전 초기화

// Android: 앱 시작 시 웹뷰 미리 준비
class WebViewPool {
    private var preInitializedWebView: WebView? = null

    fun preInitializeWebView(context: Context) {
        // 앱 시작 시 백그라운드에서 웹뷰 미리 생성
        preInitializedWebView = WebView(context).apply {
            settings.javaScriptEnabled = true
            settings.domStorageEnabled = true
            // 기본 설정들 미리 적용
        }
    }

    fun getReadyWebView(): WebView? {
        return preInitializedWebView?.also {
            preInitializedWebView = null // 한 번 사용하면 null로 설정
        }
    }
}
 
// iOS: WKWebView 설정 최적화
class OptimizedWebViewFactory {
    static func createOptimizedWebView() -> WKWebView {
        let config = WKWebViewConfiguration()

        // 메모리 사용량 최적화
        config.suppressesIncrementalRendering = false
        config.allowsInlineMediaPlayback = true

        // JavaScript 최적화
        config.preferences.javaScriptCanOpenWindowsAutomatically = false

        // 프로세스 풀 공유로 메모리 절약
        config.processPool = WKProcessPool()

        return WKWebView(frame: .zero, configuration: config)
    }
}
 

 

 

 

 

최적화 결과

 

개선 전후 비교

구분
개선 전
개선 후
개선율
웹뷰 초기화
150ms
80ms
47% 단축
리소스 다운로드
2,800ms
1,200ms
57% 단축
React 앱 시작
1,500ms
600ms
60% 단축
전체 로딩 시간
5,000ms
2,200ms
56% 단축

 

 

 

사용자 경험 개선

 

로딩 속도

  • 첫 화면 표시: 3초 → 1.2초 (60% 단축)
  • 완전 로딩: 5초 → 2.2초 (56% 단축)

 

리소스 사용량

  • 메모리 사용량: 평균 35% 감소
  • CPU 사용량: 초기 로딩 시 40% 감소

 

특히 첫 화면 표시 시간이 1.2초로 단축되면서

사용자가 앱을 열자마자 바로 조작할 수 있게 되었습니다.

 

이는 사용자 이탈률 감소와 직결되는 핵심 지표입니다.

 

동시에 메모리와 CPU 사용량이 대폭 줄어들어

저사양 기기에서도 원활한 동작이 가능해졌습니다.

 

 

 

 

 

 


 

 

마치며

 

웹앱을 네이티브 앱으로 패키징할 때 성능 최적화는

네이티브와 웹 양쪽을 모두 고려해야 하는 복합적인 과제입니다.

 

네이티브 쪽에서는 웹뷰 초기화와 리소스 캐싱을,

웹 쪽에서는 번들 최적화와 렌더링 최적화를

각각 담당하면서 전체적인 사용자 경험을 개선할 수 있습니다.

 

가장 중요한 것은 체계적인 측정과 분석입니다.

 

네이티브와 웹을 통합하여 전체 로딩 과정을 측정하고,

병목 지점을 정확히 파악한 후에

우선순위를 정해서 개선해나가는 것이 효과적입니다.

 

또한 개발 환경에서의 측정뿐만 아니라

실제 사용자 환경에서의 성능도 지속적으로 모니터링해야 합니다.

 

다양한 기기와 네트워크 환경에서

일관된 성능을 제공하는 것이

진정한 성공이기 때문입니다.

 

 

운영 환경 모니터링

 

실제 사용자 환경에서의

성능을 지속적으로 모니터링하는 것도 중요합니다.

 

다음은 실사용자 성능 데이터를 수집하는 방법입니다.

// 실사용자 성능 모니터링
class RealUserMonitoring {
    constructor() {
        this.enabled = Math.random() < 0.05; // 5%의 사용자만 측정
    }

    collect() {
        if (!this.enabled) return;

        const data = {
            // 네이티브 정보
            platform: this.getPlatform(),
            appVersion: this.getAppVersion(),

            // 기기 정보
            deviceMemory: navigator.deviceMemory,
            connection: navigator.connection?.effectiveType,

            // 성능 정보
            ...window.webPerformanceTracker?.getFullReport()
        };

        this.sendToServer(data);
    }

    getPlatform() {
        if (window.Android) return 'android';
        if (window.webkit) return 'ios';
        return 'unknown';
    }

    getAppVersion() {
        // 네이티브에서 버전 정보를 받아옴
        if (window.Android) {
            return window.Android.getAppVersion();
        } else if (window.webkit) {
            return window.webkit.messageHandlers.appInfo.postMessage('version');
        }
        return 'unknown';
    }

    sendToServer(data) {
        fetch('/api/performance', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(data)
        }).catch(() => {}); // 실패해도 앱 동작에는 영향 없게
    }
}

// 앱 로딩 완료 후 데이터 수집
window.addEventListener('load', () => {
    setTimeout(() => {
        const monitor = new RealUserMonitoring();
        monitor.collect();
    }, 2000);
});
 

 

 

이러한 모니터링을 통해

실제 사용자들이 경험하는 성능을 파악하고,

지속적으로 개선해나갈 수 있습니다.

 

최적화는 한 번으로 끝나는 작업이 아니라

지속적인 관찰과 개선의 과정입니다.

 

 

지속적인 관찰과 개선의 과정

 

 

 

 


 

푸른들소프트

React, Vue.js를 활용한 웹앱 개발부터

Swift, Kotlin을 이용한 네이티브 패키징까지

하이브리드 앱 개발의 전 과정을 체계적으로 수행합니다.

 

성능 최적화와 사용자 경험 개선을 위한 체계적인 접근으로

고객과의 신뢰를 바탕으로 안정적이고

효율적인 IT 서비스를 제공하여,

귀사의 프로젝트를 성공적으로 수행할 수 있는

든든한 파트너가 되겠습니다.

 

 

 

 

 

 

 

 

 

 

푸른들소프트

웹, 앱, 기업 프로그램, 쇼핑몰 등 다양한 IT 프로젝트를 폭넓게 수행하는

대구·경북 지역에서 보기 드문 젊고, 실력있는 기술 중심 기업입니다.

 

단순히 ‘개발만 잘하는 회사’가 아니라,

고객의 비즈니스 상황을 이해하고

그에 맞는 맞춤형 기획부터 UI/UX 설계,

개발 기술 선정, 운영까지 전 과정에 깊이 관여하는 토탈 솔루션 파트너입니다.

 

저희는 유연한 사고,

빠르게 진화하는 기술에 대한 흡수력을 바탕으로

최신 트렌드와 실무 요구를 모두 반영할 수 있는 실력 있는 팀입니다.

 

기성 솔루션에 억지로 고객을 맞추는 것이 아니라,

스타트업, 소상공인, 중소·중견기업 등

고객의 상황에 꼭 맞는 시스템을 설계하고,

실질적인 업무 효율과 비용 절감을 만들어내는 데 집중하고 있습니다.

 

단기 성과에 급급한 개발이 아닌,

고객의 변화에 함께 적응하고,

장기적으로 함께 성장할 수 있는 파트너로서

신뢰할 수 있는 기술 동반자가 되어드릴 것을 약속드립니다.

 

지금, 푸른들소프트와 함께 귀사의 다음 변화를 설계해보세요.

작은 고민도 진지하게 듣고, 실력과 기술로 해답을 드리겠습니다.

 





 

 

 

 

 

 

 

 

 

 

 

 

Comments