개발 도구 (Development Tools)/Vue

Vue - unsplash API를 활용한 이미지 검색 앱 만들어보기

BiCute 2022. 9. 29. 08:00
반응형

이제 어느 정도 Vue의 기본에 대한 글은 대부분 작성했기에 종합편스럽게 뭐라도 하나 만들어 볼까 생각하고 있었습니다.

(그럼에도 글로 적기 너무 길지 않은 걸로...)

예전에 이미지 검색 앱 만드는게 잠깐 유행했었는데, 다들 React로 만드는 글이었기에 그걸 Vue로 한번 만들어 봤습니다.

 

 

최종 구현 화면 샘플

  이번 글에선 대충 아래와 같은것을 만들어 볼 것입니다.

첫 화면
이미지를 검색하면 이렇게

 

 

 

1. unsplash 개발자 등록 

Unsplash Developers 사이트로 이동하여 개발자 등록(회원가입)을 진행합니다.

회원 가입 완료 후 상단의 Your apps로 이동하여 New Application을 클릭하여 새로운 프로젝트를 등록합니다.

모든 항목 체크 > 어플리케이션 이름 및 설명을 입력하면 완료

Application 등록을 완료한 후 볼 수 있는 중앙에 위치한 Keys는 나중에 사용하게 됩니다.

 

 

2. 프로젝트 생성

vue가 이미 설치되어 있다면 터미널에 [vue create 프로젝트명]을 입력하면 끝.

vue가 설치가 되어있지 않고 처음이라면 Vue 개발 환경 세팅하기 부터 시작.

 

 

 

3. 기본 세팅 파일 정리

기본 세팅되어있는 항목 중 사용하지 않는 내용들은 모두 제거.

(1) app.vue파일의 Hellow World 컴포넌트 연결 및 샘플 텍스트

(2) assets폴더의 logo

(3) public 폴더의 faicon.ico 등.

 

 

 

4. 기본 틀 만들기 

전체적인 기획 모습은 다음과 같습니다.

우선은 위 컨샙을 기반으로 하여 각각의 컴포넌트를 간단하게 만들어 봅니다.

 

 

첫 번째 컴포넌트 - Title.vue

(Title.vue)

<template>
    <header>
        <p>Unsplash API를 활용한</p>
        <h1>이미지 검색 앱</h1>
        <p>by Atelier BiCute</p>
    </header>
</template>

<script>
export default {
    name: 'TitleComponent'
}
</script>

 

 

두 번째 컴포넌트 - Form.vue

(Form.vue)

<template>
    <form>
        <input type="text" name="keyword" placeholder="영어로 입력해주세요 예)cat" />
        <button type="submit">Search</button>
    </form>
</template>

<script>
export default {
    name: 'FormComponent',
}
</script>

 

 

세 번째 컴포넌트 - Result.vue

(Result.vue)

<template>
    <div>  // <-- 전체를 감싸는 컨테이너
        <a>
            <img />  // <- 그 안에 링크 태그로 감싼 이미지만 반복할 예정 
        </a>
    </div>
</template>

<script>
export default {
    name: 'ResultComponent',
}
</script>

 

 

App.vue 

(App.vue)

<template>
  <Title />
  <Form />
  <Result />
</template>

<script>
import Title from '@/components/Title.vue'
import Form from '@/components/Form.vue'
import Result from '@/components/Result.vue'

export default {
  name: 'App',
  components: {
    Title,
    Form,
    Result,
  },
}
</script>

컴포넌트를 만들고 삽입하는 방법을 모르는 단계라면 Vue 새로운 컴포넌트를 생성하고 추가하는 방법을 우선 익히는 것을 추천드립니다

 

여기까지 진행하면 처음 계획대로의 모습이 출력됩니다.

이제 여기서부터 하나씩 기능을 구현해 갈 것입니다.

 

 

 

5.   input 값을  전달하기 

 

타이틀은 이미 역할을 다했으니 더 이상 건들 것이 없고,

Form.vue의 input에 입력한 값을 가져와서 Result.vue에 표시해주면 되는데,

둘 다 App.vue의 자식 요소이기에 App.vue에서 데이터를 받아서 자식 요소들에게 필요한 데이터를 전달해주는 형식으로 만들 것입니다.

 

 

(1) 작성한 input 값을 받아오자

data()에 keyWord라는 데이터를 담을 공간을 만들어준 후 이곳에는 input에 입력한 값이 들어가도록 v-model로 연결. 

잘 가져오는지 확인하기 위해 아무 곳에나 데이터 바인딩을 한 상태로 input 창에 글을 입력하면 동일한 내용이 출력되는 것을 확인할 수 있음.

(Form.vue)

