Potato

모달외부를 클릭했을 때 모달이 꺼지는 이벤트를 구현하고 싶었다

 

 

<template>
<div ref="List">
	...
</div>
</template>

우선 모달의 범위를 ref를 통해 속성명을 할당시켰다.

 

 

mounted() {
    if (this.open_list_modal) {
      document.addEventListener("click", this.ListoutClick);
    } else {
      document.removeEventListener("click", this.ListoutClick);
    }
  },

그리고 모달창이 열렸을 때, document에 클릭 이벤트를 걸어주었고,

모달창이 닫혔을때 클릭이벤트가 없어지게 하였다.

 

methods: {
    ListoutClick(e) {
      if (this.$refs.List == null) {
        return;
      } else if (!this.$refs.List.contains(e.target)) {
        this.close_list_modal();
      }
    },
}

 

리스트 모달이 켜져 있을 동안 클릭을 하면 그곳이 모달 범위 안인지, 밖인지 확인을 하고

만약 모달범위밖을 클릭했다면 모달창을 끄는 함수를 실행시키도록 하였다.

 

프로젝트 진행 중 방대한 양의 데이터를 한페이지에 표시하기 위해 페이지네이션과 무한스크롤중 하나를 선택했어야 했는데, 해당 프로젝트에서는 모바일 친화적인 UI이기 때문에 페이지네이션보단 무한스크롤을 이용하는게 더 좋다고 판단하여 무한스크롤을 통해 개발을 진행하였다.

 

              <div
                @scroll="InfiniteScroll"
              ></div>

 

 

무한스크롤을 적용하고싶은 div에 @scroll을 통해 해당 div가 스크롤 될 때 마다 이벤트가 실행되게 한다

 

 

InfiniteScroll(e) {
  const { scrollHeight, scrollTop, clientHeight } = e.target;
  if (scrollTop + clientHeight === scrollHeight) {
    this.RequestAPI(this.data);
  }
}

해당 div의 scrollHeight ( 전체 div의 높이 ), scrollTop ( 현재 스크롤의 위치 ), clientHeight ( 현재 보이는 화면의 높이) 를 정의한다.

 

이벤트를 실행할 때 마다 ( 현재 스크롤의 위치 + 현재 보이는 화면의 높이 )가 ( 전체 div의 높이 )와 같은지 확인 한 후 만약같다면 추가적인 api요청을 통해 화면에 뿌려주게 했다.

 

 

 

 

하지만, 개발자환경에서는 정상적으로 동작하였으나 정작 모바일로 보았을 때 추가 api요청이 요청되지 않았다.

처음엔 모바일환경에서는 @scroll이 실행되지 않는 줄 알았으나 안드로이드 adb를 통해 모바일로 개발자환경을 확인한 결과 모바일에서는 스크롤을 전부 내려도 scrollHeight의 높이가 scrollTop + clientHeight의 차이가 대략 0.9정도의 차이가 나는걸 확인했고, 조건을 약간 수정하여서 정상적인 동작이 가능하게 하였다.

 

 

 

개발 중에 검색버튼이 없는 인풋을 구현했는데 인풋값이 바뀔 때 마다 api요청을 보내면 리소스 낭비가 심하기 때문에 마지막 인풋 후 일정시간 이후에 실행되는 디바운스를 구현하였다.

 

<template>      
      <input
        type="text"
        placeholder="Search..."
        class="navbar-input"
        @input="input_search_data = $event.target.value"
      />
</template>

 

<script>
import debounce from "lodash.debounce";

export default {
	...
  data() {
    return {
      input_search_data: "",
    };
  },
  
  watch: {
    input_search_data: debounce(function(e) {
      if (e == '') { 
        return //인풋값을 받았다가 1초안에 다시 지우는 경우 (인풋이 취소된 경우)
      } else {
        this.input_search_data = "" // 인풋값을 초기화하도록 구현하였음
        this.search_data(e) // api요청
      }
    }, 1000),
  },
}

</<script>

 

 

 

loadsh에서 debounce를 임포트 시킨 후

watch를 통해 input_search_data를 봐주면서 마지막 변경 후 1초가 지나면 인풋값으로 api요청을 보내면된다.

 

 

