<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>오병문</title>
    <link>https://develop-obm.tistory.com/</link>
    <description>블록체인 개발 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Tue, 14 Apr 2026 17:45:06 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>오병문</managingEditor>
    <image>
      <title>오병문</title>
      <url>https://tistory1.daumcdn.net/tistory/4764482/attach/76aaa28671ba43be82be979b099ad9e5</url>
      <link>https://develop-obm.tistory.com</link>
    </image>
    <item>
      <title>[WebView] 카메라 호출 및 QR 스캐너 추가</title>
      <link>https://develop-obm.tistory.com/126</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;웹에서 네이티브 카메라 모듈을 사용하기 위해서 웹뷰와의 통신을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 ( 카메라 호출 요청 ) -&amp;gt; 웹뷰 카메라 호출 과정 자체는 단순하나 문제가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 떠있는 웹뷰에서 카메라를 호출할 경우 스캔 이후에 다시 웹뷰가 초기화 된다는 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 생각해낸 방법이 웹뷰는 그대로 로드하되 오버레이 형태로 카메라 모듈을 띄우는것&lt;/p&gt;
&lt;pre id=&quot;code_1703207881395&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;		&amp;lt;RNSafeAreaView style={{ flex: 1, backgroundColor: 'white' }}&amp;gt;
			&amp;lt;RNStatusBar backgroundColor=&quot;white&quot; barStyle=&quot;dark-content&quot; /&amp;gt;
			{injectJavaScriptCode &amp;amp;&amp;amp; fcmToken &amp;amp;&amp;amp; (
				&amp;lt;&amp;gt;
					&amp;lt;WebView
						ref={webViewRef}
						source={{ uri: CIC_APP_SERVICE_FRONTEND }}
						javaScriptEnabled={true}
						geolocationEnabled={true}
						sharedCookiesEnabled={true}
						thirdPartyCookiesEnabled={true}
						domStorageEnabled={true}
						mixedContentMode=&quot;compatibility&quot;
						originWhitelist={['*']}
						webviewDebuggingEnabled={true}
						onMessage={handleIncomingMessage}
						onShouldStartLoadWithRequest={handleWebRequest}
						injectedJavaScript={injectJavaScriptCode}
						injectedJavaScriptBeforeContentLoaded={
							injectFcmJavaScriptCode
						}
						decelerationRate={'normal'}
					/&amp;gt;
					{isScannerOpen &amp;amp;&amp;amp; (
						&amp;lt;QRScanner
							onQRCodeScanned={onQRCodeScanned}
							onBackPress={onBackPress}
						/&amp;gt;
					)}
				&amp;lt;/&amp;gt;
			)}
		&amp;lt;/RNSafeAreaView&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 카메라 호출을 받으면 플래그를 참조해서 카메라를 호출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순서대로 보자면, 웹에서 네이티브쪽으로 카메라를 호출하는 로직은 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1703207967028&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	const [isLoadingCamera, setIsLoadingCamera] = useState(false)

	useEffect(() =&amp;gt; {
		if (window.ReactNativeWebView) {
			function handleRequestCamera(): void {
				setIsLoadingCamera(!isLoadingCamera)
				const message = {
					action: 'OPEN_CAMERA_FOR_QR'
				}

				window.ReactNativeWebView.postMessage(JSON.stringify(message))
			}

			function handleMessage(event: MessageEvent): void {
				const data = event.data
				try {
					const parsedData = JSON.parse(data)
					if (parsedData) {
						router.push(parsedData)
					}
				} catch (error) {
					console.error('Error parsing message data', error)
				}
			}

			// @ts-ignore
			document.addEventListener('message', handleMessage)
			window.addEventListener('message', handleMessage)

			handleRequestCamera()

			return () =&amp;gt; {
				// @ts-ignore
				document.removeEventListener('message', handleMessage)
				window.removeEventListener('message', handleMessage)
			}
		}
	}, [])&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OPEN_CAMERA_FOR_QR 액션을 가진 메세지를 네이티브 쪽으로 보내고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네이티브 웹뷰에서 onMessage={handleIncomingMessage} 메세지를 통해서 들어온 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;OPEN_CAMERA_FOR_QR 액션을 받으면 카메라 모듈을 띄울 플래그를 변경한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;카메라 모듈은 react-native-vision-camera을 사용했고, 관련 설정은 공식문서를 참조하면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;QR 스캔 관련한 풀코드는 아래와 같다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1703208146710&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React, { useState, useEffect, useMemo, useRef } from 'react';
import {
	View,
	Text,
	StyleSheet,
	BackHandler,
	Dimensions,
	Alert,
	Linking,
	Platform,
} from 'react-native';
import {
	Camera,
	useCameraDevice,
	CameraPermissionStatus,
	useCodeScanner,
} from 'react-native-vision-camera';
import { openSettings } from 'react-native-permissions';

const screen = Dimensions.get('window');
const qrSize = screen.width * 0.75;

type QRScannerProps = {
	onQRCodeScanned: (data: string) =&amp;gt; void;
	onBackPress: () =&amp;gt; void;
};

const QRScanner: React.FC&amp;lt;QRScannerProps&amp;gt; = ({
	onQRCodeScanned,
	onBackPress,
}) =&amp;gt; {
	const [cameraPermission, setCameraPermission] =
		useState&amp;lt;CameraPermissionStatus&amp;gt;();

	const camera = useRef(null);
	const isProcessing = useRef(false);
	const device = useCameraDevice('back');

	useEffect(() =&amp;gt; {
		const backHandler = BackHandler.addEventListener(
			'hardwareBackPress',
			() =&amp;gt; {
				onBackPress();
				return true;
			},
		);

		return () =&amp;gt; backHandler.remove();
	}, [onBackPress]);

	const openAppSettings = (): void =&amp;gt; {
		if (Platform.OS === 'android') {
			openSettings();
		} else {
			// 시도
			Linking.openSettings();
			// 안될경우 시도
			// Linking.openURL('app-settings:');
		}
	};

	const showPermissionAlert = (): void =&amp;gt; {
		Alert.alert(
			'권한 필요',
			'QR 스캔을 하기 위해서는 카메라 권한이 필요합니다. 설정에서 권한을 허용해주세요.',
			[
				{ text: '취소', style: 'cancel' },
				{ text: '설정으로 이동', onPress: openAppSettings },
			],
			{ cancelable: false },
		);
	};

	useEffect(() =&amp;gt; {
		const checkPermissionAndRedirect = async (): Promise&amp;lt;void&amp;gt; =&amp;gt; {
			const status = await Camera.getCameraPermissionStatus();
			console.log('카메라 권한 상태...', status);
			if (status !== 'granted') {
				showPermissionAlert();
			}
		};

		checkPermissionAndRedirect();
	}, []);

	const codeScanner = useCodeScanner({
		codeTypes: ['qr', 'ean-13'],
		onCodeScanned: useMemo(() =&amp;gt; {
			return codes =&amp;gt; {
				if (codes.length &amp;gt; 0 &amp;amp;&amp;amp; !isProcessing.current) {
					isProcessing.current = true;

					const scannedValue = codes[0].value as string;
					onQRCodeScanned(scannedValue);

					isProcessing.current = false;
				}
			};
		}, [onQRCodeScanned]),
	});

	if (device == null) return &amp;lt;Text&amp;gt;loading...&amp;lt;/Text&amp;gt;;

	return (
		&amp;lt;View style={styles.container}&amp;gt;
			&amp;lt;Camera
				ref={camera}
				codeScanner={codeScanner}
				style={StyleSheet.absoluteFill}
				device={device}
				isActive={true}
			/&amp;gt;
			&amp;lt;View style={styles.overlay}&amp;gt;
				&amp;lt;View style={styles.background} /&amp;gt;
				&amp;lt;View
					style={[styles.qrFrame, { width: qrSize, height: qrSize }]}
				/&amp;gt;
				&amp;lt;Text style={styles.instructionText}&amp;gt;
					QR코드를 프레임 안에 위치 시켜주세요.
				&amp;lt;/Text&amp;gt;
			&amp;lt;/View&amp;gt;
		&amp;lt;/View&amp;gt;
	);
};

const styles = StyleSheet.create({
	container: {
		position: 'absolute',
		top: 0,
		left: 0,
		right: 0,
		bottom: 0,
		zIndex: 10,
	},
	preview: {
		flex: 1,
	},
	overlay: {
		position: 'absolute',
		top: 0,
		bottom: 0,
		left: 0,
		right: 0,
		alignItems: 'center',
		justifyContent: 'center',
		backgroundColor: 'transparent',
	},
	background: {
		position: 'absolute',
		top: 0,
		left: 0,
		right: 0,
		bottom: 0,
		backgroundColor: 'rgba(0,0,0,0.5)',
	},
	qrFrame: {
		width: qrSize,
		height: qrSize,
		alignItems: 'center',
		justifyContent: 'center',
		borderColor: 'white',
		borderWidth: 2,
		borderRadius: 20,
		backgroundColor: 'transparent',
	},
	instructionText: {
		position: 'absolute',
		width: '100%',
		bottom: qrSize * 0.5,
		textAlign: 'center',
		color: 'white',
		fontSize: 16,
	},
});

