Skip to content

Commit c98cdff

Browse files
committed
feat: getDevice device utilities
1 parent 781acd0 commit c98cdff

4 files changed

Lines changed: 162 additions & 7 deletions

File tree

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/**
2+
* vitest가 JSDOM을 사용하여 브라우저 환경을 시뮬레이션하도록 설정합니다.
3+
* 이를 통해 테스트 환경에서 window, navigator 등의 객체를 사용할 수 있습니다.
4+
*
5+
* @vitest-environment jsdom
6+
*/
7+
import { describe, test, expect, afterEach } from "vitest";
8+
import getDevice from ".";
9+
10+
describe("getDevice", () => {
11+
// userAgent를 테스트 케이스마다 다르게 설정하기 위한 헬퍼 함수
12+
const mockUserAgent = (userAgent: string) => {
13+
// JSDOM이 생성한 window.navigator 객체의 userAgent 속성을 덮어씁니다.
14+
Object.defineProperty(window.navigator, "userAgent", {
15+
value: userAgent,
16+
writable: true,
17+
configurable: true,
18+
});
19+
};
20+
21+
// 각 테스트가 끝난 후, 다음 테스트에 영향을 주지 않도록 userAgent를 초기화합니다.
22+
afterEach(() => {
23+
mockUserAgent("");
24+
});
25+
26+
// * ----- 1. SSR 환경 테스트 ----- * //
27+
describe("SSR (Server-Side Rendering) 환경", () => {
28+
test("window 객체가 없을 때, 기본 데스크톱 정보를 반환해야 합니다", () => {
29+
// 'window' 객체를 일시적으로 없애서 SSR 환경을 시뮬레이션합니다.
30+
const originalWindow = global.window;
31+
// @ts-ignore: 'window' is a read-only property.
32+
delete global.window;
33+
34+
const device = getDevice();
35+
36+
expect(device).toEqual({
37+
isMobile: false,
38+
isTablet: false,
39+
isDesktop: true,
40+
isIOS: false,
41+
isAndroid: false,
42+
});
43+
44+
// 다른 테스트에 영향을 주지 않도록 window 객체를 복원합니다.
45+
global.window = originalWindow;
46+
});
47+
});
48+
49+
// * ----- 2. 클라이언트 환경 테스트 ----- * //
50+
describe("클라이언트 (브라우저) 환경", () => {
51+
describe("데스크톱", () => {
52+
test("Windows Chrome User Agent를 데스크톱으로 인식해야 합니다", () => {
53+
mockUserAgent(
54+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"
55+
);
56+
const { isDesktop, isMobile, isTablet } = getDevice();
57+
expect(isDesktop).toBe(true);
58+
expect(isMobile).toBe(false);
59+
expect(isTablet).toBe(false);
60+
});
61+
});
62+
63+
describe("모바일", () => {
64+
test("iPhone User Agent를 모바일 및 iOS로 인식해야 합니다", () => {
65+
mockUserAgent(
66+
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148"
67+
);
68+
const { isMobile, isIOS, isDesktop } = getDevice();
69+
expect(isMobile).toBe(true);
70+
expect(isIOS).toBe(true);
71+
expect(isDesktop).toBe(false);
72+
});
73+
74+
test("Android Phone User Agent를 모바일 및 Android로 인식해야 합니다", () => {
75+
mockUserAgent(
76+
"Mozilla/5.0 (Linux; Android 13; SM-S908B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Mobile Safari/537.36"
77+
);
78+
const { isMobile, isAndroid, isTablet } = getDevice();
79+
expect(isMobile).toBe(true);
80+
expect(isAndroid).toBe(true);
81+
expect(isTablet).toBe(false);
82+
});
83+
});
84+
85+
describe("태블릿", () => {
86+
test("iPad User Agent를 태블릿 및 iOS로 인식해야 합니다", () => {
87+
mockUserAgent(
88+
"Mozilla/5.0 (iPad; CPU OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1"
89+
);
90+
const { isTablet, isIOS, isMobile } = getDevice();
91+
expect(isTablet).toBe(true);
92+
expect(isIOS).toBe(true);
93+
expect(isMobile).toBe(false);
94+
});
95+
96+
test("Android Tablet User Agent를 태블릿 및 Android로 인식해야 합니다", () => {
97+
mockUserAgent(
98+
"Mozilla/5.0 (Linux; Android 12; SM-X906C) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.99 Safari/537.36"
99+
);
100+
const { isTablet, isAndroid, isMobile } = getDevice();
101+
expect(isTablet).toBe(true);
102+
expect(isAndroid).toBe(true);
103+
expect(isMobile).toBe(false);
104+
});
105+
});
106+
});
107+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* @typedef {object} DeviceInfo
3+
* @property {boolean} isMobile - 모바일 디바이스 여부
4+
* @property {boolean} isTablet - 태블릿 디바이스 여부
5+
* @property {boolean} isDesktop - 데스크톱 디바이스 여부
6+
* @property {boolean} isIOS - iOS 운영체제 여부
7+
* @property {boolean} isAndroid - 안드로이드 운영체제 여부
8+
*/
9+
10+
/**
11+
* 사용자의 디바이스 환경 정보를 반환합니다.
12+
* window.navigator.userAgent를 기반으로 분석하며, 클라이언트 사이드에서만 정확한 값을 반환합니다.
13+
* 서버 사이드 렌더링(SSR) 환경에서는 에러를 방지하기 위해 기본값으로 데스크톱 환경 정보를 반환합니다.
14+
*
15+
* @returns {DeviceInfo} 디바이스 환경 정보 객체
16+
*/
17+
export default function getDevice() {
18+
// * ----- SSR 환경일 경우, 기본값(데스크톱)을 반환하여 에러 방지 ----- * //
19+
if (typeof window === "undefined" || !window.navigator) {
20+
return {
21+
isMobile: false,
22+
isTablet: false,
23+
isDesktop: true,
24+
isIOS: false,
25+
isAndroid: false,
26+
};
27+
}
28+
29+
// * ----- 클라이언트 환경일 경우, 실행 ----- * //
30+
const userAgent = window.navigator.userAgent;
31+
32+
const isIOS = /iPhone|iPad|iPod/i.test(userAgent);
33+
34+
const isAndroid = /Android/i.test(userAgent);
35+
36+
const isTablet = /(iPad)|(tablet)|(android(?!.*mobi))/i.test(userAgent);
37+
38+
const isMobile =
39+
!isTablet &&
40+
/Mobi|iP(hone|od)|Android|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);
41+
42+
const isDesktop = !isMobile && !isTablet;
43+
44+
return { isMobile, isTablet, isDesktop, isIOS, isAndroid };
45+
}

package/deviceUtil/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as getDevice } from "./getDevice";

