백엔드 연결 ⇒ 데이터 읽기/쓰기

Back-End 연결 튜토리얼

백엔드 환경에서 코드 실행

nuxtServerInit 액션 메서드 내부에서 process 객체를 통해 서버(server) 또는 클라이언트(client) 환경을 감지하여 코드 실행 범위를 조정할 수 있습니다.

nuxtServerInit 액션 메서드는 애플리케이션이 초기화 될 때 서버에서 실행되는 코드로 Node.js의 process 객체를 사용할 수 있습니다.

client/store/index.js
actions: {
nuxtServerInit(vuexContext, context) {
// 서버 환경에서 코드 실행 (클라이언트 환경에서는 실행되지 않음)
if ( process.server ) {
// 요청(request) 객체의 HTTP 버전 정보 출력
console.log(context.req.httpVersion)
}
// ...
}
}

실행 결과를 살펴보면 서버를 구동한 명령 창에는 HTTP 버전 정보 1.1이 출력되지만, 클라이언트(브라우저) 환경에는 아무런 정보를 출력하지 않습니다. 다시 말해 서버 환경에서만 코드가 실행된 것입니다.

서버 환경
클라이언트 환경

백엔드 환경으로 Firebase 연결

Firebase를 백엔드 환경으로 하여 제작 중인 Nuxt.js 애플리케이션에 연결할 것입니다.

Firebase 콘솔(Console)에서 새로운 프로젝트를 추가합니다. 이름, ID, 애널리틱스, Cloude Firestore 위치 정보를 입력하고 프로젝트 만들기 버튼을 눌러 만듭니다.

생성된 프로젝트의 여러 기능 중 Database를 선택합니다. Firebase는 글을 작성 중인 현재 2개의 데이터베이스를 제공합니다. 하나는 Realtime 데이터베이스 이고, 다른 하나는 Cloud Firestore 입니다. 여기서는 안정적인 Realtime 데이터베이스를 사용해보겠습니다.

어떤 데이터베이스가 서비스에 적합할지 고민된다면 데이터베이스 비교 글을 참고하세요.

데이터베이스 만들기 버튼을 누르면 실시간 데이터베이스 보안 규칙 설정 모달 창이 화면에 표시됩니다. 실제 구동되는 서비스를 개발할 때는 잠금 모드로 시작 해야겠지만, 여기서는 테스트 모드로 시작을 선택하겠습니다. 사용 설정 버튼을 누릅니다.

Realtime 데이터베이스가 생성되었습니다. 테스트 모드로 시작했기 때문에 보안 규칙이 공개 상태임을 경고하는 메시지가 화면에 출력됩니다. 튜토리얼 상에서만 공개 모드를 사용하고, 실제 개발할 때는 보안 규칙을 설정해야 합니다.