export default QRScanner;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카메라 스타일을 커스텀하고, back 버튼에 대한 핸들러가 없을 경우 앱이 백그라운드 상태로 내려가니 핸들러 처리도 추가했다. 또, 카메라에 대한 접근 권한이 없을 경우 추가 권한을 받기 위한 알럿과, 권한이 설정 안되어 있을경우 앱의 설정 페이지로 이동하는 로직도 추가했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://react-native-vision-camera.com/docs/guides&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://react-native-vision-camera.com/docs/guides&lt;/a&gt;&lt;/p&gt;</description>
      <category>WebView</category>
      <category>react-native-vision-camera</category>
      <category>웹뷰 카메라 호출</category>
      <author>오병문</author>
      <guid isPermaLink="true">https://develop-obm.tistory.com/126</guid>
      <comments>https://develop-obm.tistory.com/126#entry126comment</comments>
      <pubDate>Fri, 22 Dec 2023 10:26:07 +0900</pubDate>
    </item>
    <item>
      <title>[ETC] 미사용 의존성 모듈 삭제하기</title>
      <link>https://develop-obm.tistory.com/125</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 수동 검사&lt;/b&gt;: 가장 간단한 방법은 package.json 파일을 열어서 각 라이브러리를 검토하는 것입니다. 프로젝트에서 해당 라이브러리를 사용하는지 여부를 수동으로 확인합니다. 이 방법은 시간이 많이 소요되며, 대규모 프로젝트에서는 비효율적일 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 도구 사용&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;depcheck&lt;/b&gt;: depcheck는 노드 프로젝트에서 사용되지 않는 의존성을 찾아주는 도구입니다. 설치 후 프로젝트 디렉토리에서 depcheck를 실행하면 미사용 의존성 목록을 볼 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1701416272623&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm install -g depcheck
depcheck

Unused dependencies
* @date-io/date-fns
* @mui/x-data-grid
* @types/googlemaps
* apexcharts-clevision
* bootstrap-icons
* chart.js
* cleave.js
* i18next-browser-languagedetector
* i18next-http-backend
* payment
* recharts
Unused devDependencies
* @iconify/iconify
* @types/payment
* eslint-import-resolver-alias&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt; npm-check&lt;/b&gt;: 이 도구도 depcheck와 유사하게 작동하며, 미사용 의존성을 찾을 뿐만 아니라 버전 업데이트가 필요한 의존성도 확인할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1701416295776&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm install -g npm-check
npm-check&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 통합 개발 환경(IDE) 플러그인&lt;/b&gt;: 일부 IDE는 사용되지 않는 의존성을 찾는 플러그인을 제공합니다. 예를 들어, Visual Studio Code에는 이러한 기능을 제공하는 확장 프로그램이 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Import Cost&lt;/b&gt;: 이 확장 프로그램은 코드에 작성된 각 import 문의 크기를 보여줍니다. 이를 통해 프로젝트에 얼마나 많은 부분이 실제로 사용되고 있는지 파악할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Version Lens&lt;/b&gt;: 패키지의 최신 버전 정보를 보여주며, 어떤 의존성이 업데이트되었는지 쉽게 파악할 수 있습니다. 비록 직접적으로 미사용 의존성을 찾지는 않지만, 프로젝트의 의존성 관리에 유용합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Path Intellisense&lt;/b&gt;: 파일 경로를 자동으로 완성해주며, 잘못된 경로나 더 이상 존재하지 않는 파일에 대한 import를 찾아내는 데 도움을 줄 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 빌드 도구의 트리 쉐이킹 기능&lt;/b&gt;: 웹팩과 같은 빌드 도구는 '트리 쉐이킹(Tree Shaking)' 기능을 제공하여 미사용 코드를 제거할 수 있습니다. 이는 프로젝트의 최종 번들 크기를 줄이는 데 도움이 됩니다. 트리 쉐이킹은 미사용 코드를 제거하지만, package.json 파일에서 직접 의존성을 제거하지는 않습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;ES2015 모듈 문법(ESM)&lt;/b&gt;: 트리 쉐이킹은 ESM(import와 export)을 사용할 때 가장 잘 작동합니다. CommonJS(require와 module.exports)와 같은 다른 모듈 시스템은 트리 쉐이킹에 적합하지 않습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;sideEffects 속성&lt;/b&gt;: package.json 파일에서 sideEffects 속성을 false로 설정하여 해당 패키지나 모듈에 부수 효과(side effects)가 없음을 나타낼 수 있습니다. 이는 웹팩이 안전하게 미사용 코드를 제거할 수 있도록 도와줍니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Production 모드&lt;/b&gt;: 웹팩의 프로덕션 모드는 트리 쉐이킹을 포함한 여러 최적화를 자동으로 수행합니다. 개발 모드에서는 이러한 최적화가 일반적으로 비활성화됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1701416472015&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 웹팩 설정 예시
module.exports = {
  mode: 'production', // 트리 쉐이킹 활성화
  // ... 기타 설정 ...
};&lt;/code&gt;&lt;/pre&gt;</description>
      <category>ETC</category>
      <category>의존성 모듈</category>
      <category>의존성 삭제</category>
      <author>오병문</author>
      <guid isPermaLink="true">https://develop-obm.tistory.com/125</guid>
      <comments>https://develop-obm.tistory.com/125#entry125comment</comments>
      <pubDate>Fri, 1 Dec 2023 16:41:25 +0900</pubDate>
    </item>
    <item>
      <title>[NextJS] Naver Map Clustering</title>
      <link>https://develop-obm.tistory.com/124</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;준비물 : 네이버 맵 사용을 위한 클라이언트 ID&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;풀코드는 아래와 같음.&lt;/p&gt;
&lt;pre id=&quot;code_1701319131689&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React, { useEffect, useRef } from 'react'

import { NaverMaps, ClusteringMapProps } from '../types/naverMaps'

import authConfig from 'src/configs/auth'

// shopList (상점 목록)과 onShopSelect (상점 선택시 호출할 함수) 두 개의 prop을 받음 
const NaverMapMarketClustering = ({ shopList, onShopSelect }: ClusteringMapProps) =&amp;gt; {
    // useRef를 사용하여 지도를 표시할 DOM 요소의 참조(mapRef)를 생성
	const mapRef = useRef(null)

    // 동적으로 외부 스크립트를 로드하는 loadScript 함수를 정의
    // 스크립트 URL(src)과 로드 완료시 호출할 콜백 함수(callback)를 매개변수로 받음
	const loadScript = (src: any, callback: any) =&amp;gt; {
        // 이미 로드된 스크립트인지 확인하여 중복 로드를 방지
		if (document.querySelector(`script[src=&quot;${src}&quot;]`)) {
			callback()
			return
		}

		const script = document.createElement('script')
		script.src = src
		script.async = true
		script.onload = () =&amp;gt; {
			if (callback) callback()
		}
		script.onerror = () =&amp;gt; {
			console.error(`Script load error for ${src}`)
		}
		document.head.appendChild(script)
	}

    // 스크립트 로드가 완료되면 지도를 초기화하고 상점 마커를 추가
	const initializeMapAndMarkers = async (navermaps: NaverMaps) =&amp;gt; {
		let map: any

        // 네이버 지도 인스턴스를 생성하고, shopList를 이용하여 각 상점 위치에 마커를 추가
		if (mapRef.current) {
			map = new navermaps.Map(mapRef.current, {
				center: new navermaps.LatLng(36, 128),
				zoom: 7,
				mapTypeControl: false,
				mapDataControl: false,
				logoControl: false,
				scaleControl: false,
				zoomControl: true,
				zoomControlOptions: {
					position: navermaps.Position.TOP_RIGHT,
					style: navermaps.ZoomControlStyle.SMALL
				}
			})
		}

		if (shopList) {
			const markers = shopList.map((shop: any) =&amp;gt; {
				const position = new navermaps.LatLng(shop.si_branch_latiitude, shop.si_branch_longitude)

				const imageUrl = `${authConfig.imageEndpoint}${shop.si_company_image_logo}`

				const marker = new navermaps.Marker({
					position,
					map,
					title: shop.si_shop_name,
					icon: {
						content: `&amp;lt;img src=&quot;${imageUrl}&quot; alt=&quot;${shop.si_shop_name}&quot; style=&quot;width: 40px; height: 40px; object-fit: contain;&quot;&amp;gt;`,
						size: new navermaps.Size(40, 40),
						anchor: new navermaps.Point(20, 20)
					}
				})

                // 마커 클릭 이벤트에 displayShopInfo 함수를 연결하여 상점 정보를 표시
				navermaps.Event.addListener(marker, 'click', () =&amp;gt; {
					displayShopInfo(shop)
				})

				return marker
			})

			function displayShopInfo(shop: any) {
				onShopSelect({
					shopName: shop.si_company_name,
					shopAddress: shop.si_company_address_1 + ' ' + shop.si_company_address_2,
					image: shop.si_company_image_main_1
				})
			}

			var htmlMarker1 = {
					content: `&amp;lt;div style=&quot;cursor:pointer;width:40px;height:40px;line-height:42px;font-size:10px;color:white;text-align:center;font-weight:bold;background:url(${authConfig.frontEndpoint}/images/image/cluster-marker-1.png);background-size:contain;&quot;&amp;gt;&amp;lt;/div&amp;gt;`,
					size: new navermaps.Size(40, 40),
					anchor: new navermaps.Point(20, 20)
				},
				htmlMarker2 = {
					content: `&amp;lt;div style=&quot;cursor:pointer;width:40px;height:40px;line-height:42px;font-size:10px;color:white;text-align:center;font-weight:bold;background:url(${authConfig.frontEndpoint}/images/image/cluster-marker-2.png);background-size:contain;&quot;&amp;gt;&amp;lt;/div&amp;gt;`,
					size: new navermaps.Size(40, 40),
					anchor: new navermaps.Point(20, 20)
				},
				htmlMarker3 = {
					content: `&amp;lt;div style=&quot;cursor:pointer;width:40px;height:40px;line-height:42px;font-size:10px;color:white;text-align:center;font-weight:bold;background:url(${authConfig.frontEndpoint}/images/image/cluster-marker-1.png);background-size:contain;&quot;&amp;gt;&amp;lt;/div&amp;gt;`,
					size: new navermaps.Size(40, 40),
					anchor: new navermaps.Point(20, 20)
				},
				htmlMarker4 = {
					content: `&amp;lt;div style=&quot;cursor:pointer;width:40px;height:40px;line-height:42px;font-size:10px;color:white;text-align:center;font-weight:bold;background:url(${authConfig.frontEndpoint}/images/image/cluster-marker-2.png);background-size:contain;&quot;&amp;gt;&amp;lt;/div&amp;gt;`,
					size: new navermaps.Size(40, 40),
					anchor: new navermaps.Point(20, 20)
				},
				htmlMarker5 = {
					content: `&amp;lt;div style=&quot;cursor:pointer;width:40px;height:40px;line-height:42px;font-size:10px;color:white;text-align:center;font-weight:bold;background:url(${authConfig.frontEndpoint}/images/image/cluster-marker-1.png);background-size:contain;&quot;&amp;gt;&amp;lt;/div&amp;gt;`,
					size: new navermaps.Size(40, 40),
					anchor: new navermaps.Point(20, 20)
				}

			// @ts-ignore
            // Cannot find name 'MarkerClustering'. Did you mean 'markerClustering'? 에러 회피
			const markerClustering = new MarkerClustering({
				minClusterSize: 2,
				maxZoom: 8,
				map: map,
				markers: markers,
				disableClickZoom: false,
				gridSize: 120,
				icons: [htmlMarker1, htmlMarker2, htmlMarker3, htmlMarker4, htmlMarker5],
				indexGenerator: [10, 100, 200, 500, 1000],
				stylingFunction: (clusterMarker: any, count: number) =&amp;gt; {
					clusterMarker.getElement().querySelector('div:first-child').textContent = count
				}
			})
		}
	}

    // 컴포넌트가 마운트될 때 네이버 지도 API 스크립트와 마커 클러스터링 스크립트를 로드
	useEffect(() =&amp;gt; {
		loadScript(
			`https://openapi.map.naver.com/openapi/v3/maps.js?ncpClientId=${authConfig.naverMapCliendId}&amp;amp;submodules=geocoder,panorama`,
			() =&amp;gt; {
				if (window.naver &amp;amp;&amp;amp; window.naver.maps) {
					loadScript('/scripts/MarkerClustering.js', () =&amp;gt; {
						initializeMapAndMarkers(window.naver.maps)
					})
				}
			}
		)
	}, [shopList])

	return &amp;lt;div ref={mapRef} style={{ width: '100%', height: '400px', marginBottom: 15 }} /&amp;gt;
}