vitest-report.xml

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
<?xml version="1.0" encoding="UTF-8" ?>
2-
<testsuites name="vitest tests" tests="5" failures="0" errors="0" time="0.00300075">
3-
<testsuite name="package/commonUtil/copyToClipboard/index.test.ts" timestamp="2025-09-11T08:34:04.771Z" hostname="users-MacBook-Pro.local" tests="5" failures="0" errors="0" skipped="0" time="0.00300075">
4-
<testcase classname="package/commonUtil/copyToClipboard/index.test.ts" name="copyToClipboard &gt; 성공 케이스 &gt; 최신 Clipboard API를 사용하여 성공적으로 복사한다" time="0.001606541">
2+
<testsuites name="vitest tests" tests="6" failures="0" errors="0" time="0.002620875">
3+
<testsuite name="package/deviceUtil/getDevice/index.test.ts" timestamp="2025-09-19T05:15:13.469Z" hostname="users-MacBook-Pro.local" tests="6" failures="0" errors="0" skipped="0" time="0.002620875">
4+
<testcase classname="package/deviceUtil/getDevice/index.test.ts" name="getDevice &gt; SSR (Server-Side Rendering) 환경 &gt; window 객체가 없을 때, 기본 데스크톱 정보를 반환해야 합니다" time="0.001096292">
55
</testcase>
6-
<testcase classname="package/commonUtil/copyToClipboard/index.test.ts" name="copyToClipboard &gt; 성공 케이스 &gt; Clipboard API 실패 시, 레거시 execCommand 방식으로 대체하여 성공적으로 복사한다" time="0.000289792">
6+
<testcase classname="package/deviceUtil/getDevice/index.test.ts" name="getDevice &gt; 클라이언트 (브라우저) 환경 &gt; 데스크톱 &gt; Windows Chrome User Agent를 데스크톱으로 인식해야 합니다" time="0.000288">
77
</testcase>
8-
<testcase classname="package/commonUtil/copyToClipboard/index.test.ts" name="copyToClipboard &gt; 실패 및 예외 케이스 &gt; 두 가지 방식 모두 실패하면 false를 반환한다" time="0.000157542">
8+
<testcase classname="package/deviceUtil/getDevice/index.test.ts" name="getDevice &gt; 클라이언트 (브라우저) 환경 &gt; 모바일 &gt; iPhone User Agent를 모바일 및 iOS로 인식해야 합니다" time="0.000204708">
99
</testcase>
10-
<testcase classname="package/commonUtil/copyToClipboard/index.test.ts" name="copyToClipboard &gt; 실패 및 예외 케이스 &gt; 빈 문자열도 성공적으로 복사한다" time="0.000105666">
10+
<testcase classname="package/deviceUtil/getDevice/index.test.ts" name="getDevice &gt; 클라이언트 (브라우저) 환경 &gt; 모바일 &gt; Android Phone User Agent를 모바일 및 Android로 인식해야 합니다" time="0.000102084">
1111
</testcase>
12-
<testcase classname="package/commonUtil/copyToClipboard/index.test.ts" name="copyToClipboard &gt; 실패 및 예외 케이스 &gt; 매우 긴 텍스트가 주어지면 에러를 던지고 대체(fallback) 로직으로 복사를 시도한다" time="0.00016025">
12+
<testcase classname="package/deviceUtil/getDevice/index.test.ts" name="getDevice &gt; 클라이언트 (브라우저) 환경 &gt; 태블릿 &gt; iPad User Agent를 태블릿 및 iOS로 인식해야 합니다" time="0.00009875">
13+
</testcase>
14+
<testcase classname="package/deviceUtil/getDevice/index.test.ts" name="getDevice &gt; 클라이언트 (브라우저) 환경 &gt; 태블릿 &gt; Android Tablet User Agent를 태블릿 및 Android로 인식해야 합니다" time="0.000103209">
1315
</testcase>
1416
</testsuite>
1517
</testsuites>

0 commit comments

Comments
 (0)