[Vue3 - Part3] 인스타그램 프로젝트 만들기1 (ajax 요청, 사진 업로드하기 - URL.createObjectURL, slot, mitt)

흑염소·2024년 2월 26일

coding apple - Vue

목록 보기
5/6

✨ 인스타그램 프로젝트 만들기

프로젝트 생성

vue create 프로젝트명

[X] Default ([Vue3] babel, eslint)

생성 명령어 입력하고 옵션 vue3 선택함

npm run serve

실행 명령어 입력

style 속성 데이터바인딩

뷰에서 style 속성 데이터바인딩 하려면 :style="" 안에 자바스크립트 코드를 넣으면 된다.
보다 깔끔한 코드를 위해서 object 데이터 형태로 다중 css 속성을 넣을 수 있다.

<div :style="{ fontSize : '20px', marginTop : '10px' }">
<div :style="{ color : 'red' }">

자바스크립트에서 css 속성명은 대시기호를 쓸 수 없기 때문에 카멜케이스로 작성한다.

ajax 요청

ajax 요청하려면

  1. axios 라이브러리 사용
  2. 기본 내장 fetch 함수 사용

호환성 때문에 axios 사용하기로 함.

npm install axios

get() 요청

axios 요청하는 방법은 import 해오고 axios.get('/경로') 하면 됨.

<template>
  <ContainerComp :data="data" />
  <button @click="handleClick">더보기</button>
</template>

더보기 button 클릭시 axios 요청 메서드 작동

<script setup>
import postdata from '@/assets/postdata';
import axios from 'axios';
import { ref } from 'vue';

// data
const data = ref(postdata);

// methods
const handleClick = async () => {
  const response = await axios.get('https://codingapple1.github.io/vue/more0.json');
  data.value.push(response.data);
}
</script>

vue3 composition API 방식으로 코딩해봤다.

post() 요청

axios.post('URL', {name: 'KIM'}).then((res)=>{실행코드}).catch((err)=>{실행코드})

성공하면 then() 실행
실패하면 catch() 실행
get요청할때 쓴 것 처럼 async await 문법으로 then() 대신 결과를 변수에 담아 사용해도 된다.

사진 업로드 페이지 만들기

라우터 없이 tab으로 페이지 구분하기

(app.vue)
<script setup>
// tab 구분용 state
const step = ref(0);
</script>

<template>
  // 하위 컴포넌트로 props 전달
  <ContainerComp :step="step" />
</template>

(최상위 컴포넌트는 composition API로 script 작성했음)
step 값에 따라 tab이 변경되도록 UI를 구성했다.
props를 넘겨준 하위 컴포넌트에서 나머지를 세팅해보자.

(ContainerComp.vue)

<template>
  <div>
    <!-- 글목록 페이지 -->
    <div v-if="step == 0">
      <PostComp :data="data[idx]" v-for="(item, idx) in data" :key="idx" />
    </div>

    <!-- 필터선택페이지 -->
    <div v-if="step == 1">
      <div class="upload-image"></div>
      <div class="filters">
        <div class="filter-1"></div>
        <div class="filter-1"></div>
        <div class="filter-1"></div>
        <div class="filter-1"></div>
        <div class="filter-1"></div>
      </div>
    </div>

    <!-- 글작성페이지 -->
    <div v-if="step == 2">
      <div class="upload-image"></div>
      <div class="write">
        <textarea class="write-box">write!</textarea>
      </div>
    </div>
  </div>
</template>
  
<script>
export default {
  props: {
    step: Number
  }
}
</script>

각 섹션마다 wrap div에 v-if를 사용해 step을 구분해줬다.
이제 상위 컴포넌트의 step 값이 변경될 때 마다 각 값에 맞는 tab이 노출된다.

서버없이 업로드한 이미지 다루기

이미지 업로드한걸 HTML에 보여주려면 어떻게 해야할까?
이전 방식은 서버가 필요하다.

  1. 업로드한걸 서버로 보내고 저장시킴
  2. 서버가 저장된 URL을 유저에게 보냄
  3. 그걸 img src=""에 넣으면 사진이 뿅

하지만 익스11 이후부터는 이미지 다루는 함수를 쓰면 된다.
브라우저에서 이미지 다루는 함수를 사용하면 서버로 보내기도 전에 내가 업로드한 이미지를 바로 띄워보고 조작할 수 있음.

  1. FileReader() : 파일을 글자로 변환해줌
  2. URL.createObjectURL() : 이미지의 가상 URL을 생성해줌

둘중 사용하면 됨.
createObjectURL이 좀 더 가벼우니 이거 사용해보기로.

<ul class="footer-button-plus">
  <input @change="uploadImage" type="file" id="file" class="inputfile" />