export default NaverMapMarketClustering&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참조 문서 : &lt;a href=&quot;https://navermaps.github.io/maps.js.ncp/docs/tutorial-marker-cluster.example.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://navermaps.github.io/maps.js.ncp/docs/tutorial-marker-cluster.example.html&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1701317848778&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;NAVER Maps API v3&quot; data-og-description=&quot;NAVER Maps API v3로 여러분의 지도를 만들어 보세요. 유용한 기술문서와 다양한 예제 코드를 제공합니다.&quot; data-og-host=&quot;navermaps.github.io&quot; data-og-source-url=&quot;https://navermaps.github.io/maps.js.ncp/docs/tutorial-marker-cluster.example.html&quot; data-og-url=&quot;https://navermaps.github.io/maps.js.ncp&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/CyFyv/hyUE7CUxQV/fUfUarLGAUPkiVirL5Q7f0/img.png?width=912&amp;amp;height=466&amp;amp;face=0_0_912_466,https://scrap.kakaocdn.net/dn/b8JFuj/hyUE1CHZCB/fH5khabRfKcqiqQtdVTz21/img.png?width=912&amp;amp;height=466&amp;amp;face=0_0_912_466&quot;&gt;&lt;a href=&quot;https://navermaps.github.io/maps.js.ncp/docs/tutorial-marker-cluster.example.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://navermaps.github.io/maps.js.ncp/docs/tutorial-marker-cluster.example.html&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/CyFyv/hyUE7CUxQV/fUfUarLGAUPkiVirL5Q7f0/img.png?width=912&amp;amp;height=466&amp;amp;face=0_0_912_466,https://scrap.kakaocdn.net/dn/b8JFuj/hyUE1CHZCB/fH5khabRfKcqiqQtdVTz21/img.png?width=912&amp;amp;height=466&amp;amp;face=0_0_912_466');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;NAVER Maps API v3&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;NAVER Maps API v3로 여러분의 지도를 만들어 보세요. 유용한 기술문서와 다양한 예제 코드를 제공합니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;navermaps.github.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;github : &lt;a href=&quot;https://github.com/navermaps/marker-tools.js/tree/master/marker-clustering&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/navermaps/marker-tools.js/tree/master/marker-clustering&lt;/a&gt;&lt;/p&gt;</description>
      <category>NextJS</category>
      <author>오병문</author>
      <guid isPermaLink="true">https://develop-obm.tistory.com/124</guid>
      <comments>https://develop-obm.tistory.com/124#entry124comment</comments>
      <pubDate>Thu, 30 Nov 2023 13:43:01 +0900</pubDate>
    </item>
    <item>
      <title>[NextJS] Google map 사용</title>
      <link>https://develop-obm.tistory.com/123</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js에서는 React와 비슷하지만, 서버사이드 렌더링이나 정적 사이트 생성 등의 추가 기능을 제공합니다. 이 경우 Google Maps를 사용하기 위해 직접 스크립트를 로드하는 방식을 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환경 변수를 사용하여 Google Maps API 스크립트를 로드하고 현재 위치에 마커를 표시하며, 2km 반경의 원을 그리는 방법에 대한 예시입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, public 디렉토리에 GoogleMap.js라는 스크립트 파일을 만듭니다&lt;/p&gt;
&lt;pre id=&quot;code_1699485900868&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// public/GoogleMap.js

window.initMap = function() {};

function loadScript(src, position, id) {
  if (!window.google) {
    const script = document.createElement('script');
    script.type = 'text/javascript';
    script.src = src;
    script.id = id;
    position.appendChild(script);
  }
}

const src = `https://maps.googleapis.com/maps/api/js?key=${process.env.NEXT_PUBLIC_GOOGLE_MAP_API_KEY}&amp;amp;callback=initMap`;
loadScript(src, document.body, 'google-maps-api');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로는, Next.js 페이지 또는 컴포넌트 내에서 Google Maps API와 인터랙션하는 로직을 다음과 같이 구현합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1699485927817&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// pages/index.js 또는 components/MyMapComponent.js

import { useEffect, useRef, useState } from 'react';

const MyMapComponent = () =&amp;gt; {
  const mapRef = useRef(null);
  const [map, setMap] = useState(null);

  useEffect(() =&amp;gt; {
    // GoogleMap.js 스크립트 로드 기다리기
    window.initMap = initMap;

    // 스크립트가 이미 페이지에 존재하는지 확인
    if (!window.google) {
      const script = document.createElement('script');
      script.src = `https://maps.googleapis.com/maps/api/js?key=${process.env.NEXT_PUBLIC_GOOGLE_MAP_API_KEY}&amp;amp;callback=initMap`;
      script.defer = true;
      document.head.appendChild(script);
    } else {
      initMap();
    }

    return () =&amp;gt; {
      window.initMap = null;
    };
  }, []);

  // 지도 초기화
  const initMap = () =&amp;gt; {
    if (window.google &amp;amp;&amp;amp; !map) {
      navigator.geolocation.getCurrentPosition(function(position) {
        const currentLocation = {
          lat: position.coords.latitude,
          lng: position.coords.longitude
        };

        const map = new window.google.maps.Map(mapRef.current, {
          center: currentLocation,
          zoom: 13
        });

        new window.google.maps.Marker({
          position: currentLocation,
          map: map,
          title: &quot;Your Location&quot;
        });

        new window.google.maps.Circle({
          strokeColor: '#FF0000',
          strokeOpacity: 0.8,
          strokeWeight: 2,
          fillColor: '#FF0000',
          fillOpacity: 0.35,
          map: map,
          center: currentLocation,
          radius: 2000 // 2km
        });

        setMap(map);
      });
    }
  };

  return &amp;lt;div id=&quot;map&quot; ref={mapRef} style={{ height: '400px', width: '100%' }} /&amp;gt;;
};

