본문 바로가기
Firebase

Firebase를 이용한 웹앱 메모 어플리케이션

by kmmguumnn 2018. 3. 16.

인프런 <파이어베이스(Firebase)를 이용한 웹+안드로이드 메모 어플리케이션 만들기> 강의(강사 신휴창 님)를 수강하는 과정에서 작성한 게시물입니다.



Firebase 개발 환경 설정

firebase.google.com 

→ 구글 로그인 

→ 프로젝트 생성

→ 좌측 Authentication 탭에서, 구글 로그인 방식을 활성화


로컬 개발 환경 설정

node 설치 

→ 터미널에서 node -v로 설치 및 버전 확인 

→ npm install firebase-tools -g (Permission denied 에러 발생 시, 앞에 sudo 붙여서 다시 시도)

→ firebase login 입력해서 로그인

→ firebase list로 생성된 프로젝트 리스트 확인


→ 작업 중인 디렉토리로 이동 후, firebase init 입력

→ database 선택

→ 작업하고자 하는 프로젝트 선택

→ database.rule.json 입력


→ firebase init 입력

→ hosting 선택

→ public 입력


→ firebase serve 입력

→ localhost:5000으로 이동하면 index 화면이 뜬다!


설정 script 추가

console.firebase.google.com으로 이동

→ 작업 중인 프로젝트 클릭 후, 웹 앱에 Firebase 추가

→ script 코드 복사 후, index.html에 추가

<script src="https://www.gstatic.com/firebasejs/4.11.0/firebase.js"></script>
<script>
// Initialize Firebase
var config = {
apiKey: "blahblahblahblah",
authDomain: "memowebapp-blah.firebaseapp.com",
databaseURL: "https://memowebapp-blah.firebaseio.com",
projectId: "memowebapp-blah",
storageBucket: "memowebapp-blah.appspot.com",
messagingSenderId: "012301230123"
};
firebase.initializeApp(config);
</script>


인증 기능 구현

구글 로그인 인증 구현

var auth;
// Initialize Firebase
var config = {
apiKey: "blahblahblahblah",
authDomain: "memowebapp-blah.firebaseapp.com",
databaseURL: "https://memowebapp-blah.firebaseio.com",
projectId: "memowebapp-blah",
storageBucket: "memowebapp-blah.appspot.com",
messagingSenderId: "012301230123"
};
firebase.initializeApp(config);
auth = firebase.auth();
var authProvider = new firebase.auth.GoogleAuthProvider();
auth.signInWithPopup(authProvider);

페이지를 열면, 팝업창과 함께 구글 로그인 절차가 진행된다.


onAuthStateChanged를 통해 현재 로그인한 사용자를 가져올 수 있다. 로그인에 성공하면 콘솔에 성공 메시지를 출력하고, 로그인에 실패했을 경우에만 팝업을 띄우도록 수정한다.