</ul>

업로드한 이미지를 받아오는 탬플릿이다.
해당 input 태그에 @change 이벤트리스너로 함수를 넘겨줬다.

const imageUrl = ref('');

const uploadImage = (e) => {
  let files = e.target.files;
  let url = URL.createObjectURL(files[0]);
  imageUrl.value = url;
  
  step.value = 1; // 이미지 노출 확인 페이지(tab) 이동
}

input에서 받아오는 e.target.files에는 업로드한 모든 파일 정보를 담고있다.
변수에 담아주자.
콘솔에 출력해보면 FileList라는 객체에 정보가 담겨오기 때문에 file[0] 식으로 배열 선택을 해줘야한다.

업로드한 이미지를 띄워보자.
URL.createObjectURL() 함수를 이용해서 가상 url을 생성해줬다.
가상 url을 변수에 담고 props로 하위 컴포넌트에 전달 해준다.

(app.vue)

<template>
  <ContainerComp :url="imageUrl" />
</template>
(ContainerComp.vue)

<template>
<!-- 필터선택페이지 -->
<div v-if="step == 1">
  <div class="upload-image" :style="{backgroundImage: `url(${url})`}"></div>
  ...
</div>
</template>
  
  
export default {
  props: {
    url: String,
  }
}

다중 이미지 업로드 방법

<ul class="footer-button-plus">
  <input type="file" id="file" multiple />
</ul>

input 태그에 multiple 속성을 추가해주면 이미지 업로드시 다중 선택이 가능하다.

이미지 파일 형식 제한

<ul class="footer-button-plus">
  <input type="file" id="file" accept="image/*" />
</ul>

accept 옵션을 사용하면 input 태그로 받아오는 파일 형식을 제한할 수 있다.
근데 사실 제한이라기 보다는 선택 화면에서 특정 형식만 보이도록 하는 거라서 이용자가 선택에서 바꿀 수 있다.
엄격한 파일 형식 제한을 위해서는 자바스크립트로 확장자를 검열하는 작업이 필요하다.

const uploadImage = (e) => {
  let files = e.target.files;
  console.log(files[0].type);
}

files[0].type하면 확장자 확인 가능.
내가 원하는 타입만 거를 수 있게 엄격히 작업하는게 좋다.

글 발행기능 만들기

메인페이지에 Post 컴포넌트를 하나 더 추가하는게 글 발행 기능이다.
이용자가 업로드한 이미지에 작성한 글을 포함하여 게시물을 추가한다.

(app.vue)

<ul class="header-button-right">
  <li v-if="step == 1" @click="step++">Next</li>
  <li v-if="step == 2" @click="publish">발행</li>
</ul>
const publish = () => {
  let newPost = {
  	내용들
  };
  data.value.unshift(newPost) // 새로 배열 추가
  step.value = 0; // 메인 페이지로 돌아가기
}

unshift()는 배열의 맨 상단, 처음부분에 해당 자료를 추가해준다.

업로드한 이미지 필터 기능 만들기

(index.html)

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cssgram/0.1.12/cssgram.min.css" integrity="sha512-kr3JaEexN5V5Br47Lbg4B548Db46ulHRGGwvyZMVjnghW1BKmqIjgEgVHV8D7V+Cbqm/VBgo3Rcbtv+mGLoWXA==" crossorigin="anonymous" />

인스타그램 이미지 필터기능을 위해 cssgram cdn 추가
link 태그를 직접 최상위 index.html에 추가해준다.

(ContainerComp.vue)

<template>
<!-- 필터선택페이지 -->
    <div v-if="step == 1">
      <div class="upload-image" :style="{backgroundImage: `url(${url})`}"></div>
      <div class="filters">
        <FilterBox :url="url" :filters="arr" v-for="arr in filters" :key="arr" />
      </div>
    </div>
</template>

<script>
  ...
data() {
    return {
        filters: [ "aden", "_1977", "brannan", ...]    
    }
  },
</script>

상위컴포넌트에서 필터명들을 data()에 담고 v-for 사용해서 컴포넌트에 각 array들을 내려준다.
하위 컴포넌트에서 props로 받아온 내용대로 처리해주면 끝

(FilterBox.vue)

<template>
  <div class="filter-item" :class="filters" :style="`background-image: url(${url})`"></div>
</template>
<script>
export default {
    props: {
        filters: String
    }
}

html상으로 표현되는 클래스명은 filter-item :class명이 된다.

props 대신 slot