앞서 이야기 한 것처럼 보안 규칙이 모두 공개 상태라 누구나 데이터베이스에 접근, 조작할 수 있습니다. 그래서 별도의 인증(Authentication) 없이 데이터베이스 주소(https://nuxt-blog-y9.firebaseio.com/)와 이름만 알면 접근이 가능합니다.

axios 패키지 설치

백엔드와 비동기 통신을 하기 위해 Ajax 라이브러리 axios 패키지를 프로젝트에 설치합니다.

CLI
$ yarn add axios

패키지가 설치 되면 package.json 파일이 업데이트 됩니다.

포스트 폼 저장 메서드

PostForm 컴포넌트 파일(client/components/Admin/PostForm.vue)을 열어 onSave 메서드에 부모 컴포넌트로 커스텀 이벤트를 방출(Emit) 하는 코드를 작성하고 페이로드(Payload, 저장할 데이터)를 추가합니다.

client/components/Admin/PostForm.vue
onSave() {
this.$emit('submit', this.editedPost)
}

백엔드에 포스트 생성(저장)

포스트를 생성하는 페이지 파일(client/pages/admin/create/index.vue)을 열어 백엔드 환경과 비동기 통신을 수행할 준비를 합니다. 먼저 템플릿의 PostForm 컴포넌트에 커스텀 이벤트 submit을 수신하는 코드를 작성하고, 이벤트에 바인딩 될 메서드를 설정합니다.

이어서 이벤트에 연결된 메서드를 정의하고 axios 패키지를 불러온 후, post 메서드를 사용해 Firebase 데이터베이스 URL을 입력하고, 저장할 JSON 파일 이름을 작성합니다. then 메서드 내부에는 응답(response) 받은 데이터를 출력하는 코드를, catch 메서드 내부에는 오류를 감지하여 출력하는 코드를 추가합니다.

client/pages/admin/create/index.vue
<template lang="pug">
// PostForm 컴포넌트로부터 커스텀 이벤트 submit 수신
// 이벤트가 수신되면 onSubmitted 메서드 실행
post-form(@submit="onSubmitted")
</template>
<script>
// axios 패키지 로드
import axios from 'axios'
export default {
methods: {
// 커스텀 이벤트 수신 메서드 정의
onSubmitted(newPost) {
// 백엔드와 비동기 통신을 수행할 axios
axios
// 백엔드인 Firebase 데이터베이스 URL + 저장할 JSON 파일 이름, 저장할 데이터
.post('https://nuxt-blog-y9.firebaseio.com/posts.json', newPost)
// 통신 결과가 성공일 경우, 응답 결과 출력
.then(res => console.log(res))
// 통신 결과가 실패일 경우, 오류 출력
.catch(e => console.error(e))
}
}
}
</script>

포스트 생성 / 저장

포스트를 생성하는 페이지(localhost:3000/admin/create)에 접속한 다음 포스트 내용을 입력하고 저장 버튼을 눌러 백엔드 환경과 통신을 시도해봅니다.

통신 기록은 브라우저 개발도구 Network 탭에서 확인할 수 있습니다. Firebase URL로 요청 보낸 시도가 성공(200 OK)이고, 요청 페이로드(Request Payload)에 전송한 포스트 정보를 확인할 수 있습니다.

Firebase 데이터베이스로 가서 확인해보면 posts가 생성 되었고, 데이터 식별 코드(-LQLpbNAcXPjdlSxx8k2) 아래 전송한 포스트 정보가 저장된 것을 확인할 수 있습니다.

nuxtServerInit 메서드 수정

nuxtServerInit 액션 메서드에 작성된 기존 코드를 지운 후, axios GET 통신을 통해 Firebase 데이터베이스에서 데이터를 요청하는 코드를 작성합니다.

client/store/index.js
import axios from 'axios'
const createStore = () => {
return new Vuex.Store({
// ...
actions: {
nuxtServerInit({commit}, context) {
// axios를 통해 Firebase 데이터베이스에 데이터 요청
axios.get('https://nuxt-blog-y9.firebaseio.com/posts.json')
// 응답 받은 데이터 출력
.then(res => console.log(res))
.catch(e => console.error(e))
}
}
})
}
export default createStore

응답 받은 데이터가 서버를 구동한 명령 창에 출력됩니다. 응답 받은 데이터는 객체이며, 데이터 식별자 키 값에 연결된 객체가 포스트 정보를 가지고 있습니다.

아래와 같이 코드를 수정해 응답 결과에서 data만 추출해 객체를 순환하는 for - in 문을 작성하고 키: 값을 출력해봅니다.

client/store/index.js
axios.get('https://nuxt-blog-y9.firebaseio.com/posts.json')
.then(({data}) => {
const postsList = []
for (let key in data) {
console.log('KEY:', key)
console.log('VAKUE:', data[key])
}
}),

명령 창에 출력된 키, 값 정보를 확인할 수 있습니다.

postsList 빈 배열에 data[key] 값을 그대로 푸시 하게 되면 참조가 되므로, 복사한 값을 푸시 하도록 코드를 설정합니다. 그리고 푸시 된 결과를 출력하는 코드를 작성합니다.

ES6의 전개(spread) 연산자를 활용하면 객체를 손쉽게 복사할 수 있습니다.

client/store/index.js
axios.get('https://nuxt-blog-y9.firebaseio.com/posts.json')
.then(({data}) => {
const postsList = []
for (let key in data) {
// data[key] 데이터의 속성을 복사해서 postsList에 푸시
postsList.push({ ...data[key] })
}
console.log(postsList)
}),

postsList 배열에 필요로 하는 포스트 정보만 포함된 것을 명령 창을 통해 확인할 수 있습니다.

이어서 Firebase 데이터베이스의 데이터 식별자를 id 값으로 저장할 수 있도록 푸시 구문을 수정합니다.

client/store/index.js
axios.get('https://nuxt-blog-y9.firebaseio.com/posts.json')
.then(({data}) => {
const postsList = []
for (let key in data) {
// Firebase 데이터베이스의 데이터 식별자를 id 값으로 저장
postsList.push({ ...data[key], id: key })
}
console.log(postsList)
}),

출력된 결과를 보면 id 값에 Firebase 데이터 식별자 값이 저장된 것을 확인할 수 있습니다.

nuxtServerInit 액션 메서드의 vuexContext 첫번째 매개 변수를 { commit }으로 바꿔 commit 만 추출합니다. 스토어에 커밋하여 뮤테이션의 setPosts 메서드를 실행하는 코드를 작성하고, postsList를 페이로드로 전달합니다.

client/store/index.js
nuxtServerInit({ commit }, context) {
axios
.get('https://nuxt-blog-y9.firebaseio.com/posts.json')
.then(({ data }) => {
const postsList = []
for (let key in data) {
postsList.push({ ...data[key], id: key })
}
commit('setPosts', postsList)
})
.catch(e => console.error(e))
},

그런데 문제가 생겼습니다. 작성한 코드에 오류는 없지만, 스토어의 loadedPosts 데이터가 업데이트 되지 않아 애플리케이션 화면에 포스트 리스트가 출력되지 않습니다. 문제 원인을 파악해보니 nuxtServerInit 액션 메서드가 서버에서 실행되는 코드이고, axios를 통해 요청한 데이터가 Vuex 스토어에 제대로 패치 하는데 시간 차 문제가 있는 것 같습니다.

이 문제를 해결하기 위해 async / await를 사용해 nuxtServerInit 액션 메서드를 수정합니다.

client/store/index.js
async nuxtServerInit({ commit }, context) {
try {
const { data } = await axios.get('https://nuxt-blog-y9.firebaseio.com/posts.json')
const postsList = []
for (let key in data) {
postsList.push({ ...data[key], id: key })
}
commit('setPosts', postsList)
} catch (e) {
console.error(e)
}
},

파악한 문제 원인을 해결하기 위한 방법이 맞았나 봅니다. async/await 를 사용하니 정상적으로 백엔드에서 가져온 데이터가 스토어에 패치 되었습니다.

싱글 포스트 수정

포스트 리스트를 애플리케이션에 정상적으로 출력 했으나, 라우트 링크 값이 Firebase의 데이터 식별자로 출력되고 있습니다. 이전에 더미 데이터에서 id 값을 일반적인 정수로 설정 했으나, Firebase는 고유 식별자를 사용하기 때문입니다.

포스트 라우터 링크 설정은 PostPreview 컴포넌트 파일(client/components/Posts/PostPreview.vue)에서 설정됩니다. 파일을 열어 계산된 속성 loadedPostthis.id 값을 this.title로 변경합니다. 포스트 데이터의 고유 식별자인 id 보다 title 값이 읽기 용이하기 때문입니다.

client/components/Posts/PostPreview.vue
computed: {
postLink() {
return this.isAdmin ? '/admin/' + this.title : '/posts/' + this.title
},
}

변경 사항이 반영되면 읽기 어려웠던 포스트의 고유 식별자인 id에서 읽기 쉬운 포스트의 title로 라우터 링크가 변경됩니다. 하지만 링크를 클릭해도 포스트 데이터가 정상 출력 되지는 않습니다. 이제 해야할 일은 싱글 포스트에서 정상적으로 데이터를 처리하도록 코드를 수정해야 합니다.

싱글 포스트 파일(client/pages/posts/_id/index.vue)을 열어 loadedPost 계산된 속성 코드를 수정합니다.

client/pages/posts/_id/index.vue
<script>
export default {
computed: {
loadedPost() {
// 라우트 매개변수 id 값 (예: 'Nuxt.js 프레임워크')
const id = this.$route.params.id
// 스토어의 loadedPosts 배열에서
// 포스트 타이틀과 라우트 매개변수 id 값이 일치하는
// 아이템을 찾아(find) 반환
return this.$store.getters.loadedPosts.find(post => post.title === id)
}
}
}
</script>

수정이 반영되면 싱글 포스트 페이지에 데이터가 출력되는 것을 볼 수 있습니다. 그런데 업데이트 일자(updatedDate) 정보가 출력되지 않았습니다. 전달 받은 데이터에 해당 속성이 없기 때문입니다.

포스트 생성 메서드 수정

포스트 생성 페이지 컴포넌트 파일(client/pages/admin/create/index.vue)을 열어 onSubmitted 메서드를 수정합니다. newPost 객체의 속성을 복사하고, updateData 속성을 추가한 객체를 백엔드에 보내도록 작성합니다.

client/pages/admin/create/index.vue
onSubmitted(newPost) {
axios
.post('https://nuxt-blog-y9.firebaseio.com/posts.json', {
...newPost,
updatedDate: new Date().toLocaleString()
})
.then(res => console.log(res))
.catch(e => console.error(e))
}

Firebase 데이터베이스에 저장된 자료를 삭제하고, 다시 포스트 생성 폼에 내용을 입력해 데이터를 다지 저장해봅니다.

새롭게 입력한 데이터가 Firebase 데이터베이스에 저장됩니다.

다시 싱글 포스트 페이지에 접속해보면 업데이트 일자가 정상 출력됩니다.

관리자 모드: 포스트 편집 설정

관리자 메인 페이지에 출력된 포스트 리스트의 아이템 링크를 클릭하면 포스트를 편집하는 폼이 화면에 표시됩니다.

현재는 백엔드에서 가져온 데이터가 아닌, 더미 데이터가 연결되어 있습니다.

관리자 포스트 편집 페이지 파일(client/pages/admin/_postId/index.vue)을 열어 계산된 속성 loadedPost를 추가합니다. 스토어의 데이터 중 일부를 라우터 정보를 통해 식별하여 계산된 값을 사용하도록 설정합니다.

client/pages/admin/_postId/index.vue
<script>
import PostForm from '@/components/Admin/PostForm'
export default {
layout: 'admin',
name: 'SinglePost',
components: { PostForm },
computed: {
loadedPost() {
const id = this.$route.params.postId
return this.$store.getters.loadedPosts.find(post => post.title === id)
}
}
}
</script>

관리자 모드의 포스트 편집 페이지에 접속하면 데이터가 정상적으로 출력됩니다.

데이터를 수정 폼에 뿌리는 것에 성공했으니, 이어서 수정한 내용을 저장하면 Firebase 데이터베이스에 저장되도록 코드를 수정합니다. PostForm 컴포넌트의 커스텀 이벤트 submit에 연결되는 메서드 onSubmitted를 정의한 후 axios를 사용해 PUT 통신을 시도합니다. 그리고 통신이 성공하면 관리자 메인 페이지로 이동하도록 설정합니다.

client/pages/admin/_postId/index.vue
<template lang="pug">
.page-admin-post.container
section.update-form
h2.page-title 포스트 수정
p 작성된 글을 수정합니다.
post-form(:post="loadedPost", @submit="onSubmitted")
</template>
<script>
import axios from 'axios'
import PostForm from '@/components/Admin/PostForm'
export default {
layout: 'admin',
name: 'SinglePost',
components: { PostForm },
computed: {
loadedPost() {
const id = this.$route.params.postId
return this.$store.getters.loadedPosts.find(post => post.title === id)
}
},
methods: {
onSubmitted(editedPost) {
axios
.put(
`https://nuxt-blog-y9.firebaseio.com/posts/${editedPost.id}.json`,
editedPost
)
// 통신이 성공하면 관리자 메인 페이지로 이동
.then(res => this.$router.push('/admin'))
.catch(e => console.error(e))
}
}
}
</script>

수정한 후, 포스트 수정 페이지에 내용을 변경해 입력한 후, 저장 버튼을 누르면 Firebase 데이터베이스에 PUT 통신을 수행하고 관리자 메인 페이지로 이동합니다.

Firebase 데이터베이스는 수정한 내용이 반영되어 저장된 것을 확인할 수 있습니다.

하지만 수정한 내용이 즉시 애플리케이션에 반영 되지는 않습니다. 이유는 백엔드와의 통신은 성공했으나, 스토어의 데이터를 업데이트 하지 않았기 때문에 실시간으로 결과가 반영되지 않은 것입니다.

스토어 뮤테이션/액션 메서드

Firebase 데이터베이스와의 통신도 스토어의 상태 업데이트도 모두 스토어에서 관리하도록 수정합니다. 스토어에 포스트를 생성하는 createPost 액션, 뮤테이션 메서드를 추가합니다.

client/store/index.js
const createStore = () => {
return new Vuex.Store({
// ...
mutations: {
// ...
createPost(state, createdPost) {
// 스토어의 loadedPosts 데이터에 createdPost 추가
state.loadedPosts.push(createdPost)
}
},
actions: {
// ...
createPost({ commit }, createdPost) {
createdPost.createdDate = new Date().toLocaleString()
createdPost.updatedDate = createdPost.createdDate
// Firebase 데이터베이스와 통신
return axios
.post('https://nuxt-blog-y9.firebaseio.com/posts.json', createdPost)
.then(res => {
// 통신이 성공하면 뮤테이션에 커밋
commit('createPost', { ...createdPost, id: res.data.name })
})
.catch(e => console.error(e))
}
}
})
}

포스트 생성 페이지(client/pages/admin/create/index.vue) onSubmitted 메서드 코드는 스토어의 createPost 액션 메서드를 실행하도록 변경하고, 통신이 성공적으로 수행되면 관리자 메인 페이지로 이동하도록 설정합니다.

client/pages/admin/create/index.vue
<script>
// ...
export default {
// ...
methods: {
onSubmitted(newPost) {
// 스토어에 createPost 디스패치
this.$store
.dispatch('createPost', newPost)
.then(() => this.$router.push('/admin'))
}
}
}
</script>

이어서 스토어에 포스트를 수정하는 updatePost 액션, 뮤테이션 메서드를 추가합니다.

client/store/index.js
const createStore = () => {
return new Vuex.Store({
// ...
mutations: {
// ...
updatePost(state, updatedPost) {
const idx = state.loadedPosts.findIndex(
post => post.id === updatedPost.id
)
state.loadedPosts[idx] = updatedPost
}
},
actions: {
// ...
updatePost({ commit }, updatedPost) {
updatedPost.updatedDate = new Date().toLocaleString()
return axios
.put(
`https://nuxt-blog-y9.firebaseio.com/posts/${updatedPost.id}.json`,
updatedPost
)
.then(res => {
commit('updatePost', updatedPost)
})
.catch(e => console.error(e))
}
}
})
}

포스트 수정 페이지(client/pages/admin/_postId/index.vue) onSubmitted 메서드 코드는 스토어의 updatePost 액션 메서드를 실행하도록 변경하고, 통신이 성공적으로 수행되면 관리자 메인 페이지로 이동하도록 설정합니다.

client/pages/admin/create/index.vue
<script>
// ...
export default {
// ...
methods: {
onSubmitted(editedPost) {
// 스토어에 updatePost 디스패치
this.$store
.dispatch('updatePost', editedPost)
.then(res => this.$router.push('/admin'))
}
}
}
</script>

포스트 생성, 수정에 문제는 없는지 테스트를 진행합니다. 먼저 새로운 포스트 내용을 입력한 후 저장 버튼을 누릅니다.

Vue Devtools 도구의 Vuex 패널에 createPost 메서드가 실행 됨이 기록됩니다. 그리고 실시간으로 작성한 포스트가 추가된 화면이 업데이트 됩니다. Firebase 데이터베이스에 저장한 후에 데이터를 스토어 데이터에 업데이트 해줬기 때문입니다.

Firebase 데이터베이스에 저장된 내역도 확인할 수있습니다.

이어서 포스트를 수정해봅니다. 이전의 콘텐츠를 수정한 후, 저장 버튼을 누릅니다.

포스트 제목에?기호가 들어가 관리자 모드의 싱글 포스트 페이지에 내용이 출력되지 않습니다. 문제 해결을 위해 싱글 포스트 페이지 파일(client/pages/admin/_postId/index.vue) 코드를 다음과 같이 수정해야 합니다.?기호를 포스트 타이틀 정보에서 제거한 후 비교하도록 변경합니다.

client/pages/admin/_postId/index.vue
<script>
export default {
// ...
computed: {
loadedPost() {
const id = this.$route.params.postId
return this.$store.getters.loadedPosts.find(
post => post.title.replace(/\?/, '') === id
)
}
},
// ...
}
</script>

※ 일반 모드 싱글 포스트 페이지(client/pages/posts/_id/index.vue) 또한 수정해야 합니다.

Vue Devtools 도구의 Vuex 패널에 updatePost 메서드가 실행 됨이 기록됩니다.

수정된 결과가 반영되면 관리자 메인 페이지로 이동합니다.

완성 코드 참고

완료된 코드는 아래 링크를 통해 ZIP 파일을 다운로드 받아 확인할 수 있습니다.