Home Project SW Test Forum 6. CRUD 게시판 구현 및 오류 해결(프론트엔드) - 1
Post
Cancel

Project SW Test Forum 6. CRUD 게시판 구현 및 오류 해결(프론트엔드) - 1

프로젝트 레이아웃 변경

  • Bootstrap 공식 홈페이지에 있는 예제 프로젝트를 참고하여 반응형 레이아웃을 개발 하였으며, 이중 Header, Sidebar, Footer 템플릿을 사용하였다.

출처: https://getbootstrap.com/docs/5.0/examples/

Vue.js 환경 변수 설정

  • 프론트엔드 서버와 백엔드 서버 간의 비동기 통신을 위해서 사용하는 백엔드 서버 주소는 개발 환경과 운영 환경에서 다르다.
  • 실행될 때 호출되는 벡엔드 서버 주소를 다르게 호출하기 위해서는 Vue.js 환경 변수를 사용해야 한다.

  • 각 환경에 따른 .env 파일을 생성한다.
1
2
3
4
5
<module-app-web\front\.env.local>

NODE_ENV=local
BASE_URL=/login
VUE_APP_MODULE_APP_API_URL=http://localhost:8082
1
2
3
4
5
<module-app-web\front\.env.prod>

NODE_ENV=prod
BASE_URL=/login
VUE_APP_MODULE_APP_API_URL=http://서버주소:8082
  • 터미널에서 프론트엔드 서버를 환경에 맞게 실행한다.
1
2
# local 환경에서 수행
$ npm run local
  • ‘process.env.환경변수 이름’으로 환경 변수에 접근할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<module-app-web\front\src\components\cwe\cwe\CweList.vue>

// ...

// onBeforeMount, init
onBeforeMount(async () => {
 fireSuccessToast("cwe");

 await searchList({"page": 1});

 await axios.get(process.env.VUE_APP_MODULE_APP_API_URL + "/api/cwe/list-access-authority",
     {},
 )
     .then((response) => {
       access.value = response.data;
     })
     .catch((error) => {
       parseErrorMsg(error.response);
     })
     .then(() => {
     });
})

// ...

출처: https://velog.io/@skyepodium/vue-%EC%8B%A4%ED%96%89-%EB%AA%A8%EB%93%9C%EC%99%80-%ED%99%98%EA%B2%BD-%EB%B3%80%EC%88%98-%EC%84%A4%EC%A0%95

Vue.js Composition API 적용

  • 재사용성과 가독성을 높이기 위해서 Vue.js 3에서 새로 도입된 Composition API를 적용하였다.

출처: https://kyounghwan01.github.io/blog/Vue/vue3/composition-api/#composition-api%E1%84%80%E1%85%A1-%E1%84%82%E1%85%A1%E1%84%8B%E1%85%A9%E1%84%80%E1%85%A6-%E1%84%83%E1%85%AC%E1%86%AB-%E1%84%87%E1%85%A2%E1%84%80%E1%85%A7%E1%86%BC

Vue 전역 Sass 변수 설정

  • 다음 명령어로 sass-loader를 설치할 수 있다.
1
$ npm install sass-loader sass webpack --save-dev
  • 본 프로젝트에서는 sass-loader가 8버전을 사용하므로, prependData proeprty를 사용하여 전역에서 scss 파일을 include 하였다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<module-app-web\front\vue.config.js>

module.exports = {
   css: {
       loaderOptions: {
           sass: {
               prependData:
                   `
                       @import "@/assets/css/variable.scss";
                       @import "@/assets/css/general.scss";
                       @import "@/assets/css/layout.scss";
                   `
           }
       }
   }
}

출처: https://m.blog.naver.com/mgveg/221900939600

axios 설정

  • main.js에서는 다음과 같이 전역으로 axios csrf 전달 옵션, JWT 토큰 전달 옵션, 로딩 바를 설정하였다.
  • 로딩 바는 각 컴포넌트 상단에 위치한다. axios 인터셉터를 사용하여 요청이 시작할 때 로딩 바가 생성되며 종료될 때 로딩 바가 사라진다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<module-app-web\front\src\main.js>