<template>
    <form>
        <input v-model="keyWord"   // <-- 추가
        type="text" name="keyword" placeholder="영어로 입력해주세요 예)cat" />
        <button type="submit">Search</button>
    </form>
    {{ keyWord }}  // <-- 값을 잘 가져오는지 확인용 (확인후 삭제 또는 주석으로 제거)
</template>

<script>
export default {
    name: 'FormComponent',
    data() {
        return {
            keyWord: '',  // <-- 추가
        }
    },
}
</script>

 

 

(2) input 값을 부모에게 전달하자.

  input 창에 이벤트가 이벤트가 발생하면 "keyWordChange"라는 걸 부탁하는 코드를 작성. 여기에 2번째 인자로 keyWord값을 함께 넘겨 보내자.

(Form.vue)

<template>
  <form>
    <input @input="$emit('keyWordChange', keyWord)"  //<-- 부모에게 키워드를 담아 요청하는 코드 추가
    v-model="keyWord"
    type="text" name="keyword" placeholder="영어로 입력해주세요 예)cat" />
    <button type="submit">Search</button>
  </form>
</template>
(App.vue)

<template>
  <Title />
  <Form @keyWordChange="keyWordChange"/>  // <-- 자식이 keyWordChange라는 요청을 해오면 "keyWordChange"라는 함수를 실행 
  <Result />
</template>

<script>
...
  data(){
    return {
      keyWord: '',  // <-- 여기에도 keyWord값을 담기위한 변수 생성
    }
  },
  methods: {
    keyWordChange: 
      function(data) {
        this.keyWord = data  // <-- 그저 keyWord에 가져온 data를 넣어줄 뿐인 함수
      }
  }
};
...
</script>

 

    이제 부모에게도 데이터가 잘 넘어오는지 확인을 한번 해보자

(App.vue)

<template>
  <Title />
  <Form @keyWordChange="keyWordChange"/>
  <Result />
  {{ keyWord }}  // <-- 이전처럼 아무곳에나 데이터 바인딩을 해서 input 값이 잘 출력되는지 확인
</template>

 

 

 

6. Unsplash에 데이터 요청하기

 

  (1) 서버와의 통신을 위하여 axios 설치하기

내용을 입력하고 검색 버튼을 눌러 데이터를 요청하고 받아올 차례입니다.

서버와 통신을 하기 위해서 실행 중인 터미널을 종료하고 axios를 우선 설치합니다.

 

axios 설치

npm install axios

 

설치가 완료되었으면 axios를 사용하기 위해  App.vue의 <script> 상단에 axios를 import 합니다.

(App.vue)

import axios from 'axios'

 

unsplash에서 데이터를 검색하는 코드는 다음과 같은 형식입니다.

https://api.unsplash.com/search/photos?query=키워드&client_id=액세스키값

 

※ 액세스 키값은 처음 회원가입 후 New Application을 생성한 곳에서 확인할 수 있는 값을 넣어주면 됩니다.

 

 

자 이제 요청을 해봅시다. 

(Form.vue) 

<template>
  <form @submit="$emit('getPhoto')">  // <-- search를 실행하면 사진을 가져오는 함수를 실행해 달라고 부모에게 요청
    <input @input="$emit('keyWordChange', keyWord)" v-model="keyWord"
    type="text" name="keyword" placeholder="영어로 입력해주세요 예)cat" />
    <button type="submit">Search</button>
  </form>
</template>
(App.vue)

<template>
  <Title />
  <Form @keyWordChange="keyWordChange" @getPhoto="getPhoto" /> //<-- 추가
  <Result />
</template>

<script>
import axios from "axios"; 
import Title from "@/components/Title.vue";
import Form from "@/components/Form.vue";
import Result from "@/components/Result.vue";

export default {
  name: "App",
  components: {
    Title,
    Form,
    Result,
  },
  data() {
    return {
      keyWord: "",
    };
  },
  methods: {
    keyWordChange: function (data) {
      this.keyWord = data;
    },
    // ↓ 추가
  getPhoto: 
    function () {
      axios
        .get(
          `https://api.unsplash.com/search/photos?query=${this.keyWord}&client_id=클라이언트키값`
        )
        .then((response) => {
          console.log(response);
        });
    },
    // ↑ 추가
  },
};
</script>

axios 관련 간략 설명

  getPhoto 함수가 실행되면 데이터를 달라는 요청(.get)을 위 주소로 보내고, 

데이터를 가져오는 것에 성공했다면(.then) 파라미터에 값(response)을 담아서 오게 되는데 그것을 콘솔 출력해달라는 내용입니다.