export default MyMapComponent;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Google Maps API 스크립트를 로드하고, 사용자의 현재 위치를 가져와서 지도에 마커를 표시하고 2km 반경의 원을 그립니다. useEffect 내에서 스크립트 로딩 상태를 감지하고 지도를 초기화하는 로직을 구현했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 마커 아이콘을 변경하고 싶을 경우 아래와 같이 구현 가능합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1699486058213&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;			const customIcon = {
				url: '/images/image/logo.png',
				scaledSize: new google.maps.Size(40, 40), // 마커의 크기를 지정
				origin: new google.maps.Point(0, 0), // 이미지의 시작점 (기본적으로 0,0)
				anchor: new google.maps.Point(20, 20) // 마커의 앵커 포인트를 지정
			}

			new window.google.maps.Marker({
				position: currentLocation,
				map: map,
				title: 'Your Location',
				icon: customIcon
			})&lt;/code&gt;&lt;/pre&gt;</description>
      <category>NextJS</category>
      <category>google map circle</category>
      <category>nextjs google map</category>
      <author>오병문</author>
      <guid isPermaLink="true">https://develop-obm.tistory.com/123</guid>
      <comments>https://develop-obm.tistory.com/123#entry123comment</comments>
      <pubDate>Thu, 9 Nov 2023 08:28:52 +0900</pubDate>
    </item>
    <item>
      <title>[React-Native] 노치 디자인 대응</title>
      <link>https://develop-obm.tistory.com/122</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;iPhone X 이후 모델의 노치(notch) 또는 홈 인디케이터(home indicator)로 인해 컨텐츠가 가려지는 문제가 발생할 수 있습니다. 이는 &quot;Safe Area&quot; 개념을 적용하여 해결할 수 있습니다. Safe Area는 노치나 홈 버튼이 존재하는 영역을 피해 콘텐츠가 올바르게 표시될 수 있도록 화면의 안전한 영역을 정의하는 것입니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;SafeAreaView 사용하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SafeAreaView 컴포넌트는 iOS의 노치와 같은 화면의 비표준 영역을 자동으로 감지하고, 해당 영역을 피해 콘텐츠가 올바르게 표시될 수 있도록 조정해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1699419005432&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { SafeAreaView } from 'react-native';

&amp;lt;SafeAreaView style={{ flex: 1 }}&amp;gt;
  &amp;lt;WebView source={{ uri: 'https://example.com' }} /&amp;gt;
&amp;lt;/SafeAreaView&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;StatusBar Height 조정하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태 표시줄(Status Bar)의 높이를 조정하여 WebView 상단의 패딩을 추가할 수도 있습니다. 이 방법은 react-native-status-bar-height 라이브러리를 사용하여 현재 상태 표시줄의 높이를 가져와서 적용합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1699419018630&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { getStatusBarHeight } from 'react-native-status-bar-height';
import { WebView } from 'react-native-webview';

&amp;lt;WebView
  style={{ marginTop: getStatusBarHeight() }}
  source={{ uri: 'https://example.com' }}
/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;CSS 또는 Meta Tag 사용하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 컨텐츠 내부에서 CSS를 사용하거나 meta 태그를 활용해 viewport를 조정하여 Safe Area를 적용하는 방법도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSS 예시&lt;/p&gt;
&lt;pre id=&quot;code_1699419035866&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;padding-top: constant(safe-area-inset-top); /* iOS 11.0 */
padding-top: env(safe-area-inset-top); /* iOS 11.2 이상 */&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Meta 태그 예시&lt;/p&gt;
&lt;pre id=&quot;code_1699419062134&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;meta name=&quot;viewport&quot; content=&quot;viewport-fit=cover&quot;&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;react-native-safe-area-context 사용하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;react-native-safe-area-context 라이브러리를 사용하면, 디바이스의 안전 영역 경계를 고려하여 뷰를 배치할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1699419086584&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context';