// axios 설정
axios.defaults.xsrfCookieName = 'XSRF-TOKEN' // csrf 기본 설정을 명시적으로 선언
axios.defaults.xsrfHeaderName = 'X-XSRF-TOKEN' // csrf 기본 설정을 명시적으로 선언
axios.defaults.withCredentials = true; // 다른 origin에 JWT를 전달하기 위한 설정
// axios.defaults.headers.common['Access-Control-Allow-Origin'] = '*'; // cors 설정
// axios.defaults.headers.common['Access-Control-Allow-Headers'] = 'Origin, Content-Type, X-Auth-Token'; // cors 설정

// 요청 인터셉터 추가
axios.interceptors.request.use(
   (config) => {
       // 요청을 보내기 전에 수행할 일
       document.getElementById("loading-wrapper").style.visibility = "visible";
       return config;
   },
   (error) => {
       // 오류 요청을 보내기전 수행할 일
       document.getElementById("loading-wrapper").style.visibility = "hidden";
       return Promise.reject(error);
   });

// 응답 인터셉터 추가
axios.interceptors.response.use(
   (response) => {
       document.getElementById("loading-wrapper").style.visibility = "hidden";
       // 응답 데이터를 가공
       return response;
   },
   (error) => {
       // 오류 응답을 처리
       document.getElementById("loading-wrapper").style.visibility = "hidden";
       return Promise.reject(error);
   });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<module-app-web\front\src\components\common\Loading.vue>

<template>
 <div class="container-fluid">
   <div id="loading-wrapper">
     <img :src="require(`@/assets/images/loading.gif`)" id="loading" alt="Loading">
   </div>
 </div>
</template>

<style lang="scss">

#loading-wrapper {
 position: fixed;
 left: 50%;
 top: 50%;
 transform: translate(-50%, -50%);
 z-index: 1000;
 visibility: hidden;
}
</style>

<script>
// vue.js

export default {
 name: "Loading"
}
</script>

출처: https://jess2.xyz/vue/axios/
https://heewon26.tistory.com/103

동적으로 img 태그 생성할 때 img src의 절대 경로를 불러오는 방법

  • 파일 드래그 앤 드랍 기능을 개발할 때, 동적으로 생성되는 첨부 파일의 취소 이미지 파일을 img 태그의 src를 바인딩하여 불러 올 수 없다.
  • public 폴더에 위치된 정적한 assets 들은 webpack으로 복사되지 않으므로, 절대 경로를 통하여 접근할 수 있다.

image

  • 하단 출처를 참고하여, 동적으로 img 태그가 생성되는 경우, public 폴더에 위치시킨 다음 img 파일을 호출하였다.
1
2
3
4
5
6
7
8
<module-app-web\front\src\components\common\FileUpload.vue>

const tag = '<div id="uploadFileId' + tempUploadFileId + '" + class="d-flex">'
   + '<span class="d-flex align-items-center">'
   + file.name + ",&nbsp; 파일 크기: " + convertFileSize(file.size)
   + '</span>'
   + '<img id="cancelFileIcon' + tempUploadFileId + '" src="/x-circle-main-black.svg" class="ms-2">'
   + '</div>';

image

출처: https://cli.vuejs.org/guide/html-and-static-assets.html#the-public-folder

자바스크립트: 일반 함수를 모두 화살표 함수로 변경

  • 자바스크립트 함수 표현식 보다 화살표 함수를 사용하면 조금 더 간결하게 함수를 만들 수 있다. 따라서 화살표 함수로 모두 대체 하였다.
1
2
3
4
5
6
7
8
9
10
11
12
 await axios.get(process.env.VUE_APP_MODULE_APP_API_URL + "/api/misra-cpp/list-access-authority",
     {},
 )
  .then(function (response) {
    // ...
  })
  .catch(function (error) {
    // ...
  })
  .then(function () {
    // ...
  });
1
2
3
4
5
6
7
8
9
10
11
12
13
 await axios.get(process.env.VUE_APP_MODULE_APP_API_URL + "/api/misra-cpp/list-access-authority",
     {},
 )
     .then((response) => {
    // ...
     })
     .catch((error) => {
    // ...
     })
     .then(() => {
    // ...
     });
})