*검색어는 ${this.keyWord}로 데이터 바인딩이 되어있지만 cliendt_id는 본인의 key값을 넣어줘야 작동합니다.

 

 

근데 뭔가 이상합니다.

검색 버튼을 누를 때마다 화면이 새로고침이 되어 모든 내용이 초기화되기 때문에 내용을 제대로 가져오는 건지 알 수가 없습니다.

새로 고침을 막기 위해 다음 코드를 추가해 줍시다.

(Form.vue)

<template>
  <form @submit.prevent="$emit('getPhoto')">  // <-- .prevent 추가
    <input @input="$emit('keyWordChange', keyWord)" v-model="keyWord"
    type="text" name="keyword" placeholder="영어로 입력해주세요 예)cat" />
    <button type="submit">Search</button>
  </form>
</template>

 

여기까지 제대로 진행되었다면 이제 검색 버튼을 누르거나 엔터를 입력하게 되면 콘솔에 받아온 데이터가 출력됩니다.

오브젝트 형식으로 되어있는데, 여기서 사용할 데이터는 data.results.urls(그중에서도 regular)와 data.results.alt_description입니다.

 

서버에서 받아온 결괏값을 담기 위해 data에 변수를 하나 만들어 담아 줍니다.

(App.vue)
  
  data() {
    return {
      keyWord: "",
      getPhotos: [],  // <-- 추가
    };
  },
  
  ...
  
   methods: {
    keyWordChange: function (data) {
      this.keyWord = data;
    },
    getPhoto: function () {
      axios
        .get(
          `https://api.unsplash.com/search/photos?query=${this.keyWord}&client_id=클라이언트키값
        )
        .then((response) => {
          this.getPhotos = response.data.results;  // <-- 가져온 데이터값을 getPhotos에 담기
        });
    },
  },

제대로 되는지 걱정된다면 항상 HTML 쪽에 {{ getPhotos }}처럼 데이터 바인딩을 해서 하나하나 확인해보시길 바랍니다.

오브젝트 형식의 텍스트가 잔뜩 출력된다면 정상입니다.

 

 

 

7. 받아온 데이터를 출력하기

 

이미지를 표시해주는 것은 Result 컴포넌트입니다.

이제 데이터를 가져오는데 까지는 성공했으니 해당 데이터를 전달해서 출력만 해주면 됩니다.

 

(App.vue)

<template>
  <Title />
  <Form @keyWordChange="keyWordChange" @getPhoto="getPhoto" />
  <Result :getPhotos="getPhotos"/>  // <-- getPhotos의 값을 Result 컴포넌트로 전달
</template>
(Result.vue)

<template>
  <div>
    <a v-for="(item, index) in getPhotos" :key="index" :href=item.links.html> // <-- (2) getPhotos의 개수만큼 반복
      <img :src=item.urls.regular :alt="item.alt_description" />  // <-- (3) 각 item에 원하는 데이터를 지정
    </a>
  </div>
</template>

<script>
export default {
  name: "ResultComponent",
  props: {
    getPhotos: Array,  // <-- (1) Props로 전달 받은 값의 타입 설정
  },
};
</script>

왜 이렇게 반복이 되는지 모르겠다면 Vue 반복문(v-for) 사용하는 방법을 참고!

반복되면서 각각의 배열을 item이란 이름으로 가져오게 되고, 그중 원하는 부분만 태그에 넣어주면 끝입니다.

 

이제 검색을 해보면 아래와 같이 이미지가 잘 출력되는 것을 볼 수 있습니다.

여기까지면 1차 구현은 끝입니다

이미지가 엄청 큰 것은 CSS 관련으로 하나도 손대지 않아서 그런 거니 이제 원하는 대로 스타일을 주면 됩니다. 

아래 내용은 취향껏 스타일이 끝났다면 그 후에 보도록 합시다.

 

그리드 형태로 이미지를 배치하 고나니 깔끔해 보이네요.

조금 더 이쁘게 배치하고 싶다면 masonry 레이아웃을 추천드립니다.

최종 화면

 

 

 

8. 수정 및 보완

 

  여기서부터는 개인 취향대로 아쉬운 것을 하나씩 수정해 봅시다.

 

첫 번째. 사진이 10개만 나오는데요?

  지금 상황에서 첫 번째로 아쉬운 점은 사진이 10개만 나온다는 것입니다.

  unsplash api에서 가져오는 사진의 기본값이 10으로 되어있어서 그런데 이거 자체를 바꾸기보단 더보기 식으로 바꿔볼까 합니다.

 

설명서를 읽어보니 다음과 같이 페이지 번호를 줄 수 있군요.

이 페이지 번호만 1, 2, 3처럼 변경해주면 10개씩 데이터가 들어오도록 되어있습니다.

https://api.unsplash.com/search/photos?page=페이지번호&query=검색어&client_id=클라이언트키값

 

  (1) 버튼을 만들고 버튼을 클릭하면 새로운 데이터를 가져와 기존 getPhotos에 새롭게 가져온 내용을 추가할 것입니다.

  (2) 페이지를 구분하기 위해 data에 pageNumber라는 값을 만들어 줍니다. 

  (3) 더 보기를 클릭하면 more 함수가 실행되며, more 함수는 pageNumber를 1 올린 후 해당 페이지의 정보를 가져오게 됩니다.

새롭게 가져온 정보(response)는 기존 정보(getPhotos)를 그대로 유지한 체 뒤쪽에 삽입하는 형태(push)로 저장합니다.

(App.vue)

<template>
  <Title />
  <Form @keyWordChange="keyWordChange" @getPhoto="getPhoto" />
  <Result :getPhotos="getPhotos" />
  <button @click="more">더보기</button>    // <-- 더보기 버튼을 만들어 줍니다. 클릭하면 more라는 함수 실행
</template>

<script>
  data() {
    return {
      keyWord: "",
      getPhotos: [],
      pageNumber: 1,  // <- PageNumber를 생성 = 기본값 1
    };
  },
  
  methods: {
    // ↓ 추가
    more: 
      function () {
        this.pageNumber += 1;
        axios
          .get(`https://api.unsplash.com/search/photos?page=${this.pageNumber}&query=${this.keyWord}&client_id=클라이언트키값`)
        .then((response) => {
          this.getPhotos.push(...response.data.results);
        });
    },
    // ↑ 추가
  },
  
</script>

 

 

두 번째. 클라이언트 아이디가 그대로 노출되는데 괜찮은가요?

 

자신만의 고유 키값인 클라이언트 아이디가 그대로 노출되고 있습니다.

이러한 환경변수들은 .env 파일을 생성하여 따로 빼두는 것이 좋습니다.

 

  (1) dotenv 설치

npm install dotenv

  ※ dotenv는 환경 변수를 .env 파일에 저장하여 process.env로 로드하는 의존성 모듈입니다.

 

  (2) 루트폴더에 .env 파일 생성

  vue-cli 3에서 dotenv를 사용하기 위해선 환경변수의 이름은 반드시 VUE_APP_로 시작해야 합니다.

VUE_APP_UNSPLASH_CLIENT_ID=클라이언트아이디

 

  (3) 이제 기존에 키값이 사용된 곳은 모두 환경변수로 변경해 주면 됩니다.

  ${process.env.VUE_APP_UNSPLASH_CLIENT_ID}로 변환해 줍시다.

(App.vue)

	getPhoto: 
      function () {
        axios
          .get(`https://api.unsplash.com/search/photos?query=${this.keyWord}&client_id=${process.env.VUE_APP_UNSPLASH_CLIENT_ID}`)
        .then((response) => {
          this.getPhotos = response.data.results;
        });
      },

 

 

세 번째, 더보기 버튼이 아닌 무한 스크롤 형식으로

  코드를 아래와 같이 추가해 보는 건 어떨까요...? 

  더 좋은 방법도 많을 거 같습니다. 

(app.vue)

<script>
  data() {
    return {
      keyWord: "",
      getPhotos: [],
      pageNumber: 1,
      flagTag: false,  // <-- 플래그 태그를 하나 만들어줍니다. (false일때만 작동하게 해서 연속 동작을 막아주는 용도로 사용합니다)
    };
  },
  // ↓ 추가
    mounted() {  
    const thumbnailContainer = document.querySelector("#app");
    window.addEventListener("scroll", () => {  // <-- 스크롤할때 이벤트 줄것이고
      if (this.flagTag == false) {  // <-- flagTag가 false일때만 실행
        this.flagTag = true;  // <-- flagTag값을 true로 바꿔줌. 이제 중복 실행 되더라도 false가 아니기에 내부 코드가 실행되지 않게 해줌
        setTimeout(() => {  // <-- 살짝 텀을 주고
          if (
            thumbnailContainer.getBoundingClientRect().bottom <
            window.innerHeight * 1.3  // <-- 컨테이너의 아래쪽 값을 계산해서 스크롤이 어느정도 내려오면 내부 코드 실행
          ) {
            this.more();  // <-- 내부 코드는 위에서 만들어봤던 더보기 클릭했을때랑 똑같음. 페이지 번호 추가해서 데이터 불러오기
          }
          this.flagTag = false;  // <-- 할일 다했으면 다시 flagTag를 false로 바꿔줌.
        }, 300);
      }
    });
  },
  // ↑ 추가
</script>

 

 

 

반응형