firebase.initializeApp(config);
auth = firebase.auth();
var authProvider = new firebase.auth.GoogleAuthProvider();
auth.onAuthStateChanged(function(user) {
// Authentication Success
if (user) {
console.log("Success");
console.log(user);
}
// Authentication Fail
else {
auth.signInWithPopup(authProvider);
}

※ onAuthStateChanged 관련 "Firebase에서 사용자 관리하기" 문서 참조


데이터 출력

다음으로, 저장한 메모를 출력하는 기능을 만들기 위해 데이터베이스를 활용해보자. Firebase 콘솔의 database로 가서 "memos"라고 추가해준다.


그런데, memos 밑에 모든 데이터를 쭉 나열하면, 모든 사람이 모든 데이터들을 다 볼 수 있게 된다. 따라서 uid 별로 따로 관리를 할 것이다. 즉 이에 따라 데이터베이스 구조화 계획을 JSON 트리로 만들어 본다면, 대략 다음과 같다.

{
memos : {
uid : { txt : '본문', updateDate: '수정한 날짜', createDate : '생성한 날짜' },
uid : { txt : '본문', updateDate: '수정한 날짜', createDate : '생성한 날짜' },
uid : { txt : '본문', updateDate: '수정한 날짜', createDate : '생성한 날짜' }
}
}

memos 밑에, 각 사용자마다 갖고 있는 uid가 있고, uid 별로 txt, updateDate, createDate 등의 요소로 구성된 구조다. 

먼저 uid를 활용하려면 onAuthStateChanged에서 콜백 함수의 매개변수로 사용된 user를 통해 uid를 가져와야 한다. 이를 위해 userInfo라는 전역 변수를 하나 만든 뒤, onAuthStateChanged의 콜백 함수 내 if절에 userInfo = user를 추가한다.


데이터베이스 서비스에 대한 레퍼런스 획득 및 초기화를 진행하고, 메모 리스트를 가져오기 위한 함수(get_memo_list())를 작성한다. 여기까지의 코드는 다음과 같다.

auth.onAuthStateChanged(function(user) {
// Authentication Success
if (user) {
console.log("Success");
console.log(user);
// Print out the memo list
userInfo = user;
get_memo_list();
}
// Authentication Fail
else {
auth.signInWithPopup(authProvider);
}
});
// 데이터베이스 서비스에 대한 레퍼런스를 얻음 & 초기화 (Firebase Realtime Database 사용할 준비 끝)
database = firebase.database();

/* 데이터베이스 구조화 계획 세우기 (JSON 트리)
{
memos : {
uid : {
txt : '본문',
updateDate: '수정한 날짜',
createDate : '생성한 날짜'
},
uid : {
...
}
...
}
}
*/

function get_memo_list() {
var memoRef = database.ref('memos/' + userInfo.uid);
}

get_memo_list 함수의 첫 줄이 의미하는 바는, 'memos'에서, user의 uid에 엑세스 하겠다는 뜻이다. 로그인 인증에 성공하면 이 함수를 호출한다. 일단 함수 내부를 더 작성해보자.


경로를 읽어 변경되는 부분에 대해 수신하기 위해 on() 메소드를 사용한다. on() 메소드 내에서, 'child_added' 함수를 통해 추가된 데이터의 값을 보여준다. 해당 기능은 자주 사용될 것이므로 따로 함수로 만들어 호출한다. (on_child_added(data) 메소드)

function get_memo_list() {
var memoRef = database.ref('memos/' + userInfo.uid);
memoRef.on('child_added', on_child_added)
}

function on_child_added(data) {
console.log(data.val());
}



참조하고 있는 데이터들이 실제 페이지에 출력되도록 해보자. index 페이지 디자인을 보면, 좌측에 목록이 있고, 목록에서 메모를 하나 누르면 중앙에 메모가 출력되어야 한다. 메모 목록을 의미하는 ul element 내에 메모의 정보를 append하는 코드를 추가한다.

function on_child_added(data) {
// console.log(data.val());
var key = data.key;
var memoData = data.val();
var txt = memoData.txt;
var title = memoData.title;
var firstTxt = txt.substr(0, 1);

var html =
"<li id='" + key + "' class=\"collection-item avatar\" onclick=\"fn_get_data_one(this.id);\" >" +
"<i class=\"material-icons circle red\">" + firstTxt + "</i>" +
"<span class=\"title\">" + title + "</span>" +
"<p class='txt'>" + txt + "<br>" +
"</p>" +
"</li>";
$('.collection').append(html);
}



이제 메모하는 내용을 실제 데이터로 저장하는 코드를 추가해야 한다.

// 메모 데이터를 push하는 함수
function save_data(data) {
var memoRef = database.ref('memos/' + userInfo.uid);
var txt = $('.textarea').val();

// Push
memoRef.push({
txt : txt,
createDate : new Date().getTime()
})
}

// 메모장에서 밖으로 커서를 옮기면 메모 데이터 저장
$(function() {
$('.textarea').blur(function() {
save_data();
})
})



이제 메모를 적고 바깥 부분을 클릭하면, 왼쪽 목록에 메모가 추가된다. 그런데, 아무 변화를 주지 않고 메모 입력창 안과 밖을 번갈아 클릭하기만 해도 메모가 계속 추가된다. 이를 위한 유효성 검사를 해야 하는데, 먼저 빈 메모는 추가되지 않도록 하는 코드부터 추가하자.

// 빈 텍스트는 추가하지 않는다.
if (txt = '')
return;

위 코드를 push 블록 직전에 추가한다.



앞서 append한 html 코드에서, onclick 속성에 fn_get_data_one 함수를 포함시켰었다. 즉, 생성된 메모를 리스트에서 클릭하면 textarea에 메모 내용이 뜨도록 해야 한다. 이 함수를 정의해보자.

// 리스트를 클릭하면 메모 정보를 textarea에 출력
function fn_get_data_one(key) {
var memoRef = database.ref('memos/' + userInfo.uid + '/' + key).once('value').then(function(snapshot) {
$('.textarea').val(snapshot.val().txt);
});
}



ref 메소드 내에서, 기존 방식대로 userInfo.uid까지만 쓰는 것과 '/' + key를 추가한 것은 어떤 차이일까?

userInfo.uid까지만 쓰면, 한 유저가 입력한 모든 메모에 해당한다.



뒤에 '/' + key 를 추가하면, 메모의 key를 식별하여 메모 단위로 접근하게 된다.



이제 메모를 수정하는 기능을 만들어야 한다. 기존에 있던 메모로 들어가서 텍스트를 입력 후 빠져나오는 것과, 새로운 메모를 작성하고 빠져나오는 것을 구분해야 한다.

기존의 메모를 클릭했는지 식별하기 위해서, 먼저 selectedKey라는 전역 변수를 선언하고, fn_get_data_one(key) 함수의 첫 줄에 

selectedKey = key;

를 추가한다.


기존에 있던 메모인지 신규 메모인지 구별하기 위해 if-else 블록에 적절한 코드를 삽입한다.

// 이미 존재하는 메모를 수정했을 경우 (key가 있을 경우)
if (selectedKey) {
memoRef = database.ref('memos/' + userInfo.uid + '/' + selectedKey);
memoRef.update( {
txt : txt,
createDate : new Date().getTime(),
updateDate : new Date().getTime()
});
}
// 새로운 메모를 작성했을 경우 (selectedKey가 없을 경우)
else {
// Push
memoRef.push({
txt : txt,
createDate : new Date().getTime()
});
}

위 코드를 save_data(data) 함수의 마지막 부분에 추가한다.



또한, 리스트에서 메모의 제목은 메모 내용의 첫 줄이 되도록 하기 위해, on_child_added(data) 함수에서 title 변수를 다음과 같이 수정한다.

var title = txt.substr(0, txt.indexOf('\n')); // 메모 내용의 첫 줄을 제목으로 보여줌



이제 기존에 있던 메모를 수정하고 외부를 클릭한 뒤 새로고침 해 보면, 메모가 수정되어 있음을 확인할 수 있다!



다음으로는 우측 하단에 있는 + 플로팅 버튼의 기능을 추가하자. 버튼을 누르면 새로운 메모를 추가하게 된다.

해당 element를 찾아 onclick 속성에 initMemo();를 다음과 같이 달아준다.

<div class="fixed-action-btn" style="bottom: 45px; right: 24px;">
<a class="btn-floating btn-large waves-effect waves-light red" onclick="initMemo();"><i class="material-icons">add</i></a>
</div>



이제 initMemo() 함수를 정의해 준다.

function initMemo() {
$('.textarea').val('');
selectedKey = null; // 새로운 메모이므로, selectedKey에 들어있는 값을 비워 준다.
}



그런데 메모 내용을 수정하고 포커스를 빼면, 내용이 수정이 되기는 하지만 리스트에서는 바로 최신화가 이뤄지지 않는다(새로고침해야만 바뀜).

get_memo_list() 함수의 하단에 다음을 추가한다.

memoRef.on('child_changed', function(data) {
console.log(data.key); // 어떤 데이터가 수정되었는지 확인
console.log(data.val()); // 수정된 내용을 확인

var key = data.key;
// 수정
var txt = data.val().txt;
var title = txt.substr(0, txt.indexOf('\n'));
// 데이터 갱신
$("#" + key + " > .title").text(title);
$("#" + key + " > .txt").text(txt);
});

이제 수정하자마자 리스트에 바로 반영이 된다. Firebase의 Realtime database 덕분이다.



마지막으로, 리스트에 각각 버튼을 추가하고, 버튼을 누르면 메모 삭제가 가능하도록 한다. 버튼을 추가하기 위해 html 변수에서 "</li>"; 직전에 다음의 코드를 추가한다.

"<a href=\"#!\" onclick=\"fn_delete_date('" + key + "')\"class=\"secondary-content\"><i class=\"material-icons\">grade</i></a>" +


페이지를 새로고침하고, 추가된 별 버튼을 오른쪽으로 눌러 요소 검사를 해보면 fn_delete_date 함수의 매개변수로 key값(예를 들면, '-L7iJ4f6v3JaQSrvyxkW')이 들어간 것을 확인할 수 있다.



이제 fn_delete_date 함수를 정의하면 끝난다.

function fn_delete_date(key) {
// 팝업에서 승인을 누르면 삭제, 취소 누르면 그냥 return
if (!confirm('삭제하시겠습니까?')) {
return;
}

var memoRef = database.ref('memos/' + userInfo.uid + '/' + key);
memoRef.remove(); // firebase의 삭제
$("#" + key).remove(); // element 삭제 (jQuery의 삭제)
initMemo(); // textarea 비워줌
}










<Full Source Code>

<!DOCTYPE html>
<html>
<head>
<!--Import Google Icon Font-->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<!--Import materialize.css-->
<!-- Compiled and minified CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.8/css/materialize.min.css">


<!--Let browser know website is optimized for mobile-->
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<style>
::-webkit-scrollbar {
display:none;
}
.collection { cursor: pointer;}
</style>
</head>

<body>
<div class="row">
<div class="col s3" style="padding:0; margin:0; overflow-y:auto; overflow-x:hidden; height:1080px; -ms-overflow-style: none;">
<!-- Grey navigation panel -->
<ul class="collection" style="padding:0; margin:0;"></ul>
</div>

<div class="col s9" style="padding:0; margin:0; max-height:1080px;">
<!-- Teal page content -->
<nav>
<div class="nav-wrapper">
<div class="col s12">
<a href="#!" class="breadcrumb"><span id="modifyDate"></span></a>

</div>
</div>
</nav>

<textarea style="height:1000px;" class="textarea" width="100%" rows="1000" placeholder="새로운 메모를 입력해보세요^^"></textarea>
</div>

<div class="fixed-action-btn" style="bottom: 45px; right: 24px;">
<a class="btn-floating btn-large waves-effect waves-light red" onclick="initMemo();"><i class="material-icons">add</i></a>
</div>

<div class="preloader-wrapper big active" style="position:absolute; z-index:1000; left:50%; top:50%; display:none;">
<div class="spinner-layer spinner-blue-only">
<div class="circle-clipper left">
<div class="circle"></div>
</div><div class="gap-patch">
<div class="circle"></div>
</div><div class="circle-clipper right">
<div class="circle"></div>
</div>
</div>
</div>
</div>

<!--Import jQuery before materialize.js-->
<script type="text/javascript" src="https://code.jquery.com/jquery-2.1.1.min.js"></script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.8/js/materialize.min.js"></script>

<script src="https://www.gstatic.com/firebasejs/4.11.0/firebase.js"></script>
<script>
var auth, database, userInfo, selectedKey;
// Initialize Firebase
var config = {
apiKey: "AIzaSyAZdvRvK0woVtfRqAUXMdwQBFvItIHlpjM",
authDomain: "memowebapp-446b3.firebaseapp.com",
databaseURL: "https://memowebapp-446b3.firebaseio.com",
projectId: "memowebapp-446b3",
storageBucket: "memowebapp-446b3.appspot.com",
messagingSenderId: "616847337442"
};
firebase.initializeApp(config);
auth = firebase.auth();
var authProvider = new firebase.auth.GoogleAuthProvider();
auth.onAuthStateChanged(function(user) {
// Authentication Success
if (user) {
console.log("Success");
console.log(user);
// Print out the memo list
userInfo = user;
get_memo_list();
}
// Authentication Fail
else {
auth.signInWithPopup(authProvider);
}
});
// 데이터베이스 서비스에 대한 레퍼런스를 얻음 & 초기화 (Firebase Realtime Database 사용할 준비 끝)
database = firebase.database();

/* 데이터베이스 구조화 계획 세우기 (JSON 트리)
{
memos : {
uid : {
txt : '본문',
updateDate: '수정한 날짜',
createDate : '생성한 날짜'
},
uid : {
...
}
...
}
}
*/

function get_memo_list() {
var memoRef = database.ref('memos/' + userInfo.uid);
memoRef.on('child_added', on_child_added);
memoRef.on('child_changed', function(data) {
console.log(data.key); // 어떤 데이터가 수정되었는지 확인
console.log(data.val()); // 수정된 내용을 확인

var key = data.key;
// 수정
var txt = data.val().txt;
var title = txt.substr(0, txt.indexOf('\n'));
// 데이터 갱신
$("#" + key + " > .title").text(title);
$("#" + key + " > .txt").text(txt);
});
}

function on_child_added(data) {
// console.log(data.val());
var key = data.key;
var memoData = data.val();
var txt = memoData.txt;
var title = txt.substr(0, txt.indexOf('\n')); // 메모 내용의 첫 줄을 제목으로 보여줌
var firstTxt = txt.substr(0, 1);

var html =
"<li id='" + key + "' class=\"collection-item avatar\" onclick=\"fn_get_data_one(this.id);\" >" +
"<i class=\"material-icons circle red\">" + firstTxt + "</i>" +
"<span class=\"title\">" + title + "</span>" +
"<p class='txt'>" + txt + "<br>" +
"</p>" +
"<a href=\"#!\" onclick=\"fn_delete_date('" + key + "')\"class=\"secondary-content\"><i class=\"material-icons\">grade</i></a>" +
"</li>";
$('.collection').append(html);
}

// 리스트를 클릭하면 메모 정보를 textarea에 출력
function fn_get_data_one(key) {
selectedKey = key;
var memoRef = database.ref('memos/' + userInfo.uid + '/' + key).once('value').then(function(snapshot) {
$('.textarea').val(snapshot.val().txt);
});
}

function fn_delete_date(key) {
// 팝업에서 승인을 누르면 삭제, 취소 누르면 그냥 return
if (!confirm('삭제하시겠습니까?')) {
return;
}

var memoRef = database.ref('memos/' + userInfo.uid + '/' + key);
memoRef.remove(); // firebase의 삭제
$("#" + key).remove(); // element 삭제 (jQuery의 삭제)
initMemo(); // textarea 비워줌
}

// 메모 데이터를 push하는 함수
function save_data(data) {
var memoRef = database.ref('memos/' + userInfo.uid);
var txt = $('.textarea').val();
// 빈 텍스트는 추가하지 않는다.
if (txt == '')
return;

// 이미 존재하는 메모를 수정했을 경우 (key가 있을 경우)
if (selectedKey) {
memoRef = database.ref('memos/' + userInfo.uid + '/' + selectedKey);
memoRef.update( {
txt : txt,
createDate : new Date().getTime(),
updateDate : new Date().getTime()
});
}
// 새로운 메모를 작성했을 경우 (selectedKey가 없을 경우)
else {
// Push
memoRef.push({
txt : txt,
createDate : new Date().getTime()
});
}
}

function initMemo() {
$('.textarea').val('');
selectedKey = null; // 새로운 메모이므로, selectedKey에 들어있는 값을 비워 준다.
}

// 메모장에서 밖으로 커서를 옮기면 메모 데이터 저장
$(function() {
$('.textarea').blur(function() {
save_data();
});
})
</script>

</body>

</html>





서버를 끄고 firebase deploy를 입력하면 호스팅까지 가능하다.

댓글