출처: https://ko.javascript.info/arrow-functions-basics
https://ko.javascript.info/arrow-functions-basics

Vue.js TypeError: Cannot read property of undefined 오류

  • 백엔드 서버에서 수신 받은 JSON key와 자바스크립트 객체 property가 매핑되지 않거나 객체 property가 선언되지 않으면, vue에서 브라우저로 렌더링 할 때 발생하는 오류다.
  • 따라서 자바스크립트 객체 property를 JSON key와 매핑되도록 선언해야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<module-app-web\front\src\components\admin_page\notice\NoticeRead.vue>

<tr>
 <td colspan="2">
   <div class="float-end">
     <span class="float-end">
       <strong class="additional-information-title">작성자: </strong><span class="additional-information-content"> , </span>
       <strong class="additional-information-title">작성일: </strong><span class="additional-information-content"></span><br>

       <strong class="additional-information-title">최종 수정자: </strong><span class="additional-information-content"> , </span>
       <strong class="additional-information-title">최종 수정일: </strong><span class="additional-information-content">, </span>
       <strong class="additional-information-title">조회수: </strong> <span class="additional-information-content"></span>
     </span>
   </div>
 </td>
</tr>


// ...

setup() {
  // variable
  let notice = ref(); // 기존
  let notice = ref({createdByUser: {department: '', name: ''}, lastModifiedByUser: {department: '', name: ''}});
 // 수정
}

Vue.js 라우트 메타 필드

  • 라우트 메타 필드를 사용하여 웹 페이지 제목, 네비게이션 가드, 레이아웃을 설정하였다.

웹 페이지 제목

  • DOM이 업데이트 된 후 실행되는 nextTick을 사용하여, 컴포넌트가 변경되는 경우 메타 필드 title 속성 값으로 웹 페이지 제목을 변경한다.

네비게이션 가드

  • 로그인하지 않은 사용자가 로그인이 필요한 URI에 접근하거나 존재하지 않는 URI로 접근할 때 메타 필드 authRequired 속성 값으로 접근을 허용하지 않도록 구현하였다.
  • vueCookies를 사용하여 로그인 한 사용자가 login 페이지로 이동하는 경우 경고창을 띄어서 원래 페이지로 이동하도록 구현하였다.

레이아웃 구성

  • 로그인 페이지는 Header, Sidebar, Footer을 사용하여 템플릿을 구성하지 않는다. 라우트에서 meta 필드의 layoutView 속성 값을 사용하여 Header, Sidebar, Footer 레이아웃 적용 여부를 구분하였다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<module-app-web\front\src\router\index.js>

const routes = [
   {
       path: '/login',
       name: 'Login',
       component: Login,
       meta: {title: 'SW Test Forum - 로그인', layoutView: false, authRequired: false}
   },
   {
       path: '/dashboard',
       name: 'Dashboard',
       component: Dashboard,
       meta: {title: 'SW Test Forum - 대시보드', layoutView: true, authRequired: true}
   },

// ...
]

router.beforeEach(async function (to, from, next) {
   // page title 설정
   //nextTick은 Dom이 업데이트 된 후 실행
   nextTick(() => {
       document.title = to.meta.title;
   });

   // 잘못된 URI로 매핑되는 경우, Error404
   if (to.name === 'Error404') {
       await error.fire({
           text: "페이지를 찾을 수 없습니다.",
       });

       router.go(-1);
   }
   // to: 이동할 url에 해당하는 라우팅 객체
   else if (
       to.matched.some((routeInfo) => {
           return routeInfo.meta.authRequired;
       })) {
       if (vueCookies.get('isHasToken')) {
           next();
       }
       // 권한이 없는 경우, Error401
       else {
           await error.fire({
               text: "로그인이 필요합니다.",
           });

           next('/login');
           //router.go(-1);
       }
   } else {
       if (vueCookies.get('isHasToken')) {
           await warning.fire({
               text: "로그아웃을 하신 다음, 다른 계정으로 로그인 해주세요.",
           });

           next('/dashboard');
       } else {
           next();
       }
   }
});