function App() {
  const insets = useSafeAreaInsets();

  return (
    &amp;lt;SafeAreaProvider&amp;gt;
      &amp;lt;View style={{ paddingTop: insets.top }}&amp;gt;
        &amp;lt;WebView source={{ uri: 'https://example.com' }} /&amp;gt;
      &amp;lt;/View&amp;gt;
    &amp;lt;/SafeAreaProvider&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Flex Box 사용하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;flex 속성을 사용해 WebView 컴포넌트가 안전 영역 내에서 유연하게 크기를 조정하도록 만들 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1699419099798&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;WebView
  style={{ flex: 1 }}
  source={{ uri: 'https://example.com' }}
/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;WebView의 스크롤 인디케이터 조정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebView의 스크롤 인디케이터도 Safe Area를 고려하여 조정해야 할 수 있습니다. 이는 위에서 언급한 방법들을 사용하여 Safe Area 안에서 컨텐츠가 표시되도록 하여 해결할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 방법들을 사용하여 React Native의 WebView 컴포넌트가 iOS 기기의 노치나 홈 인디케이터로 인해 컨텐츠가 가려지는 문제를 해결할 수 있습니다. 개발 환경과 요구 사항에 맞게 가장 적절한 방법을 선택하여 적용하시기 바랍니다.&lt;/p&gt;</description>
      <category>React Native</category>
      <category>react native safe area</category>
      <category>노치 디자인 대응</category>
      <author>오병문</author>
      <guid isPermaLink="true">https://develop-obm.tistory.com/122</guid>
      <comments>https://develop-obm.tistory.com/122#entry122comment</comments>
      <pubDate>Wed, 8 Nov 2023 13:52:26 +0900</pubDate>
    </item>
    <item>
      <title>[React-Native] WebView를 사용하여 웹 콘텐츠에 GPS 위치 전달</title>
      <link>https://develop-obm.tistory.com/121</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;React Native에서 WebView를 사용하여 웹 콘텐츠에 GPS 위치를 제공하고자 할 때&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 단계를 거쳐 구현할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;권한 요청&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모바일 애플리케이션에서 GPS 위치 정보에 접근하려면 사용자로부터 위치 정보 접근에 대한 권한을 얻어야 합니다. React Native에서는 react-native-permissions 라이브러리를 통해 이를 수행할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1699418546744&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { PermissionsAndroid } from 'react-native';
import { request, PERMISSIONS } from 'react-native-permissions';

async function requestLocationPermission() {
  try {
    const granted = await request(PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION);
    if (granted === PermissionsAndroid.RESULTS.GRANTED) {
      console.log('You can use the location');
    } else {
      console.log('Location permission denied');
    }
  } catch (err) {
    console.warn(err);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iOS에서는 Info.plist에 위치 서비스에 대한 권한 요청을 추가해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;위치 정보 얻기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권한을 얻었다면, navigator.geolocation API를 사용해 현재 위치를 얻을 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1699418567521&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function getLocation() {
  return new Promise((resolve, reject) =&amp;gt; {
    navigator.geolocation.getCurrentPosition(
      position =&amp;gt; {
        resolve(position);
      },
      error =&amp;gt; reject(error),
      { enableHighAccuracy: true, timeout: 20000, maximumAge: 1000 },
    );
  });
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;WebView와 통신&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위치 정보를 웹 콘텐츠로 전달하려면 postMessage 메소드를 사용합니다. 이를 위해 WebView의 ref를 사용하여 injectJavaScript 함수를 호출합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1699418582201&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;WebView
  ref={ref =&amp;gt; {
    this.webview = ref;
  }}
  source={{ uri: 'https://example.com' }}
  onMessage={this.handleMessage}
/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 위치 정보 요청 버튼을 웹 페이지에서 클릭하면, 해당 이벤트를 감지하고 React Native로 메시지를 전송합니다. 그리고 React Native에서는 이 메시지를 받아 위치 정보를 요청하고 결과를 웹 콘텐츠로 다시 전송합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;위치 정보를 웹 콘텐츠로 전송&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React Native에서 위치 정보를 받은 후, 웹 콘텐츠에 해당 정보를 전송합니다. 이 때 injectJavaScript 함수를 사용합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1699418610848&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 위치 정보를 얻은 후 웹뷰로 전송하는 함수
async function sendLocationToWeb() {
  try {
    const position = await getLocation();
    const script = `
      window.postMessage(${JSON.stringify({
        type: 'location',
        latitude: position.coords.latitude,
        longitude: position.coords.longitude,
      })}, '*');
    `;
    this.webview.injectJavaScript(script);
  } catch (error) {
    console.error(error);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 페이지에서는 window.addEventListener('message', handleMessage)를 통해 메시지를 받을 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1699418625144&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;window.addEventListener('message', event =&amp;gt; {
  const data = JSON.parse(event.data);
  if (data.type === 'location') {
    console.log('Latitude:', data.latitude, 'Longitude:', data.longitude);
    // 위치 정보를 웹 페이지에서 사용
  }
});&lt;/code&gt;&lt;/pre&gt;</description>
      <category>React Native</category>
      <category>WebView를 사용하여 웹 콘텐츠에 GPS 위치</category>
      <author>오병문</author>
      <guid isPermaLink="true">https://develop-obm.tistory.com/121</guid>
      <comments>https://develop-obm.tistory.com/121#entry121comment</comments>
      <pubDate>Wed, 8 Nov 2023 13:45:17 +0900</pubDate>
    </item>
    <item>
      <title>[React-Native] 웹뷰(WebView)와 웹 콘텐츠 간의 통신</title>
      <link>https://develop-obm.tistory.com/120</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;React Native에서 웹뷰(WebView)와 웹 콘텐츠 간의 통신은 주로 두 가지 방법을 통해 이루어집니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;JavaScript를 통한 메시지 전송 (postMessage)&lt;/b&gt;: React Native의 WebView 컴포넌트는 웹 콘텐츠로 메시지를 보내고 웹 콘텐츠에서 메시지를 받을 수 있도록 postMessage 인터페이스를 제공합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;URL 스킴을 통한 메시지 전송&lt;/b&gt;: 커스텀 URL 스킴을 사용하여 애플리케이션과 웹 콘텐츠 간에 통신할 수 있습니다. 이 방법은 웹 페이지가 특정 URL 패턴을 로드하려고 할 때 트리거됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JavaScript를 통한 메시지 전송 (postMessage)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 페이지에서 postMessage를 사용하여 React Native로 메시지를 보내는 과정은 다음과 같습니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;웹 콘텐츠 내부에서 메시지 보내기&lt;/b&gt;: 웹 콘텐츠 내부의 JavaScript에서 window.ReactNativeWebView.postMessage(data)를 호출하여 메시지를 보냅니다. 여기서 data는 전송하려는 메시지입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;React Native에서 메시지 받기&lt;/b&gt;: React Native의 WebView 컴포넌트는 onMessage prop을 통해 메시지를 받습니다. 이 메소드는 웹 콘텐츠에서 보낸 데이터를 매개변수로 가지는 콜백 함수입니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1699418233026&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;WebView
  source={{ uri: 'https://example.com' }}
  onMessage={(event) =&amp;gt; {
    // 웹 콘텐츠에서 보낸 메시지 처리
    const message = event.nativeEvent.data;
    console.log(message);
  }}
/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로, React Native에서 웹 페이지로 메시지를 보내려면 웹뷰의 injectJavaScript 메소드를 사용할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1699418249728&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;this.webview.injectJavaScript(`window.postMessage('${message}', '*');`);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;URL 스킴을 통한 메시지 전송&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커스텀 URL 스킴은 웹 페이지가 특정 URL 패턴을 로드하려고 할 때 React Native 앱에서 이를 감지하고 적절한 조치를 취하는 데 사용됩니다. 이 방법은 특히 React Native가 postMessage를 지원하지 않는 경우 유용합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;웹 콘텐츠에서 커스텀 URL 스킴 사용&lt;/b&gt;: 웹 콘텐츠에서 JavaScript를 사용하여 커스텀 URL을 호출합니다. 예를 들어, myapp://doSomething라는 URL을 호출하도록 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;React Native에서 URL 변경 감지&lt;/b&gt;: WebView 컴포넌트의 onNavigationStateChange 또는 onShouldStartLoadWithRequest prop을 사용하여 URL 변경을 감지하고 처리합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1699418269423&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;WebView
  onNavigationStateChange={(event) =&amp;gt; {
    if (event.url.startsWith('myapp://')) {
      // 커스텀 URL 처리
    }
  }}
/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 두 메소드를 통해 React Native 앱과 웹 콘텐츠 간의 양방향 통신이 가능해집니다. 그러나 이 통신을 설정하고 사용하는 과정에서 보안을 고려해야 하며, 특히 URL 스킴을 사용할 때는 잠재적인 보안 취약점에 주의해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 사용 중인 WebView 구현에 따라 정확한 prop 이름이나 메소드 사용법이 달라질 수 있으니 해당 문서를 참조하는 것이 중요합니다. 예를 들어, react-native-webview 라이브러리는 기본 WebView와 다른 API를 가질 수 있습니다.&lt;/p&gt;</description>
      <category>React Native</category>
      <category>WebView와 웹 통신</category>
      <author>오병문</author>
      <guid isPermaLink="true">https://develop-obm.tistory.com/120</guid>
      <comments>https://develop-obm.tistory.com/120#entry120comment</comments>
      <pubDate>Wed, 8 Nov 2023 13:39:17 +0900</pubDate>
    </item>
    <item>
      <title>[ETC] 서브셋 폰트 경량화</title>
      <link>https://develop-obm.tistory.com/119</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;서브셋 폰트(subset font)&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글꼴(font)의 서브셋(subset)을 의미합니다. 즉, 문자 세트의 일부만을 포함하는 글꼴입니다. 웹에서 이를 사용하는 주된 이유는 성능 향상입니다. 전체 글꼴 대신 필요한 문자들만 포함한 서브셋을 사용함으로써, 파일 크기를 줄이고 페이지 로딩 시간을 단축시킬 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 영문 웹사이트에서 한글 폰트를 사용할 필요가 없다면, 한글을 포함하지 않은 서브셋 글꼴을 사용하여 리소스를 절약할 수 있습니다. 마찬가지로, 한글 웹사이트에서도 필요한 한글 글자들만을 포함하는 서브셋을 만들어 사용할 수 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;heading-3&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;서브셋 리스트&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 글자를 남기고 어떤 글자를 버리는 것이 좋을까? 자주 사용하는 것은 포함하고, 거의 사용하지 않는 글자를 삭제하는 게 바람직합니다. 일반적으로 통용되는 리스트는 아래와 같습니다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ㄱㄲㄳㄴㄵㄶㄷㄸㄹㄺㄻㄼㄽㄾㄿㅀㅁㅂㅃㅄㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎ &lt;br /&gt;ㅏㅐㅑㅒㅓㅔㅕㅖㅗㅘㅙㅚㅛㅜㅝㅞㅟㅠㅡㅢㅣ &lt;br /&gt;0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!&quot;\#$%&amp;amp;'()*+,-./:;&amp;lt;=&amp;gt;?@[^_`|~ &lt;br /&gt;가각간갇갈갉갊감갑값갓갔강갖갗같갚갛개객갠갤갬갭갯갰갱갸갹갼걀걋걍걔걘걜거걱건걷걸걺검겁것겄겅겆겉겊겋게겐겔겜겝겟겠겡겨격겪견겯결겸겹겻겼경곁계곈곌곕곗고곡곤곧골곪곬곯곰곱곳공곶과곽관괄괆괌괍괏광괘괜괠괩괬괭괴괵괸괼굄굅굇굉교굔굘굡굣구국군굳굴굵굶굻굼굽굿궁궂궈궉권궐궜궝궤궷귀귁귄귈귐귑귓규균귤그극근귿글긁금급긋긍긔기긱긴긷길긺김깁깃깅깆깊까깍깎깐깔깖깜깝깟깠깡깥깨깩깬깰깸깹깻깼깽꺄꺅꺌꺼꺽꺾껀껄껌껍껏껐껑께껙껜껨껫껭껴껸껼꼇꼈꼍꼐꼬꼭꼰꼲꼴꼼꼽꼿꽁꽂꽃꽈꽉꽐꽜꽝꽤꽥꽹꾀꾄꾈꾐꾑꾕꾜꾸꾹꾼꿀꿇꿈꿉꿋꿍꿎꿔꿜꿨꿩꿰꿱꿴꿸뀀뀁뀄뀌뀐뀔뀜뀝뀨끄끅끈끊끌끎끓끔끕끗끙끝끼끽낀낄낌낍낏낑나낙낚난낟날낡낢남납낫났낭낮낯낱낳내낵낸낼냄냅냇냈냉냐냑냔냘냠냥너넉넋넌널넒넓넘넙넛넜넝넣네넥넨넬넴넵넷넸넹녀녁년녈념녑녔녕녘녜녠노녹논놀놂놈놉놋농높놓놔놘놜놨뇌뇐뇔뇜뇝뇟뇨뇩뇬뇰뇹뇻뇽누눅눈눋눌눔눕눗눙눠눴눼뉘뉜뉠뉨뉩뉴뉵뉼늄늅늉느늑는늘늙늚늠늡늣능늦늪늬늰늴니닉닌닐닒님닙닛닝닢다닥닦단닫달닭닮닯닳담답닷닸당닺닻닿대댁댄댈댐댑댓댔댕댜더덕덖던덛덜덞덟덤덥덧덩덫덮데덱덴델뎀뎁뎃뎄뎅뎌뎐뎔뎠뎡뎨뎬도독돈돋돌돎돐돔돕돗동돛돝돠돤돨돼됐되된될됨됩됫됴두둑둔둘둠둡둣둥둬뒀뒈뒝뒤뒨뒬뒵뒷뒹듀듄듈듐듕드득든듣들듦듬듭듯등듸디딕딘딛딜딤딥딧딨딩딪따딱딴딸땀땁땃땄땅땋때땍땐땔땜땝땟땠땡떠떡떤떨떪떫떰떱떳떴떵떻떼떽뗀뗄뗌뗍뗏뗐뗑뗘뗬또똑똔똘똥똬똴뙈뙤뙨뚜뚝뚠뚤뚫뚬뚱뛔뛰뛴뛸뜀뜁뜅뜨뜩뜬뜯뜰뜸뜹뜻띄띈띌띔띕띠띤띨띰띱띳띵라락란랄람랍랏랐랑랒랖랗래랙랜랠램랩랫랬랭랴략랸럇량러럭런럴럼럽럿렀렁렇레렉렌렐렘렙렛렝려력련렬렴렵렷렸령례롄롑롓로록론롤롬롭롯롱롸롼뢍뢨뢰뢴뢸룀룁룃룅료룐룔룝룟룡루룩룬룰룸룹룻룽뤄뤘뤠뤼뤽륀륄륌륏륑류륙륜률륨륩륫륭르륵른를름릅릇릉릊릍릎리릭린릴림립릿링마막만많맏말맑맒맘맙맛망맞맡맣매맥맨맬맴맵맷맸맹맺먀먁먈먕머먹먼멀멂멈멉멋멍멎멓메멕멘멜멤멥멧멨멩며멱면멸몃몄명몇몌모목몫몬몰몲몸몹못몽뫄뫈뫘뫙뫼묀묄묍묏묑묘묜묠묩묫무묵묶문묻물묽묾뭄뭅뭇뭉뭍뭏뭐뭔뭘뭡뭣뭬뮈뮌뮐뮤뮨뮬뮴뮷므믄믈믐믓미믹민믿밀밂밈밉밋밌밍및밑바박밖밗반받발밝밞밟밤밥밧방밭배백밴밸뱀뱁뱃뱄뱅뱉뱌뱍뱐뱝버벅번벋벌벎범법벗벙벚베벡벤벧벨벰벱벳벴벵벼벽변별볍볏볐병볕볘볜보복볶본볼봄봅봇봉봐봔봤봬뵀뵈뵉뵌뵐뵘뵙뵤뵨부북분붇불붉붊붐붑붓붕붙붚붜붤붰붸뷔뷕뷘뷜뷩뷰뷴뷸븀븃븅브븍븐블븜븝븟비빅빈빌빎빔빕빗빙빚빛빠빡빤빨빪빰빱빳빴빵빻빼빽뺀뺄뺌뺍뺏뺐뺑뺘뺙뺨뻐뻑뻔뻗뻘뻠뻣뻤뻥뻬뼁뼈뼉뼘뼙뼛뼜뼝뽀뽁뽄뽈뽐뽑뽕뾔뾰뿅뿌뿍뿐뿔뿜뿟뿡쀼쁑쁘쁜쁠쁨쁩삐삑삔삘삠삡삣삥사삭삯산삳살삵삶삼삽삿샀상샅새색샌샐샘샙샛샜생샤샥샨샬샴샵샷샹섀섄섈섐섕서석섞섟선섣설섦섧섬섭섯섰성섶세섹센셀셈셉셋셌셍셔셕션셜셤셥셧셨셩셰셴셸솅소속솎손솔솖솜솝솟송솥솨솩솬솰솽쇄쇈쇌쇔쇗쇘쇠쇤쇨쇰쇱쇳쇼쇽숀숄숌숍숏숑수숙순숟술숨숩숫숭숯숱숲숴쉈쉐쉑쉔쉘쉠쉥쉬쉭쉰쉴쉼쉽쉿슁슈슉슐슘슛슝스슥슨슬슭슴습슷승시식신싣실싫심십싯싱싶싸싹싻싼쌀쌈쌉쌌쌍쌓쌔쌕쌘쌜쌤쌥쌨쌩썅써썩썬썰썲썸썹썼썽쎄쎈쎌쏀쏘쏙쏜쏟쏠쏢쏨쏩쏭쏴쏵쏸쐈쐐쐤쐬쐰쐴쐼쐽쑈쑤쑥쑨쑬쑴쑵쑹쒀쒔쒜쒸쒼쓩쓰쓱쓴쓸쓺쓿씀씁씌씐씔씜씨씩씬씰씸씹씻씽아악안앉않알앍앎앓암압앗았앙앝앞애액앤앨앰앱앳앴앵야약얀얄얇얌얍얏양얕얗얘얜얠얩어억언얹얻얼얽얾엄업없엇었엉엊엌엎에엑엔엘엠엡엣엥여역엮연열엶엷염엽엾엿였영옅옆옇예옌옐옘옙옛옜오옥온올옭옮옰옳옴옵옷옹옻와왁완왈왐왑왓왔왕왜왝왠왬왯왱외왹왼욀욈욉욋욍요욕욘욜욤욥욧용우욱운울욹욺움웁웃웅워웍원월웜웝웠웡웨웩웬웰웸웹웽위윅윈윌윔윕윗윙유육윤율윰윱윳융윷으윽은을읊음읍읏응읒읓읔읕읖읗의읜읠읨읫이익인일읽읾잃임입잇있잉잊잎자작잔잖잗잘잚잠잡잣잤장잦재잭잰잴잼잽잿쟀쟁쟈쟉쟌쟎쟐쟘쟝쟤쟨쟬저적전절젊점접젓정젖제젝젠젤젬젭젯젱져젼졀졈졉졌졍졔조족존졸졺좀좁좃종좆좇좋좌좍좔좝좟좡좨좼좽죄죈죌죔죕죗죙죠죡죤죵주죽준줄줅줆줌줍줏중줘줬줴쥐쥑쥔쥘쥠쥡쥣쥬쥰쥴쥼즈즉즌즐즘즙즛증지직진짇질짊짐집짓징짖짙짚짜짝짠짢짤짧짬짭짯짰짱째짹짼쨀쨈쨉쨋쨌쨍쨔쨘쨩쩌쩍쩐쩔쩜쩝쩟쩠쩡쩨쩽쪄쪘쪼쪽쫀쫄쫌쫍쫏쫑쫓쫘쫙쫠쫬쫴쬈쬐쬔쬘쬠쬡쭁쭈쭉쭌쭐쭘쭙쭝쭤쭸쭹쮜쮸쯔쯤쯧쯩찌찍찐찔찜찝찡찢찧차착찬찮찰참찹찻찼창찾채책챈챌챔챕챗챘챙챠챤챦챨챰챵처척천철첨첩첫첬청체첵첸첼쳄쳅쳇쳉쳐쳔쳤쳬쳰촁초촉촌촐촘촙촛총촤촨촬촹최쵠쵤쵬쵭쵯쵱쵸춈추축춘출춤춥춧충춰췄췌췐취췬췰췸췹췻췽츄츈츌츔츙츠측츤츨츰츱츳층치칙친칟칠칡침칩칫칭카칵칸칼캄캅캇캉캐캑캔캘캠캡캣캤캥캬캭컁커컥컨컫컬컴컵컷컸컹케켁켄켈켐켑켓켕켜켠켤켬켭켯켰켱켸코콕콘콜콤콥콧콩콰콱콴콸쾀쾅쾌쾡쾨쾰쿄쿠쿡쿤쿨쿰쿱쿳쿵쿼퀀퀄퀑퀘퀭퀴퀵퀸퀼큄큅큇큉큐큔큘큠크큭큰클큼큽킁키킥킨킬킴킵킷킹타탁탄탈탉탐탑탓탔탕태택탠탤탬탭탯탰탱탸턍터턱턴털턺텀텁텃텄텅테텍텐텔템텝텟텡텨텬텼톄톈토톡톤톨톰톱톳통톺톼퇀퇘퇴퇸툇툉툐투툭툰툴툼툽툿퉁퉈퉜퉤튀튁튄튈튐튑튕튜튠튤튬튱트특튼튿틀틂틈틉틋틔틘틜틤틥티틱틴틸팀팁팃팅파팍팎판팔팖팜팝팟팠팡팥패팩팬팰팸팹팻팼팽퍄퍅퍼퍽펀펄펌펍펏펐펑페펙펜펠펨펩펫펭펴편펼폄폅폈평폐폘폡폣포폭폰폴폼폽폿퐁퐈퐝푀푄표푠푤푭푯푸푹푼푿풀풂품풉풋풍풔풩퓌퓐퓔퓜퓟퓨퓬퓰퓸퓻퓽프픈플픔픕픗피픽핀필핌핍핏핑하학한할핥함합핫항해핵핸핼햄햅햇했행햐향허헉헌헐헒험헙헛헝헤헥헨헬헴헵헷헹혀혁현혈혐협혓혔형혜혠혤혭호혹혼홀홅홈홉홋홍홑화확환활홧황홰홱홴횃횅회획횐횔횝횟횡효횬횰횹횻후훅훈훌훑훔훗훙훠훤훨훰훵훼훽휀휄휑휘휙휜휠휨휩휫휭휴휵휸휼흄흇흉흐흑흔흖흗흘흙흠흡흣흥흩희흰흴흼흽힁히힉힌힐힘힙힛힝 &lt;br /&gt;　、。&amp;middot;‥&amp;hellip;&amp;uml;〃&amp;shy;―∥＼&amp;sim;&amp;lsquo;&amp;rsquo;&amp;ldquo;&amp;rdquo;〔〕〈〉《》「」『』【】&amp;plusmn;&amp;times;&amp;divide;&amp;ne;&amp;le;&amp;ge;&amp;infin;&amp;there4;&amp;deg;&amp;prime;&amp;Prime;℃&amp;Aring;￠￡￥♂♀&amp;ang;&amp;perp;⌒&amp;part;&amp;nabla;&amp;equiv;≒&amp;sect;※☆★○●◎◇◆□■△▲▽▼&amp;rarr;&amp;larr;&amp;uarr;&amp;darr;&amp;harr;〓≪≫&amp;radic;∽&amp;prop;∵&amp;int;∬&amp;isin;&amp;ni;&amp;sube;&amp;supe;&amp;sub;&amp;sup;&amp;cup;&amp;cap;&amp;and;&amp;or;￢&amp;rArr;&amp;hArr;&amp;forall;&amp;exist;&amp;acute;～ˇ˘˝˚˙&amp;cedil;˛&amp;iexcl;&amp;iquest;ː∮&amp;sum;&amp;prod;&amp;curren;℉&amp;permil;◁◀▷▶♤&amp;spades;♡&amp;hearts;♧&amp;clubs;⊙◈▣◐◑▒▤▥▨▧▦▩♨☏☎☜☞&amp;para;&amp;dagger;&amp;Dagger;↕↗↙↖↘♭♩♪♬㉿㈜№㏇&amp;trade;㏂㏘℡&amp;euro;&amp;reg;㉾！＂＃＄％＆＇（）＊＋，－．／０１２３４５６７８９：；＜＝＞？＠ＡＢＣＤＥＦＧＨＩＪＫＬＭＮＯＰＱＲＳＴＵＶＷＸＹＺ［￦］＾＿｀ａｂｃｄｅｆｇｈｉｊｋｌｍｎｏｐｑｒｓｔｕｖｗｘｙｚ｛｜｝￣ㄱㄲㄳㄴㄵㄶㄷㄸㄹㄺㄻㄼㄽㄾㄿㅀㅁㅂㅃㅄㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎㅏㅐㅑㅒㅓㅔㅕㅖㅗㅘㅙㅚㅛㅜㅝㅞㅟㅠㅡㅢㅣㅤㅥㅦㅧㅨㅩㅪㅫㅬㅭㅮㅯㅰㅱㅲㅳㅴㅵㅶㅷㅸㅹㅺㅻㅼㅽㅾㅿㆀㆁㆂㆃㆄㆅㆆㆇㆈㆉㆊㆋㆌㆍㆎⅰⅱⅲⅳⅴⅵⅶⅷⅸⅹⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩ&amp;Alpha;&amp;Beta;&amp;Gamma;&amp;Delta;&amp;Epsilon;&amp;Zeta;&amp;Eta;&amp;Theta;&amp;Iota;&amp;Kappa;&amp;Lambda;&amp;Mu;&amp;Nu;&amp;Xi;&amp;Omicron;&amp;Pi;&amp;Rho;&amp;Sigma;&amp;Tau;&amp;Upsilon;&amp;Phi;&amp;Chi;&amp;Psi;&amp;Omega;&amp;alpha;&amp;beta;&amp;gamma;&amp;delta;&amp;epsilon;&amp;zeta;&amp;eta;&amp;theta;&amp;iota;&amp;kappa;&amp;lambda;&amp;mu;&amp;nu;&amp;xi;&amp;omicron;&amp;pi;&amp;rho;&amp;sigma;&amp;tau;&amp;upsilon;&amp;phi;&amp;chi;&amp;psi;&amp;omega;─│┌┐┘└├┬┤┴┼━┃┏┓┛┗┣┳┫┻╋┠┯┨┷┿┝┰┥┸╂┒┑┚┙┖┕┎┍┞┟┡┢┦┧┩┪┭┮┱┲┵┶┹┺┽┾╀╁╃╄╅╆╇╈╉╊㎕㎖㎗ℓ㎘㏄㎣㎤㎥㎦㎙㎚㎛㎜㎝㎞㎟㎠㎡㎢㏊㎍㎎㎏㏏㎈㎉㏈㎧㎨㎰㎱㎲㎳㎴㎵㎶㎷㎸㎹㎀㎁㎂㎃㎄㎺㎻㎼㎽㎾㎿㎐㎑㎒㎓㎔&amp;Omega;㏀㏁㎊㎋㎌㏖㏅㎭㎮㎯㏛㎩㎪㎫㎬㏝㏐㏓㏃㏉㏜㏆&amp;AElig;&amp;ETH;&amp;ordf;ĦĲĿŁ&amp;Oslash;&amp;OElig;&amp;ordm;&amp;THORN;ŦŊ㉠㉡㉢㉣㉤㉥㉦㉧㉨㉩㉪㉫㉬㉭㉮㉯㉰㉱㉲㉳㉴㉵㉶㉷㉸㉹㉺㉻ⓐⓑⓒⓓⓔⓕⓖⓗⓘⓙⓚⓛⓜⓝⓞⓟⓠⓡⓢⓣⓤⓥⓦⓧⓨⓩ①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮&amp;frac12;⅓⅔&amp;frac14;&amp;frac34;⅛⅜⅝⅞&amp;aelig;đ&amp;eth;ħıĳĸŀł&amp;oslash;&amp;oelig;&amp;szlig;&amp;thorn;ŧŋŉ㈀㈁㈂㈃㈄㈅㈆㈇㈈㈉㈊㈋㈌㈍㈎㈏㈐㈑㈒㈓㈔㈕㈖㈗㈘㈙㈚㈛⒜⒝⒞⒟⒠⒡⒢⒣⒤⒥⒦⒧⒨⒩⒪⒫⒬⒭⒮⒯⒰⒱⒲⒳⒴⒵⑴⑵⑶⑷⑸⑹⑺⑻⑼⑽⑾⑿⒀⒁⒂&amp;sup1;&amp;sup2;&amp;sup3;⁴ⁿ₁₂₃₄ぁあぃいぅうぇえぉおかがきぎくぐけげこごさざしじすずせぜそぞただちぢっつづてでとどなにぬねのはばぱひびぴふぶぷへべぺほぼぽまみむめもゃやゅゆょよらりるれろゎわゐゑをんァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶАБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдеёжзийклмнопрстуфхцчшщъыьэюя &lt;br /&gt;갋갣걥겷괐괢굠굥궸귕귬긂긇긓깄깯꺆꺍껓껕꼉꼳꽅꽸꿘뀰뀼낻냗냡냣냬넏넢넫녇녱놁놑놰뇄뇡뇸눍눝뉻늗늧늼닁닏닽댠됭둗둚뒙딮딷똠똡똣똭똰뙇뙜뚧뜳뜽뜾랃랟랲럔럲럳렜렫롣롹뢔뤤맜맟맫먄몱뫠뫴뭥뮊뮹믁믕믜밷뱜뱡볌볻볿봥뵴붠붴뷁븡븨빋빧뺜뽓뾱뿕뿝쀠쁭샏샾섁섿셱솀솁솓쇵숖슌싥싳싿쎔쎠쎤쎵쎼쏼쑝쒐쒬씃씿앋앜얬얭옏옝옦옫왘왭왰욷웇웟웻윾읩읭읻잌잍쟵젇젉좬즒즤짣짲쫃쫒쬲쮓찓찟쵀췍칢칮칰칻캨캰컄켘콛쾃쿈쿽퀌퀜퀠큲킄탇턻톧퇻툶퉷팓팤팯펵퐉핰핳핻햏햔햣헗헠헡헣헿홥홨횽훕흝힣&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한글 자모, 알파벳, 숫자, 구두점을 기본으로 한국 산업 규격으로 지정된 한국어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자 집합 KS X 1001 표준과 아래 논문에서 제시된 글자 224자를 추가해 사용합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;KS X 1001 한글 2350자 (&lt;a href=&quot;https://ko.wikipedia.org/wiki/KS_X_1001_%ED%95%9C%EA%B8%80_%EB%B6%80%EB%B6%84_%ED%91%9C&quot;&gt;위키피디아&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;KS X 1001 특수문자 (&lt;a href=&quot;https://ko.wikipedia.org/wiki/KS_X_1001%EC%9D%98_%ED%8A%B9%EC%88%98_%EB%AC%B8%EC%9E%90&quot;&gt;위키피디아&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;KS 코드 완성형 한글의 추가 글자 제안 224자 (&lt;a href=&quot;http://koreantypography.org/wp-content/uploads/2016/02/kst_12_7_2_06.pdf&quot;&gt;논문 링크&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;서브셋 폰트 적용 방법&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;서브셋 생성하기:&lt;/b&gt; 원하는 글꼴의 서브셋을 만들기 위해서는 글꼴 파일에서 필요한 문자만을 추출해야 합니다. 이 작업은 다양한 온라인 툴(예: Font Squirrel의 Webfont Generator, Google Webfonts Helper 등)을 통해 할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;웹사이트에 서브셋 폰트 적용하기:&lt;/b&gt; 서브셋 글꼴을 생성한 후, 해당 글꼴 파일을 웹사이트에 포함시키고 CSS를 통해 적용합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1699417641711&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!-- HTML 내에서 사용자의 웹페이지에 서브셋 폰트를 포함 --&amp;gt;
&amp;lt;link href=&quot;path/to/your/subset-font.css&quot; rel=&quot;stylesheet&quot; type=&quot;text/css&quot;&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1699417649518&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/* CSS 파일 또는 &amp;lt;style&amp;gt; 태그 내에 서브셋 폰트 적용 */
@font-face {
    font-family: 'YourSubsetFont';
    src: url('path/to/your/subset-font.woff2') format('woff2'),
         url('path/to/your/subset-font.woff') format('woff');
    font-weight: normal;
    font-style: normal;
}

body {
    font-family: 'YourSubsetFont', sans-serif;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주의사항&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서브셋 글꼴을 사용할 때는 저작권과 라이선스를 확인해야 합니다. 모든 글꼴이 서브셋으로 제작되어 재배포될 수 있는 것은 아니기 때문입니다.&lt;/li&gt;
&lt;li&gt;사용자가 입력하는 텍스트나 동적으로 변하는 컨텐츠에 대해 서브셋 글꼴을 사용할 때는 주의가 필요합니다. 서브셋 글꼴에 포함되지 않은 문자가 사용되면, 해당 문자는 보이지 않거나 기본 글꼴로 대체될 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;772&quot; data-origin-height=&quot;461&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baHf8t/btszX2WOtK9/j9h3Ig1uSfKxk1ZdtLhHR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baHf8t/btszX2WOtK9/j9h3Ig1uSfKxk1ZdtLhHR1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baHf8t/btszX2WOtK9/j9h3Ig1uSfKxk1ZdtLhHR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaHf8t%2FbtszX2WOtK9%2Fj9h3Ig1uSfKxk1ZdtLhHR1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;772&quot; height=&quot;461&quot; data-origin-width=&quot;772&quot; data-origin-height=&quot;461&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>ETC</category>
      <category>서브셋 폰트</category>
      <category>웹 폰트 경량화</category>
      <author>오병문</author>
      <guid isPermaLink="true">https://develop-obm.tistory.com/119</guid>
      <comments>https://develop-obm.tistory.com/119#entry119comment</comments>
      <pubDate>Wed, 8 Nov 2023 13:30:49 +0900</pubDate>
    </item>
    <item>
      <title>[JS] 제네레이터</title>
      <link>https://develop-obm.tistory.com/118</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;JavaScript에서의 제네레이터는 ECMAScript 6 (ES6)에서 도입된 기능으로, 이터러블(iterable) 프로토콜을 구현하는 함수입니다. JavaScript의 제네레이터 함수는 function* 키워드로 정의되며, yield 키워드를 사용하여 값을 반환하고 함수 실행을 일시 중지할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주요 특징&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;일시 중지와 재시작&lt;/b&gt;: 제네레이터는 실행 중인 상태를 저장하고 나중에 그 상태를 복원하여 계속 실행할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;yield 키워드&lt;/b&gt;: 제네레이터 함수 내에서 yield를 사용하여 값을 반환하고 실행을 일시 중지합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;iterator 객체&lt;/b&gt;: 제네레이터 함수를 호출하면 이터레이터 객체가 반환되며, 이 객체의 next() 메서드를 사용하여 함수를 계속 실행하고 다음 yield 값까지 가져올 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1695177610172&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function* numberGenerator() {
    yield 1;
    yield 2;
    yield 3;
}

const gen = numberGenerator();

console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gen.next()를 호출할 때마다 제네레이터 함수가 yield로 정의된 위치까지 실행되고, 그 위치의 값을 반환합니다. 모든 yield를 지나면 done 프로퍼티는 true 값을 가집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;yield와 값 전달하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제네레이터의 next() 메서드는 값을 받아 제네레이터 함수로 전달할 수 있습니다. 이를 통해 외부와 제네레이터 간의 양방향 통신이 가능합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1695177659360&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function* generatorFunction() {
    const first = yield 'First yield';
    const second = yield first + ' Second yield';
    yield second + ' Third yield';
}

const gen = generatorFunction();

console.log(gen.next());          // { value: 'First yield', done: false }
console.log(gen.next('Hello'));  // { value: 'Hello Second yield', done: false }
console.log(gen.next('World'));  // { value: 'World Third yield', done: false }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예제에서 첫 번째 next() 호출 후, 두 번째 next('Hello') 호출에서 'Hello'는 first 변수에 할당됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 JavaScript의 제네레이터는 함수의 실행 흐름을 중간에 일시 중지하고 다시 시작할 수 있어, 비동기 프로그래밍, 코루틴 등 다양한 고급 프로그래밍 패턴에 활용될 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;활용 사례&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;대용량 파일 처리&lt;/b&gt;: 대용량 로그 파일과 같은 큰 파일을 한 번에 읽기 대신, 한 번에 한 줄씩 읽을 때 제네레이터를 사용할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;무한 시퀀스&lt;/b&gt;: 특정 패턴으로 무한히 반복되는 시퀀스를 생성할 때 제네레이터를 사용할 수 있습니다. (예: 숫자의 무한 시퀀스)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;파이프라인&lt;/b&gt;: 데이터 처리 단계를 연결할 때, 각 단계를 제네레이터로 구현하여 연결할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>JS</category>
      <category>제네레이터</category>
      <author>오병문</author>
      <guid isPermaLink="true">https://develop-obm.tistory.com/118</guid>
      <comments>https://develop-obm.tistory.com/118#entry118comment</comments>
      <pubDate>Wed, 20 Sep 2023 11:42:39 +0900</pubDate>
    </item>
    <item>
      <title>[NestJS] .CSV 파일을 JSON 형태로 변환하기</title>
      <link>https://develop-obm.tistory.com/117</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;변환에 앞서 새프로젝트 구성을 마쳤다는 가정하에 진행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변환에 필요한 파일 업로드를 처리할 엔드포인트 컨트롤러 생성&lt;/p&gt;
&lt;pre id=&quot;code_1691107765221&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;app.controller.ts

import {
  Controller,
  Post,
  UploadedFile,
  UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { ExcelService } from './excel.service';

@Controller()
export class AppController {
  constructor(private readonly excelService: ExcelService) {}

  @Post('/upload') // 데코레이터를 사용하여 '/upload' 엔드포인트에서 POST 요청을 처리
  @UseInterceptors(FileInterceptor('file')) // multipart/form-data로 전송된 파일을 받아옴
  async uploadFile(@UploadedFile() file) { // 업로드된 파일은 uploadFile(@UploadedFile() file) 함수의 인자로 주입
    const data = await this.excelService.convertCsvToJson(file.path);
    console.log(data);
    return data;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 필요한 서비스나 컨트롤러 등록을 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1691107862738&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ExcelService } from './excel.service';
import { MulterModule } from '@nestjs/platform-express'; // 파일 업로드를 위한 설정을 등록
import { multerOptions } from './multer.options';

@Module({
  imports: [MulterModule.register(multerOptions)],
  controllers: [AppController],
  providers: [AppService, ExcelService], // provider로 등록하여, NestJS의 DI(Dependency Injection) 컨테이너를 통해 필요한 곳에 주입할 수 있도록 처리
})
export class AppModule {}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 실제 변환을 담당할 서비스 기능을 정의합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1691107978935&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;excel.service.ts

import { createReadStream } from 'fs';
import { Injectable } from '@nestjs/common';
import * as csv from 'csv-parser';

@Injectable()
export class ExcelService {
// 받아온 파일 경로를 사용하여 해당 파일을 읽고,
// 'csv-parser' 라이브러리를 이용하여 CSV 데이터를 JSON으로 변환하는 작업을 수행
// Promise를 반환하며, 이 Promise는 CSV 파일의 모든 데이터를 JSON으로 변환한 결과를 담은 배열을 resolve 합니다
    convertCsvToJson(filePath: string): Promise&amp;lt;any[]&amp;gt; {
        const results = [];
    
        return new Promise((resolve, reject) =&amp;gt; {
            createReadStream(filePath, { encoding: 'utf-8' })
                .pipe(csv({ separator: '\t' }))  // 탭을 구분자로 사용
                .on('data', (data) =&amp;gt; results.push(data))
                .on('end', () =&amp;gt; resolve(results))
                .on('error', (error) =&amp;gt; reject(error));
        });
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 multer 모듈의 설정을 정의합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1691108091880&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;multer.options.ts

import { diskStorage } from 'multer';
import { parse } from 'path';
import { v4 as uuid } from 'uuid';

export const multerOptions = {
// 업로드된 파일을 서버의 './uploads' 디렉토리에 저장하도록 설정
  storage: diskStorage({
    destination: './uploads',
    filename: (req, file, cb) =&amp;gt; {
      const filename: string =
      // 저장될 파일의 이름을 원래 파일 이름에서 공백을 제거하고 uuid를 추가하여 설정
        parse(file.originalname).name.replace(/\s/g, '') + uuid();
      const extension: string = parse(file.originalname).ext;

      cb(null, `${filename}${extension}`);
    },
  }),
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 각 파일들이 함께 작동하여 사용자가 '/upload' 엔드포인트로 CSV 파일을 업로드하면, 서버가 이 파일을 받아서 JSON으로 변환한 뒤 그 결과를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과 화면&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;921&quot; data-origin-height=&quot;724&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjbKS0/btspTKlwzwe/4D3wML9bZ23fMXhpDqFMnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjbKS0/btspTKlwzwe/4D3wML9bZ23fMXhpDqFMnK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjbKS0/btspTKlwzwe/4D3wML9bZ23fMXhpDqFMnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcjbKS0%2FbtspTKlwzwe%2F4D3wML9bZ23fMXhpDqFMnK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;921&quot; height=&quot;724&quot; data-origin-width=&quot;921&quot; data-origin-height=&quot;724&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>NestJS</category>
      <category>.csv</category>
      <category>Json 변환</category>
      <author>오병문</author>
      <guid isPermaLink="true">https://develop-obm.tistory.com/117</guid>
      <comments>https://develop-obm.tistory.com/117#entry117comment</comments>
      <pubDate>Fri, 4 Aug 2023 09:16:30 +0900</pubDate>
    </item>
  </channel>
</rss>