디바운스와 비슷한 용어로 쓰로틀링이 있는데, 이건 다음에 알아보도록 하겠다.

롱 클릭 했을 시 오른쪽과 같은 화면이 된다

프로젝트를 구현하던 중 롱클릭시 해당 이미지가 선택된걸 구현한 뒤, 별을 누르면 첫 번째 이벤트가 발생하고 그 외의 부분을 누르면 두 번째 이벤트가 발생하게 구현하려고 했는데 이벤트 버블링 때문에 별을 누르면 첫 번째 이벤트와 두 번째 이벤트가 동시에 발생했다.

 

검색을 하며 알아보니, Vue에선 이벤트 버블링을 막아주는 기능이 있었는데 바로 click.stop="''"이다.

 

하위에 있는 태그에 click.stop 이벤트를 걸어주면 그 아래에있는 다른 이벤트들이 동작하지 않게 된다.

 

<div>
  <font-awesome-icon
    class="scrap-icon"
    icon="fa-regular fa-star"
    @click="open_list_modal"
    @click.stop="''"
  />
</div>

 

프로젝트 진행 중, 모달이 생성될 때, 뒤에 있는 상위 컴포넌트가 스크롤되는 부분이 굉장히 어색하고 사용자 입장에서 불편할 것이라고 생각하고 스크롤 제어하는 법을 알아 보았다.

 

처음에 시도 한 것은 css를 통해 모달이 열리는 순간, 상위 컴포넌트의 width와 height를 100%, overflow값을 hidden으로 주는 방법을 시도 해보았는데, 어느정도 휠을 내린 후 모달을 열면 컴포넌트의 크기가 100%로 재조정 되어서 모달을 열기 전화면과 모달을 열고 난 후 보이는 상태가 달라지고 또한 모달이 닫히면 다시 처음위치로 돌아왔기 때문에

다른 방법으로 DOM을 제어하는 방법을 선택했다.

 

 

<script>
import { useStore } from "vuex";
import { computed } from "@vue/runtime-core";