export default router
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<module-app-web\front\src\App.vue>

<template>
 <body v-if="this.$route.meta.layoutView === true">
 <Header/>
 <main>
   <Sidebar/>
   <router-view/>
 </main>
 <Footer/>
 </body>

 <body v-else>
 <router-view/>

 </body>
</template>

출처: https://joshua1988.github.io/web-development/vuejs/vue-router-navigation-guards/

Vue.js 브러우저 뒤로가기 클릭시 스크롤 위치

  • 브라우저에서 뒤로가기 버튼을 클릭하면 브라우저의 scroll 위치가 최상단으로 변경된다.
  • 브라우저 scroll 위치가 현재 위치로 고정하도록 변경하였다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<module-app-web\front\src\router\index.js>

const router = createRouter({
   history: createWebHistory(process.env.BASE_URL),
   routes,
   scrollBehavior(to, from, savedPosition, popstate) {
       //브러우저 뒤로가기 버튼을 클릭하는 경우, scroll 위치를 변경하지 않음
       if (isEmpty(savedPosition)) { // isEmpty는 사용자 정의 함수로 savedPosition 객체가 비어있는 경우 최상단으로 scroll 이동
           return new Promise((resolve, reject) => {
               resolve({left: 0, top: 0})
           })
       } else {
           return new Promise((resolve, reject) => {
               behavior: 'smooth', resolve({left: savedPosition.left, top: savedPosition.top})
           })
       }
   }
})

유효성 검사

  • 프론트엔드와 백엔드 모두 유효성 검사 로직을 구현 하였다.
  • 백엔드로 데이터를 송신 전, 각 데이터에 해당되는 유효성 검사 로직이 수행된다. 만약 유효성 검사에 실패하면, input 태그에 focus가 맞춰지며, input 태그 하단에 에러 메시지를 출력한다.

image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<module-app-web\front\src\utils\validation-util.js>

// ...

/* 길이 및 공백 validation */
const validateLengthAndIsEmpty = (name, value) => {
   let result;
   let errorMessage = document.getElementById(name + "ErrorMessage");

   if (!validateWhiteSpace(value)) {
       result = "공란이 될 수 없습니다.";
       document.getElementsByName(name)[0].focus();
   } else if (value.length > 255) {
       result = "길이는 255 보다 작아야 합니다. \n"
           + "(현재 입력된 길이: " + value.length + ")";
       document.getElementsByName(name)[0].focus();
   } else {
       result = "";
   }

   errorMessage.innerText = result;

   return isEmpty(result);
}

// ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<module-app-web\front\src\components\cwe\cwe\CweWrite.vue>

<tr>
 <th>제목<span class="required-field">*</span><span class="auto-completed-field">*</span></th>
 <td style="overflow: visible">
   <div class="autoComplete_wrapper">
     <input type="text" name="title" id="title" v-model="title" class="form-control" placeholder="[RTE_Buffer_Overrun] 배열 최대 범위보다 큰 요소 접근을 금지한다.">
     <p id="titleErrorMessage" class="error-message"></p>
   </div>
 </td>
</tr>

if (!(validateLengthAndIsEmpty("title", title.value)
   && validateLength("hashTags", hashTags.value)
   && validateLength("language", language.value)
   && validateLength("cweId", cweId.value)
)) {
 return false;
}

Vue.js 공통 컴포넌트 분리

  • 중복하여 많이 사용하는 레이아웃 및 기능들은 컴포넌트로 생성하여 common 폴더에 위치 시켰다. 해당 컴포넌트들은 다른 컴포넌트에 등록되어 사용된다.

image

  • 기능상 컴포넌트 및 자바스크립트 파일에서 많이 사용하는 객체 및 함수는 plugins 폴더에 위치 시켰다. 해당 파일들은 다른 파일에서 사용된다.

image

This post is licensed under CC BY 4.0 by the author.

Project SW Test Forum 5. 프로젝트 구조 설계 - 1

Project SW Test Forum 7. CRUD 게시판 구현 및 오류 해결(벡엔드) - 2

Comments powered by Disqus.

Trending Tags