±âŸ(framework)
2018.03.21 / 01:55
Firebase Web äÆÃ¾Û ¸¸µé±â - Cloud Messaging°ú FunctionsÀ» ÀÌ¿ëÇÑ Çª½Ã¸Þ¼¼Áö ±â´É - Functions¸¦
ÈÞ°í
Ãßõ ¼ö 205
FCM ¹ß¼Û ÀÛ¾÷À» Ŭ¶óÀ̾ðÆ® Äڵ忡¼µµ ÇÒ ¼ö ÀÖÀ¸³ª, Firebase Messaging Server API Key°¡ Ŭ¶óÀ̾ðÆ® Äڵ忡 Æ÷ÇԵǴ °ÍÀº º¸¾È»ó ÁÁÀº ¹æ¹ýÀÌ ¾Æ´Ï¹Ç·Î ¼¹ö¸¦ »ç¿ëÇؾßÇÕ´Ï´Ù. ¿©±â¼ óÀ½À¸·Î Firebase Functions ¸¦ ÀÌ¿ëÇغ¼ °ÍÀÔ´Ï´Ù.
Functions ´Â Firebase ¼ºñ½ºµéÀÌ µ¿ÀÛÇÏ¸é¼ ¹ß»ýÇÏ´Â À̺¥Æ®¸¦ ¹Þ¾Æ ¼¹ö¿¡¼ Firebase AdminÀ» ÅëÇÏ¿© Firebase ¼ºñ½ºµéÀ» ±¸µ¿½ÃÅ°´Â Äڵ带 ¼öÇàÇÕ´Ï´Ù. Functions°¡ ¹Þ´Â À̺¥Æ®´Â ¾Æ·¡¿Í °°½À´Ï´Ù.
- Reatime Database Æ®¸®°Å
- onWrite() - ½Ç½Ã°£ µ¥ÀÌÅͺ£À̽º¿¡¼ µ¥ÀÌÅÍ°¡ »ý¼º, Æó±â ¶Ç´Â º¯°æµÉ ¶§ ¹ß»ý
- onCreate() - ½Ç½Ã°£ µ¥ÀÌÅͺ£À̽º¿¡¼ »õ µ¥ÀÌÅÍ°¡ »ý¼º ½Ã ¹ß»ý
- onUpdate() - ½Ç½Ã°£ µ¥ÀÌÅͺ£À̽º¿¡¼ µ¥ÀÌÅÍ°¡ ¾÷µ¥ÀÌÆ®µÉ ½Ã ¹ß»ý
- onDelete() - ½Ç½Ã°£ µ¥ÀÌÅͺ£À̽º¿¡¼ µ¥ÀÌÅÍ°¡ »èÁ¦µÉ ½Ã ¹ß»ý - Authentication Æ®¸®°Å
- onCreate() - AuthenticationÀ» ÅëÇØ »ç¿ëÀÚ »ý¼º ½Ã ¹ß»ý
- onDelete() - AuthenticationÀ» ÅëÇØ »ç¿ëÀÚ »èÁ¦ ½Ã ¹ß»ý - Hosting Æ®¸®°Å
- onRequest() - HostingÀ¸·Î Http ¿äûÀÌ ÀÖÀ» ½Ã ¹ß»ý - Storage Æ®¸®°Å
- onChange() - Storge³»¿¡ °³Ã¼µéÀÌ »ý¼º ¼öÁ¤ ¹× »èÁ¦ ½Ã ¹ß»ý - Messaging Æ®¸®°Å
- onPublish() - Message - Google Analytics Æ®¸®°Å
- onLog - Google Analytics Á¤ÀÇµÈ À̺¥Æ®¿¡ ·Î±×°¡ ½×ÀÌ¸é ¹ß»ý
ÀÌ·¯ÇÑ À̺¥Æ® Æ®¸®°Å Áß¿¡ MessagingÀº Realtime Database Æ®¸®°Å Áß »õ ¸Å¼¼Áö Àü¼Û ½Ã¿¡ Áï ¼¼ ¸Þ¼¼Áö µ¥ÀÌÅÍ°¡ »ý¼ºÀÌ µÇ´Â onCreate½Ã¿¡ äÆùæ À¯Àúµé Áß¿¡ Á¢¼ÓµÇÁö ¾ÊÀº À¯Àúµé¿¡°Ô¸¸ º¸³»´Â Äڵ带 ÀÛ¼ºÇغ¸°Ú½À´Ï´Ù.
À̹ø¿¡ ÄÚµå´Â index.html¾Æ·¡°¡ ¾Æ´Ñ ÇÁ·ÎÁ§Æ®¿¡ functions Æú´õ ¾Æ·¡¿¡ index.js ¸¦ ¿¾îº¾´Ï´Ù. ¿¸é ±âº»ÀûÀ¸·Î ¾Æ·¡ÀÇ Äڵ尡 ÀÖ½À´Ï´Ù.
const functions = require('firebase-functions');
// // Create and Deploy Your First Cloud Functions
//
// exports.helloWorld = functions.https.onRequest((request, response) => {
// response.send("Hello from Firebase!");
// });
±×¸®°í ÇÁ·ÎÁ§Æ®¸¦ ½ÃÀÛÇÏ¸é¼ firebase-tools¸¦ ÅëÇÏ¿© ÇÁ·ÎÁ§Æ®¸¦ ¼³Á¤ÇÒ ¶§ npm À¸·Î Depency¸¦ ¼³Ä¡ÇϰڳĴ Áú¹®¿¡ yes¸¦ ´©¸£¼ÌÀ» °ÍÀÔ´Ï´Ù. ¸¸¾à¿¡ ¼³Ä¡¸¦ ÇÏÁö ¾Ê°í ÁøÇàÇϼ̴ٸé, ´Ù½Ã À©µµ¿ìÁî¿¡ Ä¿¸Çµå ⠶Ǵ ¸ÆÀ̳ª ¸®´ª½ºÀÇ Å͹̳ÎÀ» ½ÇÇàÇÏ¿© ÇÁ·ÎÁ§Æ® functions Æú´õ °æ·Î¿¡¼ ¾Æ·¡ÀÇ ¸í·É¾î¸¦ ½ÇÇàÇÕ´Ï´Ù.
npm install
±×·¯¸é 'firebase-functions'¿Í 'firebase-admin' ¶óÀ̺귯¸®°¡ ¼³Ä¡°¡ µÉ °ÍÀÔ´Ï´Ù.
index.js Àüü ÄÚµå´Â ¾Æ·¡¿Í °°½À´Ï´Ù. ±âº»ÀûÀ¸·Î ÀÛ¼ºµÇ¾î ÀÖ´Â ÄÚµå´Â Áö¿ì°í ¾Æ·¡ÀÇ Äڵ带 ÀÔ·ÂÇØÁÖ¼¼¿ä.
Äڵ带 Çѹø »ìÆ캸°Ú½À´Ï´Ù. ¿ì¼± Äڵ尡 ES 6 Äڵ尡 Æ÷ÇԵǾî ÀÖ½À´Ï´Ù. Firebase Functions´Â ÇöÀç(2017.11.25) node.js 6.11.5 ¹öÀüÀ¸·Î ¼ºñ½º°¡ ¿î¿ëµÇ°í ÀÖ½À´Ï´Ù. 6.11.5 ¹öÀüÀÇ node.js´Â 100ÆÛ¼¾Æ®´Â ¾Æ´ÏÁö¸¸ 99ÆÛ¼¾Æ®¿¡ °¡±î¿î ES 6 Áö¿øÀ» ÇÕ´Ï´Ù.
¿ì¼± ¼ºñ½º »ç¿ëÀ» À§ÇØ functions¿Í admin ¸ðµâÀ» »ý¼ºÇÏ°í, adminÀ» ÃʱâÈ ÇÏ´Â ÄÚµå ÀÔ´Ï´Ù.
const functions = require('firebase-functions');
const admin = require('firebase-admin');
node.js´Â npmÀ» ÅëÇØ ¼³Ä¡ÇÑ ¶óÀ̺귯¸®¸¦ require¶ó´Â Àü¿ªÇÔ¼ö¸¦ ÅëÇØ ºÒ·¯¿É´Ï´Ù. ´Ù¸¥ node ¶óÀ̺귯¸®¸¦ ¼³Ä¡ÇϽðí require ¸í·É¾î¸¦ ÅëÇØ ºÒ·¯¿Í »ç¿ëÇÒ ¼ö ÀÖ½À´Ï´Ù.
¾Æ·¡ÀÇ ÄÚµå´Â firebase-admin ¶óÀ̺귯¸®¸¦ ÅëÇØ FirebaseÀÇ ´Ù¸¥ ¼ºñ½º¸¦ Á¢±ÙÇÒ¼ö ÀÖ°Ô ÇÕ´Ï´Ù.
admin.initializeApp(functions.config().firebase);
¼¹ö°¡ Äڵ带 ÀÛ¼ºÇÏ´Â °³¹ßÀÚÀÇ ¼¹ö°¡¾Æ´Ï¶ó ±¸±Û¿¡¼ ¿î¿µÇÏ´Â ¼¹öÀ̱⠶§¹®¿¡ Å°°¡ ÇÊ¿ä ¾øÀÌ À§ÀÇ ÄÚµå·Î ÃʱâÈ°¡ µË´Ï´Ù. ¸¸¾à¿¡ Firebase adminÀ» ÀÚü ¼¹ö¿¡¼ ¿î¿µÇÑ´Ù¸é À§ ÄÚµå´Â µ¿ÀÛÇÏÁö ¾Ê½À´Ï´Ù. Á÷Á¢ ¼¹ö¸¦ ¿î¿µÇÑ´Ù¸é ¾Æ·¡¿Í °°Àº ÄÚµå·Î ´ë½ÅÇÏ°Ô µË´Ï´Ù.
var admin = require("firebase-admin");
var serviceAccount = require("path/to/serviceAccountKey.json");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
});
¿©±â¼ serviceAccountKey.jsonÀº ¾Æ·¡ÀÇ Firebase Console ȸ鿡¼ jsonÆÄÀÏÀ» ´Ù¿î·Îµå ¹ÞÀ» ¼ö ÀÖ½À´Ï´Ù.
ÇÁ·ÎÁ§Æ® ¼³Á¤¿¡´Â Firebase ¼ºñ½º¸¦ ÀÌ¿ëÇϴµ¥ ÇÊ¿äÇÑ °¢Á¾ API Å°µéÀ» È®ÀÎÇÒ ¼ö Àִµ¥ '¼ºñ½º °èÁ¤' ÅÇÀ¸·Î µé¾î°©´Ï´Ù.
¼ºñ½º °èÁ¤ÅÇ È¸éÀ» º¸¸é ´Ù½Ã ÁÂÃø¿¡ 'Firebase Admin SDK' °¡ ¼±ÅõǾîÁ® ÀÖ°í, ³»¿ëÀÌ ÀÖ´Â È¸é ¾Æ·¡¿¡ '»õ ºñ°ø°³ Å° »ý¼º' À̶ó´Â ¹öÆ°ÀÌ ÀÖ½À´Ï´Ù. ÀÌ ¹öÆ°À» Ŭ¸¯ ÇϽøé json ÆÄÀÏÀ» Çϳª ´Ù¿î·ÎµåÇÏ°Ô µË´Ï´Ù.
Firebase AdminÀ» ÃʱâÈ ÇÏ´Â ÄÚµå ¾Æ·¡¿¡´Â º»°ÝÀûÀ¸·Î Realtime DatabseÀÇ À̺¥Æ®¸¦ ¹Þ¾Æ ¿ì¸®°¡ ¿øÇÏ´Â ·ÎÁ÷À» ¼öÇàÇÏ´Â Äڵ尡 ÀÛ¼ºµÇ°Ô µË´Ï´Ù.
exports.sendNotification = functions.database.ref('Messages/{roomId}/{messageId}').onCreate(event => {
... (»ý·«)
});
Firebase Functions°¡ ¼öÇàÇϱ⠹ٶó´Â ¸Þ¼Òµå¸¦ exports ÇؾßÇÕ´Ï´Ù. exportÇÏ´Â ¸Þ¼ÒµåÀÇ ¸íĪÀº ¿øÇÏ´Â ¸íĪÀ¸·Î Á¤ÇÏ½Ã¸é µË´Ï´Ù.
Realtime Database°æ·Î°¡ ÀÔ·ÂÀÌ µÇ¾î Àִµ¥ {roomId}/{messageId} ¾î¶² °ªÀÌ ¿Íµµ µÇ¸ç, Áß°ýÈ£ µÇ¾î ÀÖ´Â ºÎºÐÀº ³ªÁß¿¡ À̺¥Æ®°¡ ¹ß»ýÇÑ ÈÄ ÄݹéÇÔ¼ö¿¡¼ ÁÖ¾îÁö´Â À̺¥Æ®°´Ã¼ ¾È¿¡ Æ÷ÇԵǾî Àü´ÞµÇ¾îÁý´Ï´Ù. 'Messages/{roomId}/{messageId}' ÀÌ À§Ä¡¿¡ µ¥ÀÌÅÍ°¡ »õ·Î »ý¼ºÀÌ µÇ¸é À̺¥Æ®°¡ ¹ß»ýÇÕ´Ï´Ù.
onCreate Äݹé ÇÔ¼ö´Â ES6 ¹®¹ýÀÎ È»ìÇ¥ ÇÔ¼ö·Î µÇ¾î ÀÖ½À´Ï´Ù. ¾Æ·¡¿Í °°´Ù°í º¸¸é µË´Ï´Ù. È»ìÇ¥ ÇÔ¼ö¿Í ÀÏ¹Ý ¹«¸íÇÔ¼ö¸¦ ÄݹéÇÔ¼ö·Î µÎ´Â °Í¿¡´Â ¾à°£ÀÇ Â÷ÀÌ°¡ ÀÖ½À´Ï´Ù. È»ìÇ¥ ÇÔ¼ö´Â ÀÚ½ÅÀÇ °íÀ¯ÇÑ this¸¦ °¡ÁöÁö ¾Ê½À´Ï´Ù.
function(event){
... (»ý·«)
}
ÄݹéÇÔ¼ö ³»ºÎ¸¦ »ìÆ캸°Ú½À´Ï´Ù. ¾Æ·¡¿Í °°ÀÌ RoomUsers À§Ä¡¿¡¼ ¸Þ¼¼Áö º¸³½ °÷ÀÇ ¹æ À¯Àúµé ¸®½ºÆ®¸¦ ±¸Çϱâ À§ÇÏ¿© once¸Þ¼ÒµåÀÇ promise ¹Þ°í, UserConnection À§Ä¡¿¡¼ connection°ªÀÌ true ÀÎ °ªµéÀ» ±¸Çϱâ À§ÇÑ promise¸¦ ¹Þ¾Ò½À´Ï´Ù.
const promiseRoomUserList = admin.database().ref(`RoomUsers/${roomId}`).once('value'); // äÆùæ À¯Àú¸®½ºÆ®
const promiseUsersConnection = admin.database().ref('UsersConnection').orderByChild('connection').equalTo(true).once('value');
UserConnection À§Ä¡¿¡¼ µ¥ÀÌÅ͸¦ ±¸ÇÒ ¶§, ÇÊÅ͸¦ Àû¿ëÇÏ¿´½À´Ï´Ù. ÇÊÅÍ´Â ¹Ýµå½Ã Á¤·ÄÀ» ¸ÕÀú ¼öÇàÇؾßÇÕ´Ï´Ù. UserConnection ÇÏÀ§ Å° °ª¿¡¼ connection À» ±âÁØÀ¸·Î Á¤·ÄÀ» ¼öÇàÇÏ°í, equalTo ¸Þ¼Òµå·Î °ªÀÌ true ÀÎ °ªÀ» ÇÊÅ͸µ ÇÕ´Ï´Ù.
±× ´ÙÀ½ Äڵ尡 ¾Æ·¡ÀÇ ÄÚµå ÀÔ´Ï´Ù. Promise.all() ÇÔ¼ö°¡ ¼öÇàÇÏ°Ô µÇ¾î ÀÖ½À´Ï´Ù. Promise.all ÇÔ¼ö´Â ¾Õ¼ ¼öÇàÇÑ µÎ°³ÀÇ Promise °¡ ¸ðµÎ ¿Ï·á°¡µÇ¸é. thenÇÔ¼öÀÇ Ã¹¹ø° ÄݹéÇÔ¼ö·Î ¹ÝȯµË´Ï´Ù. Äڵ忡´Â »ý·«µÇ¾î ÀÖÁö¸¸ ¸¸¾à ½ÇÆÐÇÏ°Ô µÇ¸é then ÇÔ¼ö µÎ¹ø° ÆĶó¹ÌÅÍÀÎ ÄݹéÇÔ¼ö°¡ ¼öÇàµË´Ï´Ù.
return Promise.all([promiseRoomUserList, promiseUsersConnection]).then(results => {
... (»ý·«)
});
µÎ°³ÀÇ Promise°¡ ¸ðµÎ Á¤»óÀûÀ¸·Î µ¿ÀÛÀÌ µÇ¸é äÆù濡 ÀÖ´Â À¯Àúµé Áß¿¡ Á¢¼ÓÇÏÁö ¾ÊÀº À¯ÀúµéÀ» °ñ¶ó³»´Â ÄÚµåÀÔ´Ï´Ù.
const roomUsersSnapShot = results[0];
const usersConnectionSnapShot = results[1];
const arrRoomUserList =[];
const arrConnectionUserList = [];
if(roomUsersSnapShot.hasChildren()){
roomUsersSnapShot.forEach(snapshot => {
arrRoomUserList.push(snapshot.key);
})
}else{
return console.log('RoomUserlist Data°¡ ¾ø½À´Ï´Ù.')
}
if(usersConnectionSnapShot.hasChildren()){
usersConnectionSnapShot.forEach(snapshot => {
const value = snapshot.val();
if(value){
arrConnectionUserList.push(snapshot.key);
}
})
}else{
return console.log('UserConnections Data°¡ ¾ø½À´Ï´Ù.');
}
const arrTargetUserList = arrRoomUserList.filter(item => {
return arrConnectionUserList.indexOf(item) === -1;
});
const roomUsersSnapShot = results[0];
const usersConnectionSnapShot = results[1];
const arrRoomUserList =[];
const arrConnectionUserList = [];
if(roomUsersSnapShot.hasChildren()){
roomUsersSnapShot.forEach(snapshot => {
arrRoomUserList.push(snapshot.key);
})
}else{
return console.log('RoomUserlist is null')
}
if(usersConnectionSnapShot.hasChildren()){
usersConnectionSnapShot.forEach(snapshot => {
const value = snapshot.val();
if(value){
arrConnectionUserList.push(snapshot.key);
}
})
}else{
return console.log('UserConnections Data°¡ ¾ø½À´Ï´Ù');
}
const arrTargetUserList = arrRoomUserList.filter(item => {
return arrConnectionUserList.indexOf(item) === -1;
});
ÀÌ·¸°Ô Ãß·ÁÁø À¯ÀúµéÀÇ FCM TokenÀ» ±¸ÇØ MessagingÀ» ¹ß¼ÛÇÏ´Â ÄÚµå ÀÔ´Ï´Ù. click_action¿¡´Â °¢ ÀÚÀÇ ¾ÛÁÖ¼Ò¸¦ ÀÔ·ÂÇÕ´Ï´Ù.
for(let i=0; i < arrTargetUserListLength; i++){
admin.database().ref(`FcmId/${arrTargetUserList[i]}`).once('value',fcmSnapshot => {
const token = fcmSnapshot.val();
if(token){
//¸Þ¼¼Áö¿¡ Æ÷Ç﵃ µ¥ÀÌÅÍ
const payload = {
notification: {
title: sendUserName,
body: sendMsg,
icon: sendProfile
}
};
//¸Þ¼¼Áö¹ß¼Û
admin.messaging().sendToDevice(token, payload).then(response => {
response.results.forEach((result, index) => {
const error = result.error;
if (error) {
console.error('FCM ½ÇÆÐ :', error.code);
}else{
console.log('FCM ¼º°ø');
}
});
});
}
});
}
´ë»ó À¯ÀúµéÀÇ id °ªµéÀ» ¹Ýº¹¹®À» ½ÇÇàÇÏ¿©, FCM TokenÀ» °Ë»ö ÇÑ µÚ, ÇÔ²² Àü´ÞÇÒ µ¥ÀÌÅ͸¦ payload¿¡ ´ã°í, sendToDevice ¸Þ¼Òµå·Î Ǫ½Ã¸¦ º¸³»°Ô µË´Ï´Ù.
¸Þ¼¼Â¡À» º¸³»´Â Functions ÄÚµå´Â ¿Ï¼ºµÇ¾ú½À´Ï´Ù.
ÄÚµå Áß°£ Áß°£¿¡ console.log°¡ ½ÇÇàµÇ¾î ÀÖ½À´Ï´Ù. ÀÌ´Â Firebase functionsÀÇ µð¹ö±ëÀ» À§ÇØ Á¦°øµÇ¾îÁö´Â firebase-tools¿¡ Æ÷ÇÔµÈ FunctionsÀÇ shellÇÁ·Î±×·¡¹Ö¿¡¼ È®ÀÎÀ» Çϰųª, Firebase console ȸ鿡¼ È®ÀÎÇÒ ¼ö ÀÖ½À´Ï´Ù.
ÀÏ´Ü FunctionsÀÇ shell ÇÁ·Î±×·¡¹Ö¿¡ ´ëÇÏ¿© Àá½Ã ¾Ë¾Æº¸°Ú½À´Ï´Ù. ¾Æ·¡´Â shell ±¸µ¿ ¸í·É¾î ÀÔ´Ï´Ù. À©µµ¿ìÁîÀÇ ¸í·ÉÇÁ·ÒÇÁÆ®³ª ¸®´ª½º ¶Ç´Â ¸ÆÀÇ Å͹̳ο¡¼ ¾Æ·¡ÀÇ ¸í·É¾î¸¦ ÀÔ·ÂÇغ¸¼¼¿ä.
firebase experimental:functions:shell
À§ ±×¸²Ã³·³ Å͹̳ΠȸéÀÇ firebase ¾Õ¿¡ ÀÔ·ÂÄ¿¼°¡ »ý±é´Ï´Ù.
¿¹Á¦¿¡¼ ÀÛ¼ºÇÑ sendNotificationÀ» Å×½ºÆ® ÇÏ·Á¸é ¾Æ·¡¿Í °°ÀÌ ÀÔ·ÂÇÕ´Ï´Ù.
sendNotification('data', {params : {roomId: ¹æID, messageId: ¸Þ¼¼ÁöID}})
ÀÔ·ÂÇϸé Functions°¡ ½ÇÇàµÇ¸ç ·Î±×°¡ ÂïÈ÷´Â °ÍÀ» º¼ ¼ö ÀÖ½À´Ï´Ù. ÇÏÁö¸¸ ¾Æ½±°Ôµµ ±â´ÉÀÌ ¿Ïº®ÇÏÁö ¾Ê¾Æ¼, Functions¿¡¼ µ¥ÀÌÅͺ£À̽º °æ·Î¸¦ ÁöÁ¤ÇÏ´Â ÆĶó¹ÌÅÍ´Â ÀÔ·ÂÇÒ¼ö ÀÖÀ¸³ª.. µ¥ÀÌÅ͸¦ Å×½ºÆ®·Î ÀÔ·ÂÇÏ´Â Æĸ®¹ÌÅÍ´Â ¾ø¾î¼ payload¿¡¼ title ºÎºÐ¿¡¼ ¿¡·¯°¡ ¹ß»ýÇÒ °ÍÀÔ´Ï´Ù. Å×½ºÆ®¸¦ À§Çؼ´Â payload ºÎºÐ¿¡¼ ÇÒ´çµÇ´Â µ¥ÀÌÅÍ°¡ undefined µÉ ½Ã¿¡´Â Å×½ºÆ® µ¥ÀÌÅÍ°¡ ÀԷµǵµ·Ï ¼öÁ¤ÀÌ ÇÊ¿äÇÕ´Ï´Ù.
±×¸®°í console.log´Â Firebase console ȸ鿡¼ ¾Æ·¡ÀÇ À§Ä¡¿¡¼ È®ÀÎÀÌ µË´Ï´Ù.
¿©±â±îÁö FCM ¹ß½Å ºÎºÐ ÀÛ¼ºÀÌ ¿Ï¼ºµÇ¾ú½À´Ï´Ù. ´ÙÀ½Àº FCM ¼ö½ÅÀ» À§ÇØ Å¬¶óÀ̾ðÆ®¿¡¼ µ¿ÀÛÇÏ´Â ¼ºñ½º ¿öÄ¿¸¦ ÀÛ¼ºÇÏ°Ú½À´Ï´Ù.
éÅÍ ¿Ï¼º ¼Ò½º :
13. Cloud Messaging과 Functions을 이용한 푸시메세지 기능 - F
Ãâó: http://cionman.tistory.com/65 [Suwoni-Codelab]