export default {
  name: "SearchView",
  setup() {
    const store = useStore();

    const open_search_modal = computed(
      () => store.state.searchModalStore.open_search_modal
    );

    return {
      open_search_modal,
    };
  },

 

computed를 통해 처음에 store에 있는 search_modal의 값이 true인지 false인지 확인해주고

 

  watch: {
    open_search_modal: function (value) {
      value
        ? (document.body.style.overflow = "hidden")
        : document.body.style.removeProperty("overflow");
    },
  },

 

watch를 통해 해당 값이 true라면 DOM에 overflow hideen을 걸어주고

그게 아니라면 overflow를 주는 방식으로 해결했다.

axios를 통한 비동기 작업을 알아보려고 한다

 

 

1. api 모듈화를 위해 src 하위에 api폴더를 생성 후 index.js와 팀원들이 각자 사용할 API 파일을 생성

 

 

2. index.js 에서 axios 임포트, baseURL과 headers 설정

 

import axios from 'axios';

function apiInstance() {
  const instance = axios.create({
    baseURL: process.env.VUE_APP_API_BASE_URL,
    headers: {
      'Content-Type': 'application/json;charset=utf-8',
    },
  });
  return instance;
}

export { apiInstance };

 

 

3. baseURL 노출을 피하기 위해 env.local 파일 생성

 

 

4. env.local에 baseURL 설정 (배포 전 로컬 사용중)

VUE_APP_API_BASE_URL=http://localhost:8080/

 

 

5. 본인이 사용할 파일에서 다음과 같은 코드 작성

// src/api/Data1.js

import { apiInstance } from './index.js';
const api = apiInstance();

 

 

이제 설정은 끝!

직접 사용해보자.

 

6. 본인의 파일로 dispatch를 통해 actions 호출

 

<template>

</template>

<script>
import { useStore } from "vuex";
import { onBeforeMount } from "@vue/runtime-core";

export default {
  name: "TestView",
  setup() {
    const store = useStore();
    
    onBeforeMount(() => {
      store.dispatch("Member1/getData"),
    });

};
</script>

<style>

</style>

 

7. 모듈화된 store 파일에서 호출될 함수 정의

 

// src/store/modules/Member1.js

const Member1 = {
  namespaced: true,
  state: () => ({
    isLogin: false,
  }),
  mutations: {
    log_in(state) {
      state.isLogin = true
    },
  },
  getters: {},
  actions: {
    getData({ commit }) {
    	getItem(
          (data) => {
            commit('get_result', data)
          },
          (err) => {
            console.log(err);
          }
        )
      },
  },
};

export default Member1;

 

8. API 파일작성 및 export

 

// src/api/Data1.js

import { apiInstance } from './index.js';
const api = apiInstance();

function getItem(res, err) {
  api.get(`요청할 URL`)
    .then(res).catch(err)
}

export getItem

 

 

9. 다시 store 파일로 돌아와 API에서 export 해온걸 임포트

 

// src/store/modules/Member1.js

import getItem from "@/api/Data1";

const Member1 = {
  namespaced: true,
  state: () => ({
    isLogin: false,
  }),
  mutations: {
    log_in(state) {
      state.isLogin = true
    },
  },
  getters: {},
  actions: {
    getData({ commit }) {
    	getItem(
          (data) => {
            commit('get_result', data)
          },
          (err) => {
            console.log(err);
          }
        )
      },
  },
};

export default Member1;

 

10. 해당 axios 작업을 통해 받아온 데이터들을 state에 정의

 

// src/store/modules/Member1.js

import getItem from "@/api/Data1";

const Member1 = {
  namespaced: true,
  state: () => ({
    isLogin: false,
    my_data: null
  }),
  mutations: {
    log_in(state) {
      state.isLogin = true
    },
    get_result(state,data) {
      state.my_data = data
    }
  },
  getters: {},
  actions: {
    getData({ commit }) {
    	getItem(
          (data) => {
            commit('get_result', data)
          },
          (err) => {
            console.log(err);
          }
        )
      },
  },
};

export default Member1;

 

이번에는 vuex4 store 모듈화를 통해 데이터를 사용 변경하는법을 알아보려고 한다.

 

1. 데이터 사용하기

 

1-1. 모듈화한 파일에서 state 저장

const Member1 = {
  namespaced: true,
  state: () => ({
    isLogin: false,
  }),
  mutations: {},
  getters: {},
  actions: {},
};

export default Member1;

 

1-2. 사용하고자하는 컴포넌트에서 useStore 와 computed 임포트

 

<template>

</template>

<script>
import { useStore } from "vuex";
import { computed } from "@vue/runtime-core";

export default {
  name: "TestView",
};
</script>

<style>

</style>

 

 

1-3. setup()에서 사용하고자 하는 state의 데이터를 computed

 

<template>

</template>

<script>
import { useStore } from "vuex";
import { computed } from "@vue/runtime-core";

export default {
  name: "TestView",
  setup() {
    const store = useStore();
    
    const isLogin = computed(
      () => store.state.Member1.isLogin
    );
    
    return {
      isLogin
    };
};
</script>

<style>

</style>

 

 

2. 데이터 변경하기

 

2-1. 변경이 필요한 컴포넌트에서 setup을 통해 commit

 

<template>

</template>

<script>
import { useStore } from "vuex";
import { computed } from "@vue/runtime-core";

export default {
  name: "TestView",
  setup() {
    const store = useStore();
    
    const isLogin = computed(
      () => store.state.Member1.isLogin
    );
    
    const Login = () => {
      store.commit("Member1/log_in")
    };
    
    return {
      isLogin
    };
};
</script>

<style>

</style>

 

 

2-2. 본인이 관리하는 store 파일에서 mutation 코드 작성

 

// src/store/modules/Member1

const Member1 = {
  namespaced: true,
  state: () => ({
    isLogin: false,
  }),
  mutations: {
    log_in(state) {
      state.isLogin = true
    },
  },
  getters: {},
  actions: {},
};

export default Member1;

 

 

다음 포스팅에서는 모듈화 상태에서 axios를 통한 비동기 작업을 알아보려고 한다.

Vue3를 통한 프로젝트 중 Store모듈화의 필요성을 느끼고 공부하게 되었다.

 

 

1. Vuex4 설치

npm install --save vuex@4.0.1

 

2. index.js에 다음과 같은 코드 작성

 

// src/store/index.js

const store = new Vuex.Store({
  modules: {
  },
});
export default store;

 

3. modules 라는 폴더에 각자 필요한 데이터들을 저장하고 사용할 파일생성

설명을 위한 예시, 파일이름은 자유

4. 자신이 사용할 파일에서 다음과 같은 스켈레톤 코드 입력

// src/store/modules/Member1.js

const Member1 = {
  namespaced: true,
  state: () => ({
  }),
  mutations: {},
  getters: {},
  actions: {},
};

export default Member1;

 

5. 해당 파일을 index.js에서 임포트

 

// src/store/index.js

import Vuex from 'vuex';

// 작성한 모듈을 가져오기
import Member1 from '@/store/modules/Member1.js';

const store = new Vuex.Store({
  modules: {
    Member1,
  },
});
export default store;

 

 

다음 포스팅에서는 모듈화한 데이터를 컴포넌트에서 사용하는법과 변경하는법을 알아보려고 한다.

터미널에 다음과 같은 명령을 실행

 

$npm install vuex@next

src/ 폴더 안에 store.js 파일 생성후 다음과 같은 스켈레톤 코드 작성

 

//Store.js

import { createStore } from 'vuex'

const store = createStore({
  state(){
    return {
      
    }
  },
})

export default store

main.js에서 store를 import후 다음과 같은 코드 작성

 

//main.js

...
import store from './store.js'


...

app.use(store),mount('#app')

 

터미널에 다음과 같은 명령어 입력 ( 두 가지 중 하나만 입력하면 됨 )

$npm install vue-router@4
or
$yarn add vue-router@4

or

 

src폴더 내부에 router.js 파일을 만듦

 

//router.js

import { createWebHistory, createRouter } from "vue-router";
import ArticleList from "./components/ArticleList"

const routes = [
  {
    path: "/ArticleList",
    component: ArticleList,
  }
];

const router = createRouter({
  history: createWebHistory(),
  routes,
});

export default router;

 

main.js에서 router.js를 임포트후 createApp 뒤에 use() 추가

// main.js

import { createApp } from 'vue'
import App from './App.vue'
import router from './router.js'

createApp(App).use(router).mount('#app')

 

<router-link to="/path"> </router-link>를 통해 등록한 path로 이동 할 수 있고

보여주고싶은 위치에서 <router-view></router-view> 추가

 

// App.vue

...    
    
    <router-link to="/ArticleList">글목록</router-link>
    
    <router-view></router-view>
    
...

 

컴포넌트 구성

 

 

하위 A컴포넌트 ( AChild )에서 버튼을 누르면 B 컴포넌트 ( BChild ) 에게 데이터를 전달해서 출력을 하고 싶다.

 

 

 

 

A 컴포넌트에서 버튼을 클릭했을 때 yes나 no라는 함수를 호출하고 yes나 no함수는 각각 'yes', 'no' 라는 소리를 상위 컴포넌트에 외치게 된다.

<!-- AChild.vue -->

<template>
  <div>
    <h5>{{ msg }}</h5>
    <button @click="yes">O</button>
    <button @click="no">X</button>
  </div>
</template>

<script>
export default {
  name: 'AChild',
  props: {
    msg: String
  },
  methods: {
    yes() {
      this.$emit('yes')
    },
    no() {
      this.$emit('no')
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style>

</style>

 

 

Achild에서 yes라는 소리를 들으면 Yes함수를 실행, no라는 소리를 들으면 No라는 함수를 실행하여 mystate에 값을 저장한다.

<!-- App.vue -->

<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <div id="box">
      <h2>props, emit을 활용한 데이터 전달</h2>
      <AChild 
      :msg="Amessage"
      @yes="Yes"
      @no="No"
      />
      <BChild 
      :msg="Bmessage"
      :mystate="mystate"
      />
    </div>
  </div>
</template>

<script>
import AChild from './components/AChild'
import BChild from './components/BChild'

export default {
  name: 'App',
  components: {
    AChild,
    BChild,
  },
  data() {
    return {
      Amessage : "Props를 통해 A컴포넌트로 보낸 메세지",
      Bmessage : "Props를 통해 B컴포넌트로 보낸 메세지",
      mystate : '입력 없음'
    }
  },
  methods: {
    Yes() {
      console.log('O버튼 요청보냄');
      this.mystate = 'yes'
    },
    No() {
      console.log('X버튼 요청보냄');
      this.mystate = 'no'
    }
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  text-align: center;
  margin-top: 60px;
}

#box {
  border : 1px solid gray;
}

</style>

 

BChild에서 APP의 mystate를 props를 사용하여 데이터를 가져와서 출력

<!-- BChild.vue -->

<template>
    <div>
      <h5>{{ msg }}</h5>
      <h3>현재 상태: {{ mystate }}</h3>
    </div>
  </template>
  
  <script>
  export default {
    name: 'BChild',
    props: {
      msg: String,
      mystate : String,
    }
  }
  </script>
  
  <!-- Add "scoped" attribute to limit CSS to this component only -->
  <style>
  
  </style>

'WEB > Vue' 카테고리의 다른 글

[Vue3] Vuex 설치하기  (0) 2023.01.25
[Vue3] Vue-router 설치하기  (0) 2023.01.15
[Vue2/Vuex] state 활용 맛보기  (0) 2022.12.29
[Vue2] EventBus를 통해 컴포넌트간 데이터 전달하기  (0) 2022.12.29
[Vue2] Vue Router 사용하기  (0) 2022.11.09

Vuex의 index.js의 state를 통해 데이터를 전달하는 방법을 간단하게 코드를 구현해보았다.

 

 

 

컴포넌트 구성

 

 

 

App.vue에서 AChild, BChild라는 두 개의 하위 컴포넌트를 가지고 있음

<!-- App.vue -->

<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <div id="box">
      <AChild msg="하위 A 컴포넌트"/>
      <BChild msg="하위 B 컴포넌트"/>
    </div>
    
  </div>
</template>

<script>
import AChild from './components/AChild'
import BChild from './components/BChild'

export default {
  name: 'App',
  components: {
    AChild,
    BChild
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  text-align: center;
  margin-top: 60px;
}

#box {
  border : solid 1px gray;
}
</style>

 

AChild에서

O버튼을 누르면 yes 데이터 전달

X버튼을 누르면 no 데이터 전달

 

<!-- AChild.vue -->

<template>
  <div>
    <h1>{{ msg }}</h1>

    <button @click="Yes">O</button>
    <button @click="No">X</button>
  </div>
</template>

<script>
export default {
  name: 'AChild',
  props: {
    msg: String
  },
  methods: {
    Yes() {
      this.$store.dispatch('send','yes')
    },
    No() {
      this.$store.dispatch('send','no')
    }
  },
}
</script>

<style>

</style>

 

 

index.js에서

actions(비동기 작업이 없으므로 생략 가능) -> mutations -> state순으로 데이터 전달

//index.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    mystate: '입력없음'
  },
  getters: {
  },
  mutations: {
    SEND(state, data) {
      state.mystate = data
    }
  },
  actions: {
    send(context,data) {
      context.commit('SEND', data)
    } 
  },
  modules: {
  }
})

 

index.js에 있는 mystate의 값이 변할 때(계산될 때) 마다 화면에 출력해야 하므로 computed를 이용해 출력 

<!-- BChild.vue -->

<template>
    <div>
      <h1>{{ msg }}</h1>
      <h3>현재상태 : {{ change }}</h3>
    </div>
  </template>
  
  <script>
  export default {
    name: 'BChild',
    props: {
      msg: String
    },
    computed: {
      change() {
         return this.$store.state.mystate
      }
    }
  }
  </script>
  
  <style>
  
  </style>

 

위와 같은 컴포넌트 구성에서 자식 A에서 이벤트를 보냈을 때 자식 B에서 출력이 되게 하고 싶을 때,

props와 emit을 이용하여 데이터를 전달하는 방법도 있지만, EventBus를 통해 부모 컴포넌트를 거치지 않고 자유롭게 데이터를 전달 할 수 있다.

 

 

 

컴포넌트 구성

 

EventBus를 이용하기위해 다음과 같은 코드를 main.js에 작성한다.
//main.js

Vue.prototype.$EventBus = new Vue();

 

 

O버튼을 클릭하면 EventBus를 통해 send-yes를 보낸다
반대로 X버튼을 클릭하게되면 send-no를 보낸다

 

 

<!-- CChild.vue -->

<template>
  <div>
    <h3>하위 C 컴포넌트</h3>
    <button @click="Eventyes">O</button>
    <button @click="Eventno">X</button>
  </div>
</template>

<script>
export default {
  name : 'CChild',
  methods: {
    Eventyes () {
      this.$EventBus.$emit('send-yes')
    },
    Eventno () {
      this.$EventBus.$emit('send-no')
    },
  },
}
</script>

<style>

</style>

 

 

EventBus를 통해 send-yes나 send-no를 받았다면 Yes나 No함수를 실행시켜 mystate의 데이터 값을 바꾼다
<!-- DChild.Vue -->

<template>
  <div>
    <h4>하위 D 컴포넌트</h4>
    <h3>현재 상태 : {{ mystate }}</h3>
  </div>
</template>

<script>
export default {
  name : 'DChild',
  data() {
    return {
      mystate : '입력없음'
    }
  },
  created() {
    this.$EventBus.$on('send-yes', this.Yes),
    this.$EventBus.$on('send-no', this.No)
  },
  methods: {
    Yes() {
      this.mystate = 'yes'
    },
    No() {
      this.mystate = 'no'
    }
  },
}
</script>

<style>

</style>

 

 

 

 

실행화면

 

Routing

  • 네이트워크에서 경로를 선택하는 프로세스
  • 웹 서비스에서의 라우팅
    • 유저가 방문한 URL에 대해 적절한 결과를 응답하는 것

Vue Router

  • Vue의 공식 라우터
  • SPA 상에서 라우팅을 쉽게 개발할 수 있는 기능을 제공
  • 라우트에 컴포넌트를 매핑한 후, 어떤 URL에서 렌더링 할지 알려줌
    • 즉, SPA를 MPA처럼 URL을 이동하면서 사용가능
    • SPA 단점 중 하나인 URL이 변경되지 않는다.를 해결

Vue Router 사용하기

$ vue create vue-router-app // Vue 프로젝트 생성

$ cd vue-router-app // 디렉토리 이동

$ vue add router // Vue CLI를 통해 router plugin 적용

커밋을 할것인지 (권장) -> Yes

history mode 사용 여부 -> Yes

$ npm run serve
  • run serve 후 router 확인

 

Vuex

  • Vuex
    • 중앙 저장소를 통해 상태 관리를 할 수 있도록 하는 라이브러리
    • 데이터가 예측 가능한 방식으로만 변경 될 수 있도록 하는 규칙을 설정하며, Vue의 반응성을 효율적으로 사용하는 상태 관리 기능을 제공

사용하기 전에..

$vue create vuex-app // Vue 프로젝트 생성

$cd vuex-app // 디렉토리 이동

$vue add vuex // Vue CLI를 통해 vuex plugin 적용

vuex의 핵심 컨셉 4가지

  1. State
    • vue 인스턴스의 data에 해당
    • 중앙에서 관리하는 모든 상태 정보
  2. Mutations
    • 실제로 state를 변경하는 유일한 방법 ( state를 변경하기 위한 methods )
    • Mutations에서 호출되는 핸들러(handler)함수는 반드시 동기적 이어야 함
  3. actions
    • mutations와 비슷하지만 비동기 작업( 외부 API와의 소통 등 )을 포함할 수 있음
    • state를 직접 변경하지 않고 commit()메서드로 mutations를 호출해서 state를 변경함
  4. Getters
    • vue 인스턴스의 computed에 해당
    • state를 활용하여 계산된 값을 얻고자 할 때 사용

component에서 데이터를 조작하기 위한 데이터의 흐름

  • component => (actions) => mutations => state

component에서 데이터를 사용하기 위한 데이터의 흐름

  • state => (getters) => component

+ Recent posts