사진 위에 필터명을 표시해보자.
부모 컴포넌트에서 자식 컴포넌트로 쉽게 데이터 보내는 방법이 props 말고도 존재한다.
slot 사용하면 됨.
props를 등록하는 과정 없이 조금 더 직관적으로 보내고 사용할 수 있다.
react에서 컴포넌트 안에 사용하는 {children}과 같은 개념이다.

  1. 자식은 구멍 뚫기
  2. 컴포넌트를 홀로닫기 태그 말고 나눠서 닫고 그 사이에 데이터 넣기
(상위컴포넌트)
  <!-- <FilterBox :url="url" :filters="a" v-for="(a) in filters" :key="a" /> -->
  <FilterBox :url="url" :filters="a" v-for="(a) in filters" :key="a">
      {{ a }}
  </FilterBox>
(하위컴포넌트)

  <div class="filter-item" :class="filters">
    <slot></slot>
  </div>

출력된 화면을 보면 {{ a }} 로 전달한 필터 이름이 제대로 화면에 표시되고 있다.

그런데 slot이 props의 대체제가 되지 못하는 이유가 무엇일까?
slot 문법은 데이터바인딩을 html에 하고 싶을 때 사용한다.
속성에 데이터바인딩이 불가하다는 의미.
즉, :class 같은거에 내려받은 slot 데이터를 사용할 수 없다!

slot 여러개 사용하는 방법

부모 컴포넌트 여기저기서 slot으로 데이터를 내려보냈을 경우,
자식 컴포넌트에서는 각 부모에 맞는 slot 노출란을 만들어야 한다.

  1. 자식 컴포넌트에서 name="" 사용하기
<div>
  <slot name="a"></slot>
  <slot name="b"></slot>
</div>

slot name="" 으로 각 고유 slot 이름을 등록해주면 된다.

  1. 부모 컴포넌트에서 v-slot 사용하기
<div>
  <template v-slot="a"> 데이터1 </template>
  <template v-slot="b"> 데이터2 </template>
</div>

마찬가지로 부모 컴포넌트에서는 template v-slot="슬롯명"으로
알맞은 slot에 전송하도록 지정해주면 된다.

slot은 컴포넌트간 html을 담아서 전송하는 용도로만 사용하도록 하자.
slot 사용할 때 부모가 자식 데이터 필요한 경우 사용하는 slot props도 있다.

멀리 있는 컴포넌트간 데이터 전송은 mitt

mitt 세팅

필터 박스를 클릭 시 해당 필터가 이미지에 적용되도록 해보자!
상위 컴포넌트의 상위 컴포넌트로 이벤트를 넘겨야한다.
$emit을 두번 중첩 사용하기보다 한번에 원하는 컴포넌트로 다이렉트로 전달하는 방법을 사용하자.
vue3에서는 mitt 라이브러리를 사용한다.
조부모는 물론 하위 컴포넌트, 형제 컴포넌트로도 데이터 전달이 가능하다!

npm install mitt

설치를 진행한다.

(main.js)

import mitt from 'mitt'
// emitter 생성
let emitter = mitt();

const app = createApp(App)

// 추가!
app.config.globalProperties.emitter = emitter;

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

mitt 라이브러리 세팅을 해준다.
자주 쓰는 라이브러리는 여기다 전부 등록해도 된다.

예시 ) axios 등록하는 법

app.config.globalProperties.axios = axios;

이러면 컴포넌트에서 import 해서 사용할 필요 없이 this.axios로 사용할 수 있다!

mitt 데이터 전송하는 방법

<button @click="fire">버튼</button>

template에 이벤트와 메서드를 연결해주고

methods: {
  fire() {
      this.emitter.emit('작명', '데이터')
  }
},

메서드에 this.emitter.emit('작명', '전달하는 데이터') 형식으로 작성하여 다른 컴포넌트로 emit 해준다.

전송한 데이터 수신하는 방법

<script>
export default {
  data() {
    return {
    };
  },
  mounted() { 
    this.emitter.on("event명", (param) => {코드실행}
    );
};
</script>

this.emitter.on("이벤트명", ()=>{})으로 수신하면 된다.
이벤트명을 누군가 실행하면 내부 ()=>{} 함수 코드란을 실행하라는 의미다.

아래는 vue3 composition API에서 수신하는 방법.

import { getCurrentInstance } from 'vue'
export default{

   setup(){
      const internalInstance = getCurrentInstance(); 
      const emitter = internalInstance.appContext.config.globalProperties.emitter;
      emiiter.on('event명', (param) => {
        // 내부 코드 실행
      })
       ...
     },
  ...

}

근데 많이 쓰다보면 이벤트명 겹치고 관리가 힘들어진다.
그러므로 최종적으로는 Vuex로 상태관리 하는게 좋다.

profile
매일 TIL 중인 비전공자 프론트 개발자

0개의 댓글