[VUE] VUE 배우기

안지원·2025년 1월 2일

vue

목록 보기
1/1
post-thumbnail

vue.JS

  • 리액트는 타입스크립트 기반으로 공부했다.
  • 그와 반대로 vue는 자바스크립트 기반으로 공부한다.
  • 회사에서 vue를 쓰는 경우는 쉽고 빠르게 웹 애플리케이션을 만드는 경우다.(SI에서 주로 사용하는듯?)
  • 리액트를 공부하다 vue를 공부하면 매우 쉽게 느껴질 수 있다.

vue의 탄생 배경

  • 탄생 배경 자체도 쉽게 쓰기 위해서다.
  • 중국의 구글 개발자 에반유(Evan Yoo)라는 사람이 개발했다.
  • Angular.js(프론트엔드 고인물 프레임워크)의 단점을 완화하기 위한 사이드 프로젝트를 진행하다 구글을 떠나고 본격적으로 개발하기 시작했다.

vue.JS 공부하는 법

  • 공식문서가 굉장히 잘 되어있어서 이걸로 공부하면 된다.(100% 한글화 + 번역도 잘 되어있음)

vue와 리액트의 차이점

  • 리액트는 라이브러리의 성격이 강하지만 vue는 완전한 프레임워크의 개념이다.
  • 따라서 vue는 운영방식이 거의 정해져있어 리액트에 비해 자유도가 떨어진다.
  • 하지만 그만큼 프레임워크를 통제하기 쉽다.

프로그레시브 자바스크립트 프레임워크

  • 이 vue를 프로그레시브 자바스크립트 프레임워크 라고 부르기도 한다.
  • 프로그레시브 프레임워크란 이미 다른 사양으로 개발된 웹 애플리케이션에서 일부분만 점진적으로 적용할 수 있도록 모듈화 하고 유연한 구조를 갖춘 프레임워크를 의미한다.

MVVM

  • 대부분 오픈소스 프레임워크는 모두 특정 소프트웨어 아키텍쳐 패턴에 기반을 두고 있다.
  • vue가 속한 패턴이 MVVM이다.
    MVVM은 Model-View-ViewModel 라는 모델로 ViewModel이라는 개층이 View와 Model의 중간다리를(?) 해서 직접적인 연관을 줄여 view와 model의 상호 의존성을 줄여줄 수 있다.

반응형 데이터 바인딩

  • 아무튼 우리는 2016년도 2.n버전을 다운받을 거다.
    -vueJS로 받은 파일은 배포를 하기 위해 2.0rm 을 주로 배우는데 개발자들에게 굳혀녔기 때문? 이다.

양방향 바인딩

  • vue는 데이터를 정의하고 그 데이터를 nud
  • 리액트는 리액트 문법을 수정하려면 ViewModel이 없기 때문에 단방향으로 한번에 md해야 한다.

vue 사용하기

  • vite로 vue와 js프로젝트를 create한다.
  • 처음에 app.vue파일을 열어보면 글씨가 흰색인걸 볼 수 있는데 익스텐션에서 Vue official이란 익스텐션을 설치해야 글씨가 구분이 가게 바뀐다.(vue팀에서 공식으로 설치하라고 하는 익스텐션임)

뷰를 사용할 수 있는 방법

  1. cdn
  2. vite
  3. create vue - 가장 많이 쓰는 방법
  • 참고로 vite, create vue 둘 다 vite를 통해 실행되는것임

뷰 설치 후 해야할 것

  • Vue official 익스텐션 설치해서 vue 코드에 색깔 넣기(원래는 흰색 코드밖에 없어서 코드 분간이 힘듬)

뷰가 웹브라우저에 보이는 과정

  • index.html에서 main.js의 스크립트 파일을 가져옴
  • main.js파일은 index.html의 #app이라는 요소에 mount 해준다.
  • mount 해주는 App.vue에서 요소를 만든 것에 따라 index.html에서 mount 되는 결과물이 달라진다.

뷰 보일러 플레이트 중 필요없는 것들

  • assets폴더, components폴더 => main.js의 main.css import문 삭제
  • app.vue파일에 모든 내용 삭제

원시 뷰 랜더링 방식

  • 다음과 같다.
import { createApp, h } from "vue";

const app = createApp({
  render() {
    return h("div", h("h1", "Hello vue3"));
  },
});

app.mount("#app");
  • 이렇게 리액트의 원시 랜더링 방식과 비슷하게 생겼는데 뷰도 다음과 같이 간략화 할 수 있다.
import { createApp } from "vue";
import App from "./App.vue";

createApp(App).mount("#app");

Vue VSCode Snippets

  • 리액트의 rfc 처럼 뷰의 기본 컴포넌트 구조를 생성해주는 익스텐션
  • 뷰의 모든 버전별 기본 컴포넌트 구조를 생성할 수 있다.
  • vbase 명령어를 자주 사용하게 될 것이다.(최신 구조인듯)

sfc

  • single file component라는 뷰에서만 사용하는 독자적인 문법 작성 시스템임
  • 확장자가 vue로 끝나는 문법의 작성 방식을 뜻하며 크게 script, templete, style로 구조가 구분되어 사용한다.
  • App.vue 안에서 HTML, CSS, JS를 전부 작성할 수 있게 해주는 작성 방식이다.
  • 반드시 templete태그가 존재해야 하며, template, script, style 태그들은 각각 1개만 존재해야 한다.
  • 뷰는 리액트처럼 HTML의 속성을 바꿔서 사용하지 않는다. HTML의 속성을 그대로 사용할 수 있다.(퍼블리셔와의 협업에서도 유용함)
  • 참고로 sfc에서는 script, style태그가 어느 위치에 작성되든 정상적으로 적용이 된다.(컴파일러 언어인가?)

뷰의 버전별 API

  • 뷰는 버전 업데이트가 느린 프레임워크다
  • 이전 버전의 기능을 사용해도 호환이 잘 되어 구버전의 기능을 사용하는 경우가 많다.
  • 따라서 버전이 업데이트 되어 다른 기술이 나온다고 해도 구버전을 완전히 놓아선 안된다.

옵션스 API

  • vue2에서 사용하던 방식
  • export default{} 가 기본형태
<script>
  export default {};
</script>

1. name

  • 안에 name이라는 속성을 넣을 수 있는데 이 속성은 컴포넌트의 이름과 같게 적어줄 수 있다.
  • name 속성을 만드는 이유는 후에 디버깅할때 도움이 되기 때문이다.
export default {
  name: "App",
};

2. data

  • data라는 속성은 뷰에서 사용할 데이터를 정의하는 속성이다.
  • 뷰에서 사용하는 모든 데이터는 무조건 data속성으로만 정의한다.
export default {
  data: function(){
    return {
      message : "Hello, World!"
    }
  }
};
  • 단축문법으로 다음과 같이 표현할 수도 있다.
export default {
  data(){
    return {
      message : "Hello, World!"
    }
  }
};
  • data에서 내보낸 return값은 templete태그에서 사용 가능하다.
  • templete태그 안에서 바인딩할 공간에 중괄호 두 개를 넣어 사용 가능하다.
<template>
  <h1>{{ message }}</h1>
</template>
  • 이 두 개의 중괄호를 머스타치 신텍스라고 부른다.(콧수염 문법)
  • 이 콧수염 문법 내에선 데이터 이외에도 표현식, 연산식을 넣을 수도 있다 - 리액트와 동일하다 보면 된다.
<template>
  <h1>{{ message }}</h1>
  <h2>{{ 1 + 2 }}</h2>
  <h3>{{ message.toUpperCase() }}</h3>
</template>
  • 그런데 뷰에서 표현식, 연산식 등을 사용하여 데이터 바인딩을 하는 행위는 지양한다. => 되도록 data속성에서 전부 처리해서 바인딩 되는 공간엔 값만 들어갈 수 있게 하는걸 권장한다.

3. 객체, 배열

  • 뷰는 리액트와 다르게 객체와 배열도 있는 그대로 보여준다.
  • 리액트는 배열을 벗겨서 나타내고, 객체는 json형식일 경우 출력이 불가능한데 뷰는 값을 그대로 출력시켜준다.

4. 디렉티브(v-[])

  • 뷰에서만 사용할 수 있는 특별한 속성이 있다.
  • v-로 시작하는 속성들이 있는데 이 속성들을 디렉티브 속성이라 한다.
  • 이 디렉티브 속성은 뷰에서 사용하는 핵심적인 렌더링 매개체?다.
  • 웬만한 스크립트 코드 대신 이걸 사용하는거 같다(아마)
    1. v-html, v-text
  • 콧수염 문법을 사용하지 않고 해당 속성을 사용하여 데이터 바인딩을 할 수 있다.
<template>
  <h1>{{ message }}</h1>		// Hello, World!
  <div v-html="message"></div>  // Hello, World!
</template>
  • v-html과 유사한 역할을 한다.
<template>
  <h1>{{ message }}</h1>		// Hello, World!
  <div v-html="message"></div>  // Hello, World!
  <div v-text="message"></div>  // Hello, World!
</template>
  • 이 두 가지 속성을 사용하는 이유는 각자의 역할이 있기 때문이다.
    콧수염 신텍스 : 데이터를 있는 그대로 출력해준다 + 표현식 등으로 자유도가 높다
    v-html : 태그를 인식해서 데이터를 출력해준다
    v-text : 데이터를 있는 그대로 출력해준다 + 딱 고정된 값만 제공해준다.
  • 추가로 v-html, v-text 속성이 들어간 태그는 요소의 컨텐츠를 넣을 수 없게 된다.
  <div v-html="message">ddd</div>  // 에러
  <div v-text="message">ddd</div>  // 에러

2. v-pre

  • v-pre 속성은 컴파일을 건너뛰게 하는 속성이다.
  <div v-pre>{{ name }}</div>  // {{ name }} 그대로 출력
  • v-pre가 지정되어 있다면 컴파일 과정중에서 해당 부분을 컴파일 대상에서 제외하기 때문에 적용되지 않는다.
  • v-pre 안에 들어있는 요소들 까지 전부 적용해준다.
<div v-pre>
  <p>{{ name }}</p>
  <p>{{ name }}</p>
</div>  // {{ name }} {{ name }}
  • 컴파일 할 필요없는 파일이나 영역에 한정해 v-pre를 걸어주면 성능 향상에 도움을 줄 수 있다.

3. v-bind , :

  • 데이터를 속성에 바인딩 할 때 사용하는 속성이다.
  • 속성이라면 모두 다 사용 가능하다.
export default {
  data: function () {
    return {
      id: "main",
    };
  },
};
</script>
<template>
  <main v-bind:id="id"></main>	// id가 data에서 지정한 값이 바인딩된다.
</template>
  • v-bind는 실무에서 굉장히 많이 사용되는 속성이다.
  • 그래서 축약속성까지 존재한다.
  • v-bind 대신 콜론 : 을 사용하여 축약할 수 있다.
</script>
<template>
  <main :id="id"></main>	// 콜론만 입력해서 id값으로써 데이터바인딩이 가능하게 된다.(class, style, href 등등 다 가능)
</template>
  • 참고로 3.4 이후 버전에선 더 줄일 수도 있다.
export default {
  data: function () {
    return {
      id: "main",
      class : "sub"
    };
  },
};
</script>
<template>
  <main :id></main>	// data의 속성과 동일하면 속성값을 사용할 필요 없이 바인딩 가능하다.
  <div :class></div>
</template>
  • 삼항 연산자 등을 활용해 2가지 클래스명을 사용할 수도 있다.
  • &&을 통해 on,off로 클래스를 넣을 수도 있다.
export default {
  data: function () {
    return {
      isActive : false
    };
  },
};
</script>
<template>
  <div :class="isActive ? "show" : "hide">등장</div>
  <div :class="isActive && "show">등장2</div>
</template>
  • img 태그의 주소를 넣는 등의 용도로도 활용할 수 있다.
  • vsrc emmet으로도 바로 v-src가 담긴 img태그를 열 수 있다.
  • src주소를 데이터바인딩 할 때 사용한다.
export default {
  name: "App",
  data() {
    return {
      imageSrc:
        "https://cdn.pixabay.com/photo/2021/11/26/20/45/lantern-6826697_1280.jpg",
    };
  },
};
</script>

<template>
  <img :src="imageSrc" alt="altText" />
</template>
  • 객체로 만들어서 한번에 style을 부여할 수도 있다.
<script>
export default {
  name: "App",
  data() {
    return {
      styledObject: {
        color: "red",
        fontSize: "30px",
      },
    };
  },
};
</script>

<template>
  <p :style="styledObject">asd</p>
</template>
  • 여러개의 속성을 한 번에 바인딩 하고 싶으면 객체형태로 속성들을 만든 뒤에 v-bind 속성을 직접 사용해 넣어도 된다.
<script>
export default {
  name: "App",
  data() {
    return {
      binding: {
        id: "main",
        style: { color: "red", fontWeight: "bold" },
      },
    };
  },
};
</script>

<template>
  <p v-bind="binding">가나다</p>
</template>

5. v-if(v-else-if, v-else), v-show

  • 조건문 마저도 디렉티브 속성을 사용해 적용할 수 있다.
  • 속성값이 true, false임에 따라 요소의 렌더링/비렌더링이 결정된다.
  • v-show도 비슷한 역할을 수행한다.
<template>
  <p v-if="false">가나다</p>	// 요소의 렌더링을 막음
  <p v-if="true">마바사</p>
  <p v-show="false">가나다</p>	// 요소는 존재하고 display:none 처리
  <p v-show="true">마바사</p>
</template>
  • 이 둘의 차이점이 있다면 v-show는 css로 display:none 처리를 하지만 v-if는 렌더링 자체를 막는 차이점이 있다.
  • 화면에 자주 등장하고 사라지는 요소는 v-show로 처리하고 자주 등장하지 않는 요소에 대해선 v-if로 처리하는걸 권장한다.
  • boolean 값이 나오는 모든 표현식, data 속성을 사용 가능하다.
<script>
export default {
  name: "App",
  data() {
    return {
      isActive: true
    };
  },
};
</script>
<template>
  <p v-if="isActive">가나다</p>
  <p v-if="1 >= 0">가나다</p>
</template>
  • if문은 v-else-if, v-else문을 통해 여러개의 요소중 조건을 가장 먼저 충족한 하나의 요소만 렌더링 하게 할 수 있다.
<template>
  <p v-if="false">가나다</p>
  <p v-else-if="false">가나다</p>
  <p v-else-if="true">가나다</p>	// 이 요소만 렌더링
  <p v-else-if="true">가나다</p>
  <p v-else>가나다</p>		// 만약 전부 false였다면 해당 요소 렌더링
</template>

6. v-cloak

  • 렌더링 되는 순간에 삭제되는 요소에 대해 스타일링 할 수 있는 속성
  • style태그와 연계해서 사용할 수 있다.
  • cdn방식에서 사용할 수 있는데 잘 사용하지 않는 속성

7. v-for

  • 반복하고 싶은 태그에 사용하면 그만큼 태그를 반복해서 만들어준다.
  • 자바스크립트의 for in문과 유사하다.
<script>
import Success from "./2-1/Success.vue";
import Loading from "./2-1/Loading.vue";
import Idle from "./2-1/Idle.vue";
import Error from "./2-1/Error.vue";

export default {
  data() {
    return {
      arr: [1, 2, 3],
    };
  },
};
</script>
<ul>
    <li v-for="(value, index) in arr" key="index">{{ value }}</li>
</ul>
  • 첫번째엔 대상값, 두번째엔 인덱스값으로 파라미터를 받아 반복문을 사용할 수 있다.

  • v-for 반복문에서 추가로 filter, map 등의 함수를 사용해 반복문 돌릴 대상을 특정하는 로직을 추가하는 등의 방법을 사용해도 된다.

<script>
export default {
  data() {
    return {
      itemsObj: [
        { id: 1, name: "james" },
        { id: 2, name: "smith" },
      ],
    };
  },
};
</script>

<ul>
    <li
      v-for="(value, index) in itemsObj.filter((e) => e.name === 'james')"
      :key="index"
    >
      {{ value.id }}-{{ value.name }}
    </li>
</ul>
  • 심지어 v-for에선 객체도 반복문이 사용 가능하다.
  • 객체의 속성-속성값을 하나로 취급해 (속성값, 속성) in Obj 의 형태로 사용 가능하다.
<script>
export default {
  data() {
    return {
      obj: { id: 2, name: "smith" },
    };
  },
};
</script>

<ul>
  <li v-for="(value, key) in obj" :key="key">{{ value }}-{{ key }}</li>
</ul>
  • 복잡한 형식의 배열-객체 형태라도 다중 v-for을 활용해서 요소를 만들 수 있다.
<script>
export default {
  data() {
    return {
      categorys: [
        { name: "Fruits", items: ["Apple", "Banana", "Cherry"] },
        { name: "Vegetables", items: ["Carrot", "Tomato", "Lettuce"] },
      ],
    };
  },
};
</script>

<template>
  <ul>
    <li v-for="(category, index) in categorys" :key="index">
      <h3>{{ category.name }}</h3>
      <ul>
        <li v-for="item in category.items">
          {{ item }}
        </li>
      </ul>
    </li>
  </ul>
</template>
  • 만약 반복문을 사용하는데 if문으로 특정 반복문을 제거하고 싶다면 반복문을 진행할때 template태그를 사용해서 반복문을 진행하고 그 안에 요소에 if문을 걸면 된다. template는 요소가 표시가 안되기 때문에 반복문으로 사용해도 큰 지장은 없을듯?
<script>
export default {
  data() {
    return {
      categorys: [
        { name: "Fruits", items: ["Apple", "Banana", "Cherry"] },
        { name: "Vegetables", items: ["Carrot", "Tomato", "Lettuce"] },
      ],
    };
  },
};
</script>

<template>
  <ul>
    <li v-for="(category, index) in categorys" :key="index">
      <h3>{{ category.name }}</h3>
      <ul>
        <template v-for="item in category.items">
          <li v-if="item.includes('e')">
            {{ item }}
          </li>
        </template>
      </ul>
    </li>
  </ul>
</template>
  • 참고로 v-for + v-if 문 등의 특수한 캐이스에서 사용하는 template태그 이외에 template태그를 사용하는 것은 vue의 의도와는 맞지 않기 때문에 이런 특수한 캐이스에만 사용해야 한다.

복잡한 배열 예시

<script>
export default {
  data() {
    return {
      categorys: [
        {
          id: 1,
          name: "전자제품",
          products: [
            {
              id: 1,
              name: "스마트폰",
              isAvailable: true,
              options: [
                { id: 1, name: "블랙", inStock: true },
                { id: 2, name: "화이트", inStock: false },
              ],
            },
          ],
        },
      ],
    };
  },
};
</script>
<template>
  <div>
    <template v-for="(category, index) in categorys" :key="category.id">
      <h2>{{ category.name }}</h2>
      <template v-for="(product, index) in category.products" :key="product.id">
        <h3>{{ product.name }}</h3>
        <template v-for="(option, index) in product.options" :key="option.id">
          <h4 v-if="option.inStock">{{ option.name }}</h4>
        </template>
      </template>
    </template>
  </div>
</template>
  • 웬만하면 이런 상황은 없긴 한데 혹시나 이런 데이터를 받는다면 이렇게 처리해야 한다.

8. v-model

  • vue에서 script태그에서 작성한 html요소와 template태그에서 작성한 html요소를 양방향 업데이트 하기 위해 사용하는 디렉티브다.
  • 폼 요소에만 사용되는 디렉티브다.
<script>
export default {
  data() {
    return {
      input: "",
    };
  },
};
</script>

<template>
  <input type="text" v-model="input" />
  <h1>{{ input }}</h1>
</template>
  • 요 기능 좋네
  • 어떤 요소라 해도 내가 그 입력값을 가져오고 싶다면 v-model을 사용해서 연결하면 된다.
  • 한 가지 주의해야 할 점은 input 이벤트 리스너와 함께 사용하면 의미가 중복되는데 이로 인해 오류가 발생할 수 있다고 한다. 그래서 둘 중 하나만 적용해야한다.
  • 이 v-model은 다양한 데이터 정보 저장에 아주 간단하게 사용될 수 있다.
<script>
export default {
  data() {
    return {
      uid: "",
      upw: "",
      desc: "",
      chk: false,
      fruits: ["apple"],
      gender: "male",
    };
  },
};
</script>

<template>
  <form action="">
    <label for="uid">
      <span>아이디</span>
      <input type="text" id="uid" v-model="uid" />
    </label>
    <label for="upw">
      <span>비밀번호</span>
      <input type="password" id="upw" v-model="upw" />
    </label>
    <label for="desc">
      <span>상세내용</span>
      <textarea id="desc" v-model="desc" />
    </label>
    <label for="remember">
      <span>check</span>
      <input type="checkbox" id="remember" v-model="chk" />
    </label>
    <div>
      <label>
        <span>apple</span>
        <input type="checkbox" v-model="fruits" value="apple" />
      </label>
      <label>
        <span>banana</span>
        <input type="checkbox" v-model="fruits" value="banana" />
      </label>
      <label>
        <span>orange</span>
        <input type="checkbox" v-model="fruits" value="orange" />
      </label>
    </div>
    <label
      ><input
        type="radio"
        name="gender"
        value="male"
        v-model="gender"
      />male</label
    >
    <label
      ><input
        type="radio"
        name="gender"
        value="female"
        v-model="gender"
      />female</label
    >
  </form>
</template>
  • MVVM을 다시 생각해보면 v-model이 view모델과 데이터 사이에서 둘을 양방향으로 바인딩해주는 역할을 한다고 보면된다.
  • 그런데 v-model을 사용할때 input같은 입력값에 한글을 입력하면 데이터 반영이 안된다.
  • 입력한 뒤에 다른 곳에 포커스를 두거나 엔터를 누르거나 다른언어(영어 등)을 입력해야 그제서야 반영된다.
  • 한글이 조합문자이기 때문에 발생하는 오류다.
  • 그래서 한글을 사용하려면 v-model대신 이벤트를 사용해서 해결해야 한다.

vue에서 스타일 부여하기

  • vue에서 스타일을 부여하는 방법으로 sfc구조의 style태그를 사용하는 방법이 가장 일반적이다.

  • 사용하는 방법은 기존 css와 동일하게 하면 되는듯 하다

  • 그런데 여기서 style에 scoped속성을 부여하냐, 그냥 사용하냐에 따라 달라진다.

  • scoped속성을 추가하면 해당 컴포넌트 내에서만 스타일의 적용을 받을 수 있게 된다.

  • 전역 스타일이 되냐 지역 스타일이 되냐를 분류할 수 있다.

  • 뷰는 실무에서 style태그를 활용해서 css를 부여한다.

  • css로 스타일을 만드는 방법도 있다.

  • scc파일을 만든 후에 style태그에 @import로 불러오면 된다.

  • vue파일이 길어지는게 보기 싫으면 이렇게 css 따로 모듈화 해서 사용하면 된다.

  • vue에서 css가 담긴 style폴더는 보통 assets폴더에 보관한다.

  • 그 이유는 viteConfig의 경로 alias 기능을 사용할 수 있기 때문이다(?)

  • assets에 있는 폴더는 viteConfig에서 import할 경로의 맨 앞을 ~나 @등 원하는 기호로 정할 수 있다(절대경로 커스텀 가능한게 좋다는건가?)

  • 해외의 대부분 개발자는 ~를 주로 사용한다고 한다.

  • 자바스크립트의 import를 사용하는 방법도 있다

  • main.js에서 css파일을 import해서 전역으로 사용하는 방법이 있다.

  • 태그에 인라인으로 넣는 방법도 있다.(이거 써도 돼?)

  • 어짜피 컴포넌트 안에 스타일 스크립트 다 들어가서 상관없는가?

  • data에 style들을 만들어서 인라인에 넣어 한번에 적용할 수도 있다.

  • style들을 여러개 만들어서 style="[]" 로 안에 style들을 배열처럼 집어넣으면 한번에 적용이 된다.

class바인딩

  • class에 무언갈 바인딩할 때 class="{클래스명: true, 클래스명: false}" 을 통해 class를 키고 끌 수 있다.(boolean값에 따라 등장되거나 삭제된다.)

뷰에서 부트스트렙 사용하기

  • 뷰에서도 테일윈드가 사용 가능하다.
  • 하지만 이번엔 부트스트렙을 사용해볼거다
  • 우선 bootstrap을 설치해본다.
npm install bootstrap
  • main.js에 기초 import를 해주면 끝이다.
import "bootstrap";
import "bootstrap/dist/css/bootstrap.css";
  • 이제 테일윈드를 사용하는 것 처럼 부트스트렙의 doc을 참고해서 스타일을 넣을 수 있다.
  • 부트스트렙은 100프로 유틸리티퍼스트 라이브러리가 아니다.
  • css의 완전한 대체로 사용되는것 보단 괜찮은 ui들을 가져다 사용하는 용도로 사용된다.

뷰에서 테일윈드 사용하기

  • 이제 테일윈드를 사용해본다.
  • 테일윈드 설치는 vite형식으로 똑같이 설치하면 된다.
  • 단, tailwind.config.js에 content에 복붙하는 확장자에 vue 확장자명을 추가하면 된다.
export default {
  content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx,vue}"],
  theme: {
    extend: {},
  },
  plugins: [],
};

vue에서 scss 적용하기

  • scss도 적용해보자
  • npm install sass-loader sass --save-dev 관련 라이브러리들을 설치해준다.
  • 이제 style태그에 lang="scss" 속성을 집어넣으면 scss가 적용된다.
<style lang="scss">
div {
  h2 {
    color : orange
  }
}
</style>

vue에서 styled components 적용하기

  • styled components도 사용할 수 있다.
  • vue styled component 사이트에 들어가서 get started에 있는 설치명령어를 붙여넣으면 된다.
npm i @vue-styled-components/core
  • 이제 styled component를 사용하던 방식대로 똑같이 컴포넌트를 만들어 사용하면 된다.
  • 리액트의 styled component와 100프로 일치한다.
  • 리액트에서 styled component를 사용해봤다면 뷰에선 이걸 사용하는걸 추천한다.
<script setup lang="ts">
import { styled } from '@vue-styled-components/core'

const StyledDiv = styled.div`
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 20px;
  border-radius: 9999px;
  background-color: #4c5a6d;
  font-size: 20px;
  font-weight: bold;
  color: #fff;
`
</script>
<template>
  <StyledDiv>Hello World!</StyledDiv>
</template>

컴포지션 API

  • vue3에서 사용하는 방식

참고. docker

  • docker : 개발 버전 등의 기록을 래핑한 환경
  • 쿠버네티스? : docker의 보조로써 개발할때 해당 버전을 관리하기 쉽게 해주는것

스케폴딩(초기 보일러플레이트 같은거 말하는듯?)

  • 프로그래밍에서 새로운 프로젝트나 모듈을 시작할 때, 초기 구조와 설정을 자동으로 생성해주는 과정이나 도구

pakage파일의 scripts를 잘 봐야하는 이유

  • npm run dev 같은 실행 명령어가 회사마다 그 명령어를 다르게 설정해 놓을 수 있기 때문임

pakage 미세먼지 팁

  • pakage에 private:true가 되어있는 파일들은 npm사이트에 올릴 수 없다.(패키지에 대한 정보를 npm에 공개하지 않겠다는 명령어임)
  • devDependencies는 개발과정에서 참조할 패키지를 의미하고 dependencies는 배포과정에서 참조할 패키지를 의미함. 하지만 요즘은 웹팩, vite 등의 번들러 툴이 기능이 좋아져서 개발에 사용되는 패키지, 배포에 사용되는 패키지의 구분을 알아서 잘 구분해줘서 편의성이 좋아지긴 했어도 잘 구분해서 넣는걸 권장함(전체적인 애플리케이션의 용량을 줄이기 위해

vue의 이벤트

vue에서 메서드 함수

  • vue에선 메서드로 사용할 함수를 옵션스 API안에 methods: {} 라는 기능을 만들어 그 안에 함수를 만들어서 사용하면 된다.
<script>
export default {
  name: "App",
  data() {
    return {
      count: 0,
    };
  },
  methods: {
    decrement: function () {
      console.log("decrement");
    },
    increment: function () {
      console.log("increment");
    },
    reset: function () {
      console.log("reset");
    },
  },
};
</script>
  • 이 메서드가 data에 있는 함수를 참조하려면 this 키워드를 사용하면 된다.
<script>
export default {
  name: "App",
  data() {
    return {
      count: 0,
    };
  },
  methods: {
    decrement: function () {
      this.count -= 1;
    },
    increment: function () {
      this.count += 1;
    },
    reset: function () {
      this.count = 0;
    },
  },
};
</script>
  • 리액트의 경우 이런 객체타입의 데이터는 불변성 유지를 위해 spread연산자 또는 immer라이브러리 등을 사용해야 하지만 vue에서는 자동으로 불변성을 관리해줘서 마치 변수의 값을 저렇게 직접접근 + 재할당 해도 된다.
  • 심지어 methods 내부에서 만들어진 함수끼리 참조가 가능하다.
increment: function () {
  this.increment()
  this.count += 1;
},
  • 그리고 함수 축약도 가능하다.
methods: {
    decrement() {
      this.count -= 1;
    },
    increment() {
      this.count += 1;
    },
    reset() {
      if (this.count === 0) return;
      this.count = 0;
    },
  },

vue의 이벤트리스너

  • 이제 만든 메서드를 이벤트 핸들러 함수로 넣을 수 있다.
  • vue에서의 이벤트 핸들러는 v-on: 을 사용한다.
<template>
  <h1>{{ count }}</h1>
  <button v-on:click="increment">증가</button>
  <button v-on:click="reset">리셋</button>
  <button v-on:click="decrement">감소</button>
</template>
  • 이 v-on: 이벤트 핸들러는 축약표현을 제공하는데 바로 @다.
<template>
  <h1>{{ count }}</h1>
  <button @:click="increment">증가</button>
  <button @:click="reset">리셋</button>
  <button @:click="decrement">감소</button>
</template>
  • 함수를 넣을 필요 없이 직접 표현식을 넣어도 된다.
<template>
  <h1>{{ count }}</h1>
  <button @:click="increment">증가</button>
  <button @:click="reset">리셋</button>
  <button @:click="decrement">감소</button>
  <button @:click="count -= 2">2감소</button>
</template>
  • 함수 사용시 이벤트 객체를 사용하고 싶다면 첫 번째 인자를 만들고 이벤트 핸들러에 넣는 함수 인자에 $event를 넣으면 그 이벤트 객체가 전달이 된다.
methods: {
    decrement(event) {
      console.log(event)
      this.count -= 1;
    },
    increment() {
      this.count += 1;
    },
    reset() {
      if (this.count === 0) return;
      this.count = 0;
    },
  },

  <button @:click="decrement($event)">감소</button>

수식어

  • vue에만 존재하는 기능인 수식어가 있다.
  • 이벤트에 보조적인 의미를 부여해주는 것들이다.
  • @이벤트타입.수식어 이렇게 사용 가능하다.
  • 이 수식어는 보아하니 e.preventDefault 같은 기능을 대신하는거 같다.
  • 공식문서에 자세히 나와있다.

once

  • 이벤트가 한번만 발생하게 된다.
  • 새로고침 하기 전 까지 다시 이벤트를 반복할 수 없다.
  <button @:click.once="increment">증가</button>

prevent

  • 기본 이벤트를 취소하게 만든다.
  • a태그, form 전송 등의 기능을 제어할 때 사용해서 vue에서 제일 많이 사용되는 수식어다.
<script>
export default {
  name: "App",
  data() {
    return {
      count: 0,
      email: "",
    };
  },
  methods: {
    decrement(event) {
      console.log(event);
      this.count -= 1;
    },
    increment() {
      this.count += 1;
    },
    reset() {
      if (this.count === 0) return;
      this.count = 0;
    },
    consoleEmail() {
      console.log(this.email);
    },
  },
};
</script>

<template>
  <a href="https://www.naver.com" @click.prevent></a>
  <form @click.prevent="consoleEmail">
    <input type="text" v-model="email" />
    <button type="submit">제출</button>
  </form>
</template>

left, right, middle

  • 마우스의 버튼 기능을 제어할 수 있다.

enter, up, space

  • keyup 이벤트에 사용되며, 해당 이벤트가 특정 키를 입력해야지 발생하는 이벤트로 변경된다.
  • 이벤트 객체를 사용할 필요 없이 간편하게 엔터키 클릭 이벤트를 구현할 수 있다.
  • 이 3가지 외에도 여러개가 있다.(esc, down, tab 등등..)
    <input type="text" @keyup.enter="consoleEmail" v-model="email" />
  • 추가로 버튼기능을 순서대로 나열하면 버튼을 조합해서 사용할 수 있다.
    <input type="text" @keyup.ctrl.enter="consoleEmail" v-model="email" />
     // crtl + enter를 입력해야 발동

exact

  • 이벤트나 이벤트 수식어를 정확히 입력하게 강제할때 사용
<input type="text" @keyup.enter.exact="consoleEmail" v-model="email" />	// 반드시 enter를 입력할때만 이벤트 발동

passive

  • 탭이나 휠 사용시 성능향상에 도움이 된다고 하는데 잘 안쓰는듯

capture, buble

  • 이벤트 캡쳐링, 버블링을 막는데 사용

조합문자 양방향 업데이트 하기

  • 조합문자는 v-model을 사용하여 양방향 업데이트를 하면 조합문자의 특성 때문에 끝 글자가 포커스를 하지 않는 한 마무리 지어지지 않는다.
  • 그래서 조합문자는 리액트 처럼 원시적인 방법을 사용해야 한다.
<script>
export default {
  name: "App",
  data() {
    return {
      query: "",
    };
  },
  methods: {
    handleInput($event) {
      this.query = $event.target.value;
    },
  },
};
</script>

<input type="text" :value="query" @input="handleInput($event)" />
  {{ query }}
  • 그런데 이렇게 하는 방식이 페이지에 렌더링되는 입력값이 실시간으로 변하는걸 체크할 때만 사용하지, 입력한 데이터를 어디로 전송하는 과정엔 포커스가 무조건 발생하기 때문에 업데이트된 데이터가 이동하게 된다. 따라서 실시간으로 유저에게 입력한 값을 보여줘야 하는 상황이 아닌 경우엔 v-model을 활용해도 무관하다.

v-memo

  • 메모이제이션 기능을 사용하는 디렉티브다.
<script>
export default {
  name: "App",
  data() {
    return {
      name: "철수",
      age: 20,
      gender: "male",
    };
  },
  methods: {},
};
</script>

<template>
  <div v-memo="[name]">
    <h2>{{ name }}</h2>
    <h2>{{ age }}</h2>
    <h2>{{ gender }}</h2>
  </div>
  <button @click="name = 'james'">이름 변경</button>
  <button @click="age = 50">나이 변경</button>
  <button @click="gender = 'female'">성별 변경</button>
</template>
  • 이렇게 v-memo=[] 안에 타겟 변수를 넣으면 해당 변수의 상태가 바뀌어야 재랜더링이 발생하게 된다.
  • 실제로 이렇게 코드를 만들고 age와 gender값이 바뀌어도 재랜더링이 발생하지 않는다. name의 값이 바뀌어야 한번에 재랜더링이 된다.
  • [] 안에 2개 이상의 상태를 집어넣으면 그 중 하나만 변경해도 재랜더링이 발생하게 된다(or연산이라 보면 됨)

computed API

  • 옵션스 API의 기능이다.
  • 함수를 변수처럼 사용할 수 있게 만들어주는 API다.
  • return을 반환하는 함수에 사용할 수 있으며 내부적으로 캐시 되어있는 데이터를 출력하게 된다.
<script>
export default {
  data() {
    return {
      firstName: "kisu",
      lastName: "kim",
    };
  },
  methods: {},
  computed: {
    fullName() {
      console.log("ㅎㅇ");
      return `${this.lastName}${this.firstName}`;
    },
  },
};
</script>

<template>
  <h2>{{ fullName }}</h2>
  <h2>{{ fullName }}</h2>
  <h2>{{ fullName }}</h2>
  <h2>{{ fullName }}</h2>
  <h2>{{ fullName }}</h2>
</template>
  • 캐싱된 데이터를 사용해서 ㅎㅇ가 1번만 출력된다.
  • 참조할때 데이터가 ㅎㅇ가 아닌 다른 값이 나와야지만 캐싱이 풀리면서 다시 출력이 된다.
  • 이 computed는 캐시가 되어서 컴포넌트 성능적으로 부담이 되지 않는다는 장점이 있다.
  • 이 computed 속성은 현업에서 정말 많이 사용된다.
  • 어떠한 데이터를 가공처리할때 사용된다.
  • 그런데 파라미터를 사용할 수 없다(메서드가 아니라서, 파라미터를 쓰는건 메서드로 사용해야 한다.)
  • computed는 파라미터 없이 데이터 기반에서 조작할 때 사용한다.
  • setter와 getter 사용해서 뭔가뭔가 하는 방법이 있음(그런데 활용도가 약간 떨어짐)

vue의 컴포넌트

  • vue의 컴포넌트의 성격은 리액트와 동일하다.
  • 우린 컴포넌트를 등록하고, 데이터를 전달하는 방법을 배우면 된다.
  • vue에서 컴넌트를 등록하는 방법은 2가지가 있다.
  1. 전역 컴포넌트 등록방법
  • 이 방법은 main.js에서 건드려야 하는 작업이다.
  • main.js의 createApp(App)을 변수로 분리한다.
  • 분리한 변수에 .component를 넣고 인자값에 내가 만든 컴포넌트의 name값을 집어넣으면 설정된다.
import { createApp } from "vue";
import App from "./App.vue";
import First from "./components/First.vue";

const app = createApp(App);
// 전역 컴포넌트 등록방법
app.component("First", First);

app.mount("#app");
  • 이렇게 등록하면 어디에서든 내 컴포넌트들을 가져올 수 있게 된다.
  • 연속적으로 연결해서 컴포넌트들을 등록할 수 있다.
import { createApp } from "vue";
import App from "./App.vue";
import First from "./components/First.vue";
import Second from "./components/Second.vue";

const app = createApp(App);
// 전역 컴포넌트 등록방법
app //
  .component("First", First)
  .component("Second", Second);

app.mount("#app");
  1. 지역 컴포넌트 등록방법
  • 특정 컴포넌트 안에서만 사용하고 싶다면 옵션스 API의 components:{} 를 활용하면 된다.
<script>
import First from "./components/First.vue";
import Second from "./components/Second.vue";

export default {
  name: "App",
  components: {
    First,
    Second,
  },
};
</script>

<template>
  <article>스타또</article>
  <First />
  <Second />
</template>
  • 컴포넌트가 연결된 지역엔 그 지역의 style이 적용된다.

  • vue에서 컴포넌트를 렌더링하는 방식의 특징 때문에 발생하는 현상이다.

  • import받는 컴포넌트의 루트요소를 같은 범위로 인식하기 때문에 import되는 컴포넌트의 스타일도 루트컴포넌트의 스타일을 공유하게 된다.

  • vue의 컴포넌트엔 특징이 하나 있는데 컴포넌트요소에 id, class등 속성을 넣으면 해당 컴포넌트 파일의 루트요소에 그 속성이 전달된다.

  • 속성이 전달되는것 뿐 만 아니라 props의 역할까지 한다 = 2가지의 역할을 수행함

  <First id="title" class="title" />	// First 컴포넌트의 루트요소로 들어감
  • 그런데 2가지의 역할을 동시에 수행하는게 아니고 옵션스API의 props:[] 안에 등록된 것들만 props로 사용 가능하고 props에 등록되지 않은 것들이 속성의 역할을 수행하게 된다.
// 부모 컴포넌트
<script>
import First from "./components/First.vue";

export default {
  name: "App",
  components: {
    First,
  },
};
</script>

<template>
  <article>스타또</article>
  <First id="title" class="title" hello="hello" />		
</template>
// 자식 컴포넌트
<script>
export default {
  name: "First",
  data() {
    return {};
  },
  props: ["hello"],			// hello만 props로 사용 가능 + id랑 class는 속성값으로 들어감
};
</script>

<template>
  <div>첫번째 {{ hello }}</div>
</template>
  • 만약 숫자를 보내고 싶다면 v-bind:속성 을 사용하면 된다.
<First id="title" class="title" hello="hello" v-bind:age="20" />	
  • 만약 컴포넌트의 루트요소가 2개 이상이라면 props를 어떻게 한다고 했더라 gpt ㄱㄱ

vue의 라이프사이클

  • 리액트에서 라이프사이클은 mount. unmount, update가 있다.
  • vue에서는 더 많은 라이프사이클을 가지고 있다.
  1. setup
    // 생성전
  2. beforeCreate
  3. created
  4. beforeMount
    // 생성후
  5. mounted
  6. beforeUpdate
  7. updated
  8. beforeUnmount
  9. unmounted
  • 짱많네...

  • 총 9개의 라이프사이클을 가지고 있다.

  • 사용법은 다음과 같다.

  • 옵션스 API에 data(){} 처럼 추가해서 사용하면 된다.

export default {
  name: "First",
  data() {
    return {};
  },
  props: [],
  beforeCreate() {
    console.log("beforeCreate");
  },
  created() {
    console.log("created");
  },
  beforeMount() {
    console.log("beforeMount");
  },
  mounted() {
    console.log("mounted");
  },
  beforeUpdate() {
    console.log("beforeUpdate");
  },
  updated() {
    console.log("updated");
  },
  beforeUnmount() {
    console.log("beforeUnmount");
  },
  unmounted() {
    console.log("unmounted");
  },
};
  • vue에서는 useEffect 대신 이 라이프사이클 함수를 사용하여 관리한다.
  • 여기에 count를 넣어서 실험해보면 다음과 같다.
<script>
export default {
  name: "First",
  data() {
    return {
      count: 0,
    };
  },
  props: [],
  beforeCreate() {
    console.log("beforeCreate", this.count);
  },
  created() {
    console.log("created", this.count);
  },
  beforeMount() {
    console.log("beforeMount", this.count);
  },
  mounted() {
    console.log("mounted", this.count);
  },
  beforeUpdate() {
    console.log("beforeUpdate", this.count);
  },
  updated() {
    console.log("updated", this.count);
  },
  beforeUnmount() {
    console.log("beforeUnmount", this.count);
  },
  unmounted() {
    console.log("unmounted", this.count);
  },
};
</script>

<template>
  <div>첫번째</div>
  <p>{{ count }}</p>
  <button @click="count += 1">+</button>
</template>
// 컴포넌트 생성
beforeCreate undefined
First.vue:14 created 0
First.vue:17 beforeMount 0
First.vue:20 mounted 0
// 컴포넌트 업데이트(count + 1 눌렀을때)
First.vue:23 beforeUpdate 1				// 화면 업데이트 전 (렌더링 되기 이전부터 숫자는 증가해있다.)
First.vue:26 updated 1					// 화면 업데이트 후
// 컴포넌트 제거
First.vue:29 beforeUnmount 1
First.vue:32 unmounted 1
  • 여기서 업데이트 부분에 살짝 문제가 있다.
  • 업데이트는 어떤 data가 변경되든 간에 무조건 코드를 실행시킨다.
  • 만약 리액트의 의존성 배열 같이 특정 data의 업데이트만 코드를 실행하고 싶으면 옵션즈 API의 watch:{} 기능을 사용하면 된다.
watch: {
    count(cur, prev) {
      console.log("현재 값", cur);
      console.log("이전 값", prev);
    },
  },
  • 이렇게 watch 안에 감시할 함수를 넣는다.
  • 그렇게 되면 대상 값이 변경될 때의 최신값, 이전값을 구할수 있다.
  • 이게 의존성을 어떻게 대체해...?
  • 게다가 watch는 배열이나 객체같은 참조자료형의 변경을 감시할 수 없다.
  • 참조자료형 까지 감시하게 하려면 깊은 사용법이라는 기법을 사용해야 한다.
watch: {
    count: {
      handler(cur, prev) {
        console.log("현재 값", cur);
        console.log("이전 값", prev);
      },
      deep: true,
    },
  },
  • 하지만 이건 변경된 최신의 값만 확인이 가능하다(이전값도 최신값으로 보이는듯?)
  • 근데 실무를 하다보면 이전값이 그렇게 중요하지 않다.

vue의 props 드릴링

  • vue도 props를 전송하는 컴포넌트끼리 props드릴링 현상이 발생한다.
  • 이는 props기능을 사용하는 라이브러리들의 고질적인 문제다.

props 객체형태 저장 + 타입 부여하기

  • 오션스 API에서 props를 받는 방법으로 배열[]로 받는것 외에도 객체{}로 받는 방법이 있다.
  • 객체로 props를 받으면 추가로 타입을 설정해 줄 수 있다.
export default {
  name: "FirstChild",
  props: {
    age: { type: Number },		// age props의 타입을 number로 지정
  },
  data() {
    return {};
  },
};
</script>

<template>
  <h1>FirstChild</h1>
  <p>{{ age }} / {{ typeof age }}</p>
</template>
  • 이렇게 타입을 지정하면 타입에러가 발생했을때 에러창을 띄우진 않지만 콘솔창에 노란 경고창을 생성해서 실무에서 타입체크 경험을 향상시켜준다.
  • 유니온, 인터섹션 같은 기능은 타입스크립트의 기능이라 사용 못하지만 type의 속성값을 배열에 담으면 유니온타입 처럼 흉내낼 수는 있다.
export default {
  name: "FirstChild",
  props: {
    age: { type: [Number, String] },		// age props의 타입을 number 또는 string으로 지정
  },
  data() {
    return {};
  },
};
</script>

<template>
  <h1>FirstChild</h1>
  <p>{{ age }} / {{ typeof age }}</p>
</template>
  • 타입지정 말고도 default 값도 줄 수 있다.
props: {
    age: {
      type: [Number, String],
      default() {
        return 10;
      },
    },
  },
  • required로 옵셔널 구분할 수 있다.
props: {
    age: {
      type: [Number, String],
      require: true,
    },
  },
  • validator로 전달하면 안되는 값을 설정할 수 있다.(이건 vueJS 3.4 이상부터 가능)
props: {
    age: {
      type: [Number, String],
      require: true,
      default() {
        return 10;
      },
      validator(value) {
        return value > 0;			// 0보다 더 큰 값만 취급해줌
      },
    },
  • 값이 없는 속성을 사용할때 vue에서는 해당 값을 boolean값으로 인식해 true로 취급한다.
  • 그래서 props에서 값을 boolean으로 타입지정해주면 값이 true로 매겨진다.
props: {
    disabled: boolean	 // true로 값이 저장됨
  },
  • props로 전달되는 속성키의 타입도 지정할 수 있다.(약간 단축문법 느낌임)
props: {
    age: { type: [Number, String] },
    name: String,
  },
  • 함수도 props로 전달할 수 있다.(리액트도 됐었나?)
  <FirstChild :name="name" :print-hello="printHello"/>	// 전달할땐 케밥케이스로 쓰는데 받을땐 카멜케이스로 받는다.

사용자 정의 이벤트(잘 모르겠음)

  • 컴포넌트에 props를 전달할때 사용자가 정의한 이벤트를 만들수 있다.
  <FirstChild :name="name" :print-hello="printHello" @greet="greet" />
export default {
  name: "FirstChild",
  components: { SecondChild },
  data() {
    return {};
  },
  props: {
    name: String,
    printHello: Function,
  },
  emits: ["greet"],
};
</script>

<template>
  <h2>FirstChild</h2>
  <button @click="$emit('greet')">greet</button>
  <SecondChild :name="name" :print-hello="printHello" @greet="$emit('greet')" />
</template>
  • 이는 사용시 props로 상속 받는게 아니라 부모에 있는 함수를 호출하게 하는 기능이다.
  • 사용법이 $emit()으로 좀 불편한데 methods로 만들어서 사용하면 편하다.
emits: ["greet"],
  methods: {
    greet() {
      return this.$emit("greet");
    },
  },
  • 이거 함수 전달방법은 props든 emit든 사용하는건 취향차이니 쓰고싶은거 써라.
  • 근데 2개 차이점이 뭐지?

vue의 전역상태관리(provide)

  • vue도 리액트의 context API처럼 전역 상태관리 API가 존재한다.
  • 그런데 context API보단 더 쉽게 사용가능하다고 한다.
  • 옵션스 API의 provide, inject를 활용하면 전역으로 공유되는 변수나 함수를 생성할 수 있다.
<script>
import FirstChild from "./components/FirstChild.vue";

export default {
  name: "App",
  components: {
    FirstChild,
  },
  provide: {				// provide로 전역상태 설정
    message: "안녕!",
  },
};
</script>

<template>
  <FirstChild />
</template>
<script>
export default {
  name: "FirstChild",
  data() {
    return {};
  },
  inject: ["message"],		// inject로 전역상태 받기
  created() {
    console.log(this.message);
  },
};
</script>

<template>
  <div>{{ message }}</div>
</template>
  • 이는 전역으로 공유하는 상태이기 때문에 props드릴링이 발생하지 않는다.

  • 자신보다 하위의 컴포넌트들에게 inject로 데이터를 전달해줄 수 있다.

  • 자식에서 부모는 provide를 할 수 없다.(렌더링 시점부터 다름)

  • 물론 함수도 가능하다

<script>
import FirstChild from "./components/FirstChild.vue";

export default {
  name: "App",
  components: {
    FirstChild,
  },
  provide: {
    message: "안녕!",
    print(e) {
      console.log(e);
    },
  },
};
</script>

<template>
  <FirstChild />
</template>
<script>
import SecondChild from "./SecondChild.vue";

export default {
  name: "FirstChild",
  data() {
    return {};
  },
  inject: ["message", "print"],
  created() {
    console.log(this.message);
  },
  components: {
    SecondChild,
  },
};
</script>

<template>
  <div>{{ message }}</div>
  <SecondChild />
  <button @click="print('a')">a</button>
</template>
  • 단, provide로 전송해주는 컴포넌트에서 만든 methods나 data를 this.으로 담아서 전송하는건 불가능하다.

  • 하지만 이것도 provide를 함수처럼 활용하면 this참조로도 사용 가능하다.

  • 함수처럼 활용한다는 말은 provide를 함수형태로 만들고 함수 안에 return{} 을 넣어서 그 안에서 작성하면 된다.


<script>
import FirstChild from "./components/FirstChild.vue";

export default {
  name: "App",
  components: {
    FirstChild,
  },
  data(){
    return{
      ex: "해위이이"
    }
  },
  provide() {				// 함수형태로 작성
    return {					// return문으로 객체형식 공급
      message: "안녕!",
      print(e) {
        console.log(e);
      },
      ex: this.ex,				// this키워드로 컴포넌트 내의 상태 불러오기 가능
    };
  },
};
</script>

<template>
  <FirstChild />
</template>
  • 따라서 provide는 this도 사용할 수 있는 상위호환인 함수형태 provide를 사용하면 된다.

  • 그런데 inject로 받은 값은 그 값을 변경하려해도 화면에 반영되지 않는다.

  • provider로 공급받은 상태는 반응성을 가지지 않기 때문이다.

  • 하지만 받을 수 있게 하는 방법은 computed 함수를 사용하면 된다(옵션스 API 그거 아님)

  • provide를 넘길때 이 computed함수로 래핑한 후에 전송하면 해당 상태가 반응성을 가지게 된다.

  • 반응성을 주고 싶으면 이 computed를 사용해서 바꾸면 된다.

  • 그리고 computed를 받은 상태는 readonly가 걸리기 때문에 루트 컴포넌트에서 해당 상태를 수정하는 함수를 inject로 가져와 사용하지 않는 이상 값의 직접적인 변경은 할 수 없다.

  • inject를 사용할때 배열[] 말고 객체{}로도 가져올 수 있다.

  • 객체로 가져오면 상태의 이름을 변경할 수 있고, 기본값을 정할 수 있다.

inject: {
  message : {
    from: "message2",
    default(){
      return "방가워"
    } 
  }
}
  • 만약 provide해주는 상태명이 같은 것들이 컴포넌트 안에 2개 이상 존재한다면 받는 컴포넌트 기준으로 제일 가까운 부모 컴포넌트의 상태를 따라간다(오류표시도 안나와서 주의해야함)
  • 그래서 symbol()을 활용한 변수를 만들어 provide의 속성키값을 고정된 문자가 아닌 symbol()의 유니크한 값으로 사용하게 되면 상태명이 같아도 실질적인 값이 다르기 때문에 독립적으로 provide를 사용할 수 있게 된다(이렇게 사용할땐 반드시 inject를 객체형태로 사용해야함)
// keys.js
export const count = Symbol();
  provide() {
    return {
      [count]: computed(() => this.count),
      increment: this.increment,
    };
  },

컴포넌트에 ref속성 사용하기

  • 컴포넌트의 속성에 ref도 넣을 수 있다.
  • 사용은 $refs로 사용 가능하다
  • 이걸로 자식의 내용을 부모가 끌어와서 쓸 수 있는데 단방향 데이터의 흐름을 깨는 방법이라 사용해도 상관없긴 한데 조금 몬가몬가임 (컴포넌트가 서로를 의존하는 상황이 더욱 많아져서 결합도가 높아지고 유지보수에 어려움이 있을수 있다.)

slot

  • 리액트의 children 같은 props 전송기능이 뷰에도 있다.
  • 리액트의 컴포넌트 태그 안에, 그러니까 컨텐츠 부분에 전송할 데이터를 집어넣으면 데이터가 children 배열에 담겨서 props로 전송된다.
  • 사용 방법은 쉽다.
  • 리액트 처럼 컴포넌트태그의 컨텐츠 부분에 원하는 값을 넣은 다음, 컴포넌트에서 보여주고 싶은 부분에 태그를 넣어주면 끝이다.
import Button from "./components/Button.vue";

export default {
  name: "App",
  data() {
    return {};
  },
  components: {
    Button,
  },
};
</script>

<template>
  <div>asd</div>
  <Button>버튼</Button>		// Button 컴포넌트에 컨텐츠로 "버튼" 이라는 텍스트 전송
</template>
<script>
export default {
  name: "button",
  data() {
    return {};
  },
};
</script>

<template>
  <button>
    <slot></slot>		// 이곳에 "버튼" 텍스트 렌더링
  </button>
</template>
  • 이 을 활용해서 컨텐츠의 내용을 렌더링 하는걸 이름 없는 슬롯 방식 이라고 부른다.

  • 이름이 있는 슬롯 방식이란 것도 있다.

  • 이름이 있는 슬롯 방식은 컨텐츠를 여러개 만들어야 할 때 v-slot이라는 디렉티브를 활용해 이름을 지정해주면 여러개의 slot을 사용할 수 있게 된다.

  • v-slot을 활용하여 이름을 정해줄땐 각각의 슬롯마다 template태그로 감싸고 거기에 v-slot을 넣어줘야 한다.

<script>
import Button from "./components/Button.vue";
import DefaultLayout from "./layouts/DefaultLayout.vue";

export default {
  name: "App",
  data() {
    return {};
  },
  components: {
    Button,
    DefaultLayout,
  },
};
</script>

<template>
  <div>asd</div>
  <Button>버튼</Button>
  <DefaultLayout>
    <template v-slot:header>		
      <header>
        <h1>header</h1>
      </header>
    </template>
    <template v-slot:main>
      <main>
        <h2>main</h2>
      </main>
    </template>
    <template v-slot:footer>
      <footer>
        <h3>footer</h3>
      </footer>
    </template>
  </DefaultLayout>
</template>
<script>
export default {
    name: "DefaultLayout"
};
</script>

<template>
  <slot name="header"></slot>
  <slot name="main"></slot>
  <slot name="footer"></slot>
</template>
  • 혼합 슬롯이란 것도 있다.
  • 이름이 있는 슬롯 사이에서 이름이 없는 슬롯을 만드는 방법이 있다.
  • 이름이 없는 슬롯으로 만들고 싶은 부분에 v-slot:default 를 넣어주거나 template를 빼주면 된다.
<template>
  <div>asd</div>
  <Button>버튼</Button>
  <DefaultLayout>
    <template v-slot:header>
      <header>
        <h1>header</h1>
      </header>
    </template>

    <template v-slot:default>			// default로 이름 없는 슬롯 지정(또는 template를 없애는 것도 방법이다.)
      <main>
        <h2>main</h2>
      </main>
    </template>

    <template v-slot:footer>
      <footer>
        <h3>footer</h3>
      </footer>
    </template>
  </DefaultLayout>
</template>
<script>
export default {
  name: "DefaultLayout",
};
</script>

<template>
  <slot name="header"></slot>
  <slot></slot>					// 이름없는 슬롯으로 사용 가능
  <slot name="footer"></slot>
</template>
  • slot의 컨텐츠 안에 무언갈 넣으면 그건 slot의 기본값이 된다.
  • slot으로 전달되는 컨텐츠가 없을시 그 기본값을 보여준다.
<slot>
  <span>안녕</span>		// 기본값이 됨
</slot>
  • slot을 동적으로 끼우고 싶다면 v-slot:[]를 만들고 배열 안에 넣고싶은 슬롯명을 변수로 만들어 집어넣으면 동적으로 슬롯을 보여줄 수 있다.
<script>
import Button from "./components/Button.vue";
import DefaultLayout from "./layouts/DefaultLayout.vue";

export default {
  name: "App",
  data() {
    return {
      slotName: "header",			// 동적으로 추가할 슬롯명을 변수로 저장
    };
  },
  components: {
    DefaultLayout,
  },
};
</script>

<template>
  <DefaultLayout>
    <template v-slot:[slotName]>		// [] 안에 변수를 넣어 동적으로 슬롯명 지정
      <header>
        <h1>header</h1>
      </header>
    </template>
  </DefaultLayout>
</template>
  • slot은 #으로 단축표현이 가능하다.
<template #footer>
      <footer>
        <h3>footer</h3>
      </footer>
</template>
  • slot으로 생성된 컨텐츠는 부모 컴포넌트의 data와 스타일 영향을 받는다. 이를 슬롯의 범위라고 한다.

  • slot으로 css가 꾸며진 컨텐츠를 전송하고 싶다면 부모 컴포넌트에서 작업한 뒤 전송해야 한다.

  • slot에 전할 컨텐츠가 data(){} 안에 있는 값이 바인딩 되어 있을때 그 바인당 키값이 부모, 자식 컴포넌트 둘 다 존재한다면 부모 컴포넌트의 키값을 사용한다.

  • 슬롯의 범위의 영향을 받는 슬롯을 범위가 지정된 슬롯이라고 한다.

  • 만약 data의 영향을 자식 컴포넌트로 부터 사용하고 싶으면 v-bind 형태로 slot에 집어넣으면 된다.

<slot :buttonText="buttonText">{{ buttonText }}</slot>
  • 혼합해서 사용도 가능하다.
<Button #="{ buttonText, count }">
    <span>{{ buttonText }} {{ count }}</span>
  </Button>
<script>
export default {
  name: "button",
  data() {
    return {
      buttonText: "헬로우",
      count: 10,
    };
  },
};
</script>

<template>
  <button>
    <slot :buttonText="buttonText" :count="count"></slot>
  </button>
</template>

컴포지션 API

  • vue가 버전 3이 나오면서 옵션스 API보다 훨씬 간편하게 코드를 작성할 수 있는 방법이 나왔다.
  • 바로 컴포지션 API다.
  • 사용방법은 2가지로 나뉜다.

setup 사용하기

// 옵션스 API
<script>
export default {
  name: "Option",
  data() {
    return {
      count: 0,
    };
  },
  computed: {
    doubleCount() {
      return this.count * 2;
    },
  },
  methods: {
    increment() {
      this.count += 1;
    },
    decrement() {
      this.count -= 1;
    },
    reset() {
      this.count = 0;
    },
  },
};
</script>

<template>
  <div>{{ count }}</div>
  <div>doubleCount {{ doubleCount }}</div>
  <button @click="decrement">감소</button>
  <button @click="reset">리셋</button>
  <button @click="increment">증가</button>
</template>
// 컴포지션 API
import { computed, ref } from "vue";

export default {
  name: "Composition",
  setup() {
    const count = ref(0);
    const doubleCount = computed(() => count.value * 2);
    const decrement = () => count.value--;
    const increment = () => count.value++;
    const reset = () => (count.value = 0);

    return { count, doubleCount, decrement, increment, reset };
  },
};
</script>

<template>
  <div>{{ count }}</div>
  <div>doubleCount {{ doubleCount }}</div>
  <button @click="decrement">감소</button>
  <button @click="reset">리셋</button>
  <button @click="increment">증가</button>
</template>
  • 간단한 로직이라도 옵션스 API는 길이를 차지하는 속성을 많이 사용해 코드가 길어지는 반면에 컴포지션 API는 필요한 API를 함수 호출 처럼 부르는 형식으로 더욱 간편하고 간략하게 로직을 만들 수 있다.

  • 앞선 setup 함수는 vue의 이전버전인 옵션스API에 대한 호환성을 위해 옵션스API와 비슷하게 만들어 놨다.

  • 그러다 보니 딱히 옵션스 API와 큰 변화가 없어보여 잘 안쓰게 되었다.

  • 그래서 새로 컴포지션 API 작성문법을 추가하게 되었는데 그게 바로 script setup이다

  • 이 방법은 구버전의 코드가 아닌 이상 무조건 이 방법을 사용한다.

  • 이 코드가 너무나도 쉽고 명확하기 때문이다.

// 컴포지션 API
import { computed, ref } from "vue";

export default {
  name: "Composition",
  setup() {
    const count = ref(0);
    const doubleCount = computed(() => count.value * 2);
    const decrement = () => count.value--;
    const increment = () => count.value++;
    const reset = () => (count.value = 0);

    return { count, doubleCount, decrement, increment, reset };
  },
};
</script>

<template>
  <div>{{ count }}</div>
  <div>doubleCount {{ doubleCount }}</div>
  <button @click="decrement">감소</button>
  <button @click="reset">리셋</button>
  <button @click="increment">증가</button>
</template>
// script setup
<script setup>
import { computed, ref } from "vue";

const count = ref(0);
const doubleCount = computed(() => count.value * 2);
const decrement = () => count.value--;
const increment = () => count.value++;
const reset = () => (count.value = 0);
</script>

<template>
  <div>{{ count }}</div>
  <div>doubleCount {{ doubleCount }}</div>
  <button @click="decrement">감소</button>
  <button @click="reset">리셋</button>
  <button @click="increment">증가</button>
</template>
  • 다음과 같이 script에 setup 속성만 집어넣으면 여태껏 썻던 옵션스 API들을 전혀 쓸 필요 없이 일반 자바스크립트 처럼 API를 만들 수 있게 된다.
  • 부모 컴포넌트가 자식 컴포넌트를 import 하는 방식도 옵션스를 쓸 필요없이 setup만 자바스크립트에 속성으로 붙이면 마치 자바스크립트 언어처럼 import문으로 땡겨올 수 있다 === template에서 바로 emmet을 통해 컴포넌트를 import 할 수 있게 된다.
<script setup>
import Composition from "./components/Composition.vue";
import Option from "./components/Option.vue";
import Setup from "./components/Setup.vue";
</script>

<template>
  <Option />
  <Composition />
  <Setup />
</template>
  • 이러면 vue 배울만하지 ㅋㅋ

ref, reactive

  • ref: 기본 자료형을 반응형 데이터로 정의할때 사용
<script setup>
import { ref } from "vue";

const count = ref(0);
const state = ref({
  count: 0,
});
const numArr = ref([1, 2, 3]);
</script>

<template>
  <h2>{{ count }}</h2>
  <h2>{{ state.count }}</h2>
  <h2>{{ numArr }}</h2>

  <button @click="count++">증가(count)</button>
  <button @click="state.count++">증가(state.count)</button>
  <button @click="numArr.push(4)">4 추가</button>
</template>
  • reactive: 참조 자료형(배열, 객체)를 반응형 데이터로 정의할 때 사용
  • ref도 배열, 객체를 반응형 데이터로 정의할 수 있지만 reactive에선 script 태그에서 데이터를 참조할 때 ref는 객체에 접근하기 위해 .value를 무조건 붙여야 하지만 reactive는 일반 객체처럼 바로 접근이 가능하다.
<script setup>
import { reactive, ref } from "vue";

const state = ref({
  count: 0,
});
console.log(state.value.count);
const state2 = reactive({
  count: 0,
});
console.log(state2.count);
</script>

computed

  • computed는 computed(()=>{})로 사용하면 된다.

함수

  • 일반 자바스크립트 쓰는것 처럼 function을 사용해도 되고 화살표함수를 써도 된다.

watch

  • watch 함수를 사용할 수 있다.
<script setup>
const count = ref(0);

watch(count, (cur, prev) => {				// 1번째 파라미터에 변경되는 상태, 2번째 파라미터엔 상태변경시 실행하는 코드
  console.log("count changed", cur, prev);
});
</script>

<template>
  <h2>{{ count }}</h2>

  <button @click="count++">증가(count)</button>
</template>
  • 배열이나 객체의 감지는 일반 watch로는 안되기 때문에 깊은 감시자를 사용해야 하는데 그러기 위해서 3번째 파라미터로 감시자 옵션을 부여할 수 있다.
<script setup>
import { reactive } from "vue";

const obj = ref({ count: 0 });

watch(
  obj,
  (cur, prev) => {
    console.log("count changed", cur, prev);
  },
  { deep: true }						// 3번째 파라미터에 {deep:true}를 주면 깊은 감시자 옵션이 적용됨
);
</script>

<template>
  <h2>{{ obj.count }}</h2>

  <button @click="obj.count++">증가(count)</button>
</template>
  • 그런데 reactive로 만든 자료형은 깊은 감시자를 사용할 필요 없다.
  • reactive가 알아서 객체, 배열 자료형의 변경을 감지해주기 때문이다.
<script setup>
import { reactive } from "vue";

const obj = reactive({ count: 0 });

watch(obj, (cur, prev) => {
  console.log("count changed", cur, prev);		// reactive는 깊은 감시자 필요없음
});
</script>

<template>
  <h2>{{ obj.count }}</h2>

  <button @click="obj.count++">증가(count)</button>
</template>
  • deep:true를 설정한 watch의 3번째 파라미터엔 다른 추가설정을 할 수 있다.
  1. immediate
  • immediate는 리액트의 useEffect처럼 최초 한번은 watch의 코드를 실행시키고 싶을때 사용할 수 있다.
watch(
  obj,
  (cur, prev) => {
    console.log("count changed", cur, prev);
  },
  { immediate: true } 		// immediate를 사용하면 최초 1번 console.log("count changed", cur, prev);가 실행된다.
);
  1. once
  • 데이터의 상태에 의한 변화를 한번만 감지하고 싶을때 사용한다.
const obj = reactive({ count: 0 });
watch(
  obj,
  (cur, prev) => {
    console.log("2 changed", cur, prev);
  },
  { once: true }		// 상태 변경을 한 번만 감지해 다음 상태변경은 watch의 코드가 실행되지 않는다.
);
  1. flush
  • 상태 변경 여부에 따라 돔을 조작할때 시점을 맞출 수 있게 해준다.
  • 값을 pre, post, sync를 주는것에 따라 다른 결과를 내준다.
  • pre: DOM이 업데이트 되기 전
  • post: DOM이 업데이트 되고 난 후
  • sync: 반응형 상태가 변경될때 즉시 동기적으로 실행(오류 발생할 수도 있음)

watchEffect

  • immediate와 deep이 true로 기본설정되어있는 watch 함수
  • 로직 내부에서 사용하는 반응형 데이터 전부를 자동으로 감시해준다.
  1. watchPostEffect
  • immediate: true, deep: true, flush: post가 기본값으로 설정되어있는 watch 함수
  • 반응형 상태가 변경된 후 DOM 업데이트가 완료된 다음에 효과 함수가 실행한다.

import 없이 vue 문법 사용하기

  • vue문법을 import 없이 사용하려면 다음과 같은 작업을 거쳐야 한다.
  1. unplugin-auto-import 라이브러리를 설치한다.
npm install unplugin-auto-import
  1. vite.config에 들어가서 plugins 부분에 AutoImport({imports: ['vue'])})를 추가해주면 된다.
import AutoImport from "unplugin-auto-import/vite";

// https://vite.dev/config/
export default defineConfig({
  plugins: [vue(), vueDevTools(), AutoImport({ imports: ["vue"] })],
  resolve: {
    alias: {
      "@": fileURLToPath(new URL("./src", import.meta.url)),
    },
  },
});
  • 이러면 import문을 사용하지 않고 vue 문법을 가져올 수 있다.

setup라이프 사이클

  • setup의 라이프사이클은 옵션스API에서 사용했던 라이프사이클 훅 앞에 on을 붙여서 사용한다.
<script setup>
import {
  onBeforeMount,
  onBeforeUpdate,
  onMounted,
  onUnmounted,
  onUpdated,
} from "vue";

const count = ref(0);

console.log("component setup!");

onBeforeMount(() => {
  console.log("onBeforeMount");
});
onMounted(() => {
  console.log("onMounted");
});
onBeforeUpdate(() => {
  console.log("onBeforeUpdate");
});
onUpdated(() => {
  console.log("onUpdated");
});
onBeforeUnmount(() => {
  console.log("onBeforeUnmount");
});
onUnmounted(() => {
  console.log("onUnmounted");
});
</script>

<template>
  <h1>LifeCycle {{ count }}</h1>
  <button @click="count++">+</button>
</template>
  • 옵션스 API엔 beforeCreate와 created 훅이 있다.
  • 하지만 컴포지션 API는 이 둘이 없다.
  • 대신 setup이라는 라이프사이클이 존재한다.(맞나?)

props

  • 컴포지션API에선 defineProps라는 함수를 사용해서 props를 전송받는다.
<script setup>
const props = defineProps({
  count: {
    type: Number,
    rejected: true,
    default() {
      return 10;
    },
    validator(value) {
      return value > 0;
    },
  },
});
</script>

<template>
  <div>{{ props.count }}</div>
</template>

사용자 정의 속성

  • 옵션스 API는 사용자 정의 속성을 emits로 받았다.
  • 컴포지션 API는 defineEmits로 속성을 받을 수 있다.
<script>
const emite = defineEmits(["increment"]);
const increment = () => {
  emite("increment", 120);
};
</script>

<template>
  <button @click="increment">클릭</button>
</template>

provide, inject

  • provide랑 inject라는 함수를 사용하면 끝이다.
<script setup>
import Count from "./components/Count.vue";

const count = ref(0);
provide("provideCount", count);
</script>
<script setup>
import { inject } from "vue";

const provideCount = inject("provideCount");
</script>

<template>
  <h1>provideCount: {{ provideCount }}</h1>
</template>

vue Router

  • 뷰에서 공식으로 지원하는 라우터 라이브러리가 있다.
  • 바로 vue-router다.
  • 사용방법은 다음과 같다.
  • 먼저 공식 홈페이지에 있는 vue-router 설치 명령어를 이용해 패키지를 설치한다.
npm install vue-router@4
  • main.js에 createRouter 함수를 불러와 router 변수를 만든다.
// main.js
const router = createRouter({})
  • 이 createRouter 안에 먼저 history: createWebHistory()를 집어넣는다.
  • 이건 /user , /user/1 같은 url 엔드포인트를 추가하기 위해 사용한다.
  • 이 / 외에도 history: createWebHashHistory()를 사용하면 / 대신 # 로 url을 입력할 수 있는데 거의 안 쓴다.
const router = createRouter({
  history: createWebHistory(), // /user, /user/1 등의 url을 사용하기 위해 사용
  //   history: createWebHashHistory(),   해쉬는 잘 안씀
});
  • routes: []를 추가해서 이 안에 라우트를 정의해줄 수 있다.
import Home from "./pages/Home.vue";
import About from "./pages/About.vue";

const router = createRouter({
  routes: [
    {
        path: "/",
        name: "Home",
        component: Home
    },
    {
        path: "/about",
        name: "About",
        component: About
    },
  ], // 라우트 정의
});
  • 그런데 main.js에 라우트를 다 넣으면 보기 너무 복잡하니 router 폴더를 하나 만들고 그 안에 index.js파일을 하나 만들어 거기에 만든 라우터들을 전부 옮기자.
  • 그리고 main.js에 만든 router 변수를 import 해주면 된다.
// router -> index.js
import { createRouter, createWebHistory } from "vue-router";
import Home from "./pages/Home.vue";
import About from "./pages/About.vue";

const router = createRouter({
  history: createWebHistory(), // /user, /user/1 등의 url을 사용하기 위해 사용
  //   history: createWebHashHistory(),   해쉬는 잘 안씀
  routes: [
    {
        path: "/",
        name: "Home",
        component: Home
    },
    {
        path: "/about",
        name: "About",
        component: About
    },
  ], // 라우트 정의
});
// main.js
import { createApp } from "vue";
import App from "./App.vue";
import { router } from "./router";

const app = createApp(App);
app.use(router);
app.mount("#app");
  • 이제 라우팅을 보여줄 컴포넌트에 리액트의 Outlet 처럼 RouterView라는 태그를 사용하면 그 위치에 라우트 페이지를 보여준다.
<script setup></script>

<template>
  <RouterView></RouterView>
</template>
  • 추가로 리액트의 Link 처럼 RouterLink를 사용해서 라우트 경로로 이동해주는 하이퍼링크를 만들 수 있다.
<script setup></script>

<template>
  <nav>
    <RouterLink to="/">Home</RouterLink>
    <RouterLink to="/about">About</RouterLink>
  </nav>
  <RouterView></RouterView>
</template>
  • 지금까지 한 라우팅은 정적 임포트 방식의 라우팅이다.
routes: [
    {
      path: "/",
      name: "Home",
      component: Home, // 정적 임포트 방식
    },
    {
      path: "/about",
      name: "About",
      component: About, // 정적 임포트 방식
    },
  ], // 라우트 정의
  • 이 정적 임포트 방식엔 단점이 있는데 바로 필요없는 페이지에 대한 컴포넌트도 모두 불러온다는 것이다.

  • 내가 home 페이지에 접속했지만 사실 about 컴포넌트 까지 네트워크 요청에 요청이 가있다. === 쓰지 않는 페이지 까지 요청해서 불러와 리소스 낭비를 하고 있다.

  • 하지만 리소스를 미리 불러와 해당 페이지로 이동이 빠르기 때문에 상황에 따라선 이런 정적 임포트 방식이 더 좋을 수 있다.

  • 동적임포트로 적용하려면 콜백함수와 import함수를 사용하면된다.

routes: [
    {
      path: "/",
      name: "Home",
      component: Home, // 정적 임포트 방식
    },
    {
      path: "/about",
      name: "About",
      component: () => import("@/pages/About.vue"),  // 동적 임포트 방식
    },
  ], // 라우트 정의
  • 이렇게 하면 필요한 페이지의 컴포넌트만 불러올 수 있다.

  • 단점으론 페이지 이동 시 로딩 시간이 필요하다는 점이 있다.

  • 정적 임포트는 home에, 동적 그 외엔 전부 동적 임포트를 하는게 좋다.

동적 경로 매칭

  • vue-router의 라우터 전략중 하나인 동적경로 매칭은 리액트의 쿼리 파라미터와 같은 역할을 하는거 같다.
{
      path: "/about/:id",
      name: "About",
      component: () => import("@/pages/About.vue"), 
    },
<RouterLink to="/about/1">About</RouterLink>
  • 이렇게 엔드포인트/:원하는아이디명 을 경로로 설정해주면 엔드포인트/: 뒤의 내용이 어떤게 나와도 무조건 그 라우트에 존재하는 component를 불러온다.
  • 추가로 뒤에 추가한 내용은 쿼리 파라미터로써 활용할 수 있다.
  • 사용할 페이지에 useRoute()를 변수로 저장해 route.params로 값을 불러올 수 있다.
<script setup>
import { useRoute } from "vue-router";

const route = useRoute();
const { id, user } = route.params;
</script>

<template>
  <div>{{ id }}</div>
</template>
  • 만약 경로가 http://localhost:5173/user/3 이면 id 부분엔 ko가 담기게 된다
  • 추가로 route.query을 사용해 쿼리 스트링의 내용을 가져올 수 있다.
<script setup>
import { useRoute } from "vue-router";

const route = useRoute();
const { id, user } = route.params;
const { lang } = route.query;
</script>

<template>
  <div>{{ id }}</div>
  <div>{{ lang }}</div>
</template>

예외 페이지

  • 예외 페이지는 routes의 끝에 다음과 같이 만들어준다.
{
   path: "/:pathMatch(.*)*",
   name: "NotFound",
   component: () => import("@/pages/NotFound.vue"),
}
  • 이제 이상한 페이지로 이동하면 NotFound 컴포넌트를 보여준다(물론 NotFound 컴포넌트 만들어야 함)

  • /user-:after 이렇게 :를 /user- 가 입력된 뒤에 들어가게 되면 user- 은 고정 path가 되고 그 뒤에 값부터 쿼리 파라미터가 된다. - 이를 잘 활용하면 좋타

useRouter 컴포저블

  • 우리가 방금 전 까지 쓴 건 useRoute인데 useRouter은 프로그래밍 탐색방식을 할 때 사용한다.
  • 404페이지를 예시로 들어보자
<script setup>
import { useRouter } from "vue-router";

const router = useRouter();
</script>
<template>
  <div>NotFound</div>
  <button @click="router.push('/')">홈으로 가기</button>
</template>
  • 이렇게 router.push('/')를 사용하면 해당 경로로 이동하게 되는데 RouterLink 대신에 이걸 사용할 수도 있다.
  • 이 router는 push, replace, forwoard(앞으로 가기), back(뒤로 가기), go(n만큼 앞으로 가기 / 뒤로 가기) 등등 여러 경로이동을 할 수 있다.
  • 특히 replace는 코드 경로를 남기지 않고 페이지를 이동할때 사용할 수 있다.
  • 주로 결제페이지에 사용되는데 사용자가 해당 페이지로 돌아가지 않게 하기 위해 사용한다.
<script setup>
import { useRouter } from "vue-router";

const router = useRouter();
</script>
<template>
  <div>NotFound</div>
  <button @click="router.replace('/')">홈으로 가기</button>	// 404페이지에 접속한 기록이 남지 않는다.
</template>

이름이 있는 경로 탐색 방식

  • 우리가 routes 설정을 할때 name속성을 집어넣는다.
routes: [
    {
      path: "/",
      name: "Home",
      component: Home, // 정적 임포트 방식
    },
    {
      path: "/about/:id",
      name: "About",
      component: () => import("@/pages/About.vue"), // 동적 임포트 방식(해당 url로 접근할때 컴포넌트를 받아옴)
    },
    {
      path: "/user/:id",
      name: "User",
      component: () => import("@/pages/User.vue"),
    },
    {
      path: "/:pathMatch(.*)*",
      name: "NotFound",
      component: () => import("@/pages/NotFound.vue"),
    },
  ], // 라우트 정의
  • 이 name을 활용해서 경로 이동을 할 수 있는 방식이 있다.
  • RouterLink 태그의 to 속성을 v-bind로 묶어서 사용하면 이름이 있는 경로로 이동할 수 있다.
<RouterLink :to="{ name: 'About' }">About</RouterLink>
  • 근데 쿼리 파라미터가 있는 라우터엔 사용이 안되네 /:id 이거
  • 근데 이것도 방법이 있따
<RouterLink :to="{ name: 'About', params: { id: 1 } }">About</RouterLink>
  • 이렇게 링크에 params 까지 추가해주면 파라미터 값도 설정해서 링크보낼 수 있다.
  • 추가로 쿼리 스트링 까지도 된다.
<RouterLink
      :to="{ name: 'About', params: { id: 1 }, query: { lang: 'ko' } }"
      >About</RouterLink
    >

중첩경로

  • 만약 경로를 추가할때 이전 경로에서 다음 경로 페이지로 이동하는게 아닌 이전 경로 페이지에서 다음 경로 페이지를 보여주는 설정을 하고 싶다면 children:[]을 사용하여 추가하면 된다.
{
      path: "/product",
      name: "Product",
      component: () => import("@/pages/Product.vue"),
      children: [
        {
          path: "info",
          name: "ProductInfo",
          component: () => import("@/pages/ProductItem.vue"),
        },
        {
          path: ":item",		// path를 :item으로 설정하면 params까지 사용 가능하다.
          name: "ProductItem",
          component: () => import("@/pages/ProductItem.vue"),
        },
      ],
    },
  • 이제 사용하는 페이지에 로 outlet을 만들어주면 된다.
// product.vue
<script setup></script>

<template>
  <div>product</div>
  <RouterView />
</template>

이름이 있는 vue 방식

  • 라우팅할 페이지에 header, footer을 설정한 후 RouterLink에 name을 header, footer로 맞추면 그에 따라 컴포넌트들을 결합해 보여준다.
{
      path: "/dashboard",
      name: "Dashboard",
      components: {
        header: () => import("@/components/DashboardHeader.vue"),
        default: () => import("@/pages/Dashboard.vue"),
        footer: () => import("@/components/DashboardFooter.vue"),
      },
    },
<script setup></script>

<template>
  <nav>
    <RouterLink to="/">Home</RouterLink>
    <RouterLink
      :to="{
        name: 'About',
        params: { id: 1, more: 'hi' },
        query: { lang: 'ko' },
      }"
      >About</RouterLink
    >
    <RouterLink
      :to="{
        name: 'Product',
        params: { item: '아이템' },
      }"
      >ProductItem</RouterLink
    >
  </nav>
  <RouterView name="header"></RouterView>
  <RouterView></RouterView>
  <RouterView name="footer"></RouterView>
</template>

컴포저블

  • 컴포저블은 vue에서 사용하는 훅 같은 거다.
  • 리액트 처럼 use~로 사용된다.

redirect

  • 만약 내가 배포 이후에 route 경로의 이름을 바꾸고 싶을때 redirect라는 속성을 사용할 수 있다
{
      path: "/user/:id",
      name: "User",
      redirect: ()=> "/about-new"	// 리다이렉트
    },

alias

  • url의 별칭을 지정할 수 있다.
  • 별칭을 지정하면 path 외에도 alias를 입력하면 해당 페이지로 이동이 가능하다.
{
      path: "/dashboard",
      name: "Dashboard",
      components: {
        header: () => import("@/components/DashboardHeader.vue"),
        default: () => import("@/pages/Dashboard.vue"),
        footer: () => import("@/components/DashboardFooter.vue"),
      },
      alias: "/dash",	// dash 입력시에도 페이지 이동 가능
    },
  • 여러개를 만들 수도 았고, params도 가능하다.
{
      path: "/dashboard",
      name: "Dashboard",
      components: {
        header: () => import("@/components/DashboardHeader.vue"),
        default: () => import("@/pages/Dashboard.vue"),
        footer: () => import("@/components/DashboardFooter.vue"),
      },
      alias: ["/dash/:id", "/ddaasshh/:iidd"],
    },

vue를 create로 설치할때 3번째 항목에서 router설치하겠냐는 문구에 yes라고 답하면 라우터를 설치해주고 라우팅 초기세팅(index.js 생성, main.js 초기설정)을 다 해준다.

네비게이션 가드

  • 라우팅 과정 중에 특정 지점(시점)에서 라우트 전환을 가로채서 사용자 정의 기능을 추가할때 사용
  • 총 3가지로 beforeEach, beforeResolve, afterEach가 있다.
  • router.beforeEach는 라우트가 전환되기 전에 실행된다.
  • 페이지 전환시에 to, from, next 파라미터로 넘어가는 페이지들의 정보를 확인할 수 있고, 페이지가 전환되기 전에 if문 같은 코드를 돌려볼 수 있고 조건여부에 따라 페이지 이동을 제약할 수도 있다.
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: "/",
      name: "home",
      component: HomeView,
    },
    {
      path: "/about",
      name: "about",
      // route level code-splitting
      // this generates a separate chunk (About.[hash].js) for this route
      // which is lazy-loaded when the route is visited.
      component: () => import("../views/AboutView.vue"),
    },
  ],
});

router.beforeEach((to, from, next) => {
  //매개변수에 현재값(to), 이전값(from), 다음값(next)이 담긴다.
  const goToNext = false;
  if(goToNext){
  	next()	
  } else {
  	next(false) // 다음 페이지로 전환할 수 없게 막음
  }
});
  • 다른 시점으로 router.beforeResolve가 있다.
  • beforeResolve는 라우트가 전환되기 바로 직전에 실행된다.(beforeEach가 먼저 실행되고 다음에 이거)
  • 이 beforeResolve를 대부분 사용한다.
router.beforeResolve((to, from, next) => {
  //매개변수에 현재값(to), 이전값(from), 다음값(next)이 담긴다.
  console.log(to);
  console.log(from);
});
  • 라우트가 진행되고 난 후도 있다.
  • router.afterEach는 라우터 전환이 끝난 후에 코드가 실행된다.
  • 이 3번째 파라미터엔 라우트가 실패한 것에 대한 내용이 담기며, 이로 인한 후속처리를 하는데 사용 할 수 있다.
router.afterEach((to, from, failer) => {
  //매개변수에 현재값(to), 이전값(from), 다음값(next)이 담긴다.
  console.log(to);
  console.log(from)
  console.log(failer)	// undefined면 성공, 에러에 대한 정보객체가 담겨있으면 실패
});
  • 이 3가지 네비게이션 가드를 통해 해당 페이지에 대한 접근을 깔끔하게 막을 수 있다.
  • 그런데 전역 말고 라우트 하나에만 걸고 싶다면 beforeEnter을 routes 배열의 객체 안에 속성으로 넣을 수 있다.
{
      path: "/about",
      name: "about",
      component: () => import("../views/AboutView.vue"),
      beforeEnter: (_,__,next) => {			// 한 가지 라우트에만 넣어서 사용 가능
        const isAuthenticated = true
        if(!isAuthenticated){
          next("/")
        }
        next()
      }
    },

컴포넌트 내 가드

  • 컴포넌트 내에 이런 네비게이션 가드를 걸 수도 있다.
  • 페이지를 전환할때 동적 세그먼트의 값이 바뀐걸 파악해서 뭘 할 수 있다?(잘 못 들었음)
  • onBeforeRouteLeave랑 onBeforeRouteUpdate를 쓸 수 있다.
<script setup>
import { onBeforeRouteLeave } from "vue-router";

const text = ref("");

onBeforeRouteLeave((to, from) => {
  console.log("라우트를 이동하기 전");
  console.log(to);
  console.log(from);
  if (text.value) {
    const answer = window.confirm("정말 떠나시겠습니까?");
    if (answer) {
      return true;
    } else {
      return false;
    }
  }
});
onBeforeRouteUpdate((to, from) => {
  console.log("라우트가 업데이트 될 때");
  console.log(to);
  console.log(from);
});
</script>

<template>
  <div>
    <h1>{{ text }}</h1>
    <textarea v-model="text"></textarea>
  </div>
</template>
  • onBeforeRouteUpdate 이게 동적 세그먼트 값(/:id)을 사용자가 막 바꿔서(ex 유료 페이지의 params가 14인데 그걸 사용자가 직접 url에 입력해서 들어가는 경우?) 이동하는걸 걸러주는건가? 아오 어려워

meta속성(사용자 정의 속성)

  • routes 배열내 객체에 meta:{}를 넣으면 페이지에 meta라는 사용자 정의 속성을 만들 수 있다.
  • 네비게이션 가드를 사용할때 파라미터(to, from, next)에 해당 페이지에 meta속성을 볼 수 있는데 거기에 해당 값들이 담겨져 있다.
  • 해당 파라미터에서 .meta를 통해 속성값을 꺼내쓸 수도 있다.
{
      path: "/",
      name: "Home",
      component: HomeView,
      meta: {
        title: "Home"
      }
    },

scroll behavier

  • router에 추가할 수 있다.
  • 다른 페이지로 넘어갈때 위치값을 설정할 수 있다.
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: "/",
      name: "Home",
      component: HomeView,
      meta: {
        title: "Home",
      },
    },
  ],
  scrollBehavior(to, from, savedPosition) {
    return { top: 0 };	// 위치값을 페이지 넘어갈 때 마다 0으로 설정
  },
});
  • 여기서 savedPosition은 뒤로가기를 사용할때 이전 페이지의 위치값을 저장한다.
  • 그래서 이전 페이지의 위치값을 사용할 수도 있다.
  • html에서 링크를 클릭해서 해당 위치로 이동하는 기술을 navlink라고 한다.
  • scroll behavier를 이용해 children 페이지에 좌표를 구하고 그 페이지를 넘어가는 주소를 클릭하면 거기로 넘어가는 로직을 걸어 바로 children의 스크롤 좌표로 이동할 수 있게 만드는거 같음
  • 여기서 a태그를 사용할 수 없으니 RouterLink를 사용하는데 주소를 /가 아닌 /#을 사용해야 한다.
scrollBehavior(to, from, savedPosition) {
    console.log(to.hash);
    if (to.hash) return { el: to.hash };
    return { top: 0 };
  },
  • 스무스 하게 이동시키고 싶다면 behavier: "smooth"를 주면 된다.
scrollBehavior(to, from, savedPosition) {
    console.log(to.hash);
    if (to.hash) return { el: to.hash,  behavier: "smooth"};
    return { top: 0 };
  },

피니아(전역 상태관리)

  • 전역상태관리는 필수로 필요하다.
  • 일반적인 상태관리 방법으론 다른페이지를 오고가면 상태가 저장되지 않고 초기화되기 때문이다.
  • 아예 컴포넌트 자체가 새로 렌더링 되는거니 당연하다.
  • 그래서 여러페이지에 거쳐 상태를 관리하고 싶다면 전역상태관리 라이브러리를 사용해야 한다.
  • vue에선 전역상태관리 라이브러리를 공식적으로 피니아를 권장하고 있다.
  • 피니아는 vue 공식 팀이 만들고 있는 공식 라이브러리라 호환성이 매우 좋다.
  • 사용 방법은 zustand보다도 쉽다고 한다.
  1. 라이브러리를 설치한다.
npm install pinia
  1. main.js에 pinia를 연결해주면 준비 완료
import { createApp } from "vue";
import App from "./App.vue";

const app = createApp(App);
const pinia = createPinia();
app.use(pinia);
app.mount("#app");
  1. stores파일을 만들고 countStore.js파일을 만든다.
  2. 변수명은 use~로 zustand처럼 짓고 defineStore("이름", ()=>)를 사용한다.(이건 setup 스토어방식)
  3. 그냥 평범한 컴포지션 API문법처럼 사용하고 export로 내보내주면 전역상태관리가 된다.
import { defineStore } from "pinia";
import { computed, ref } from "vue";

export const useCountStore = defineStore("countStore", () => {
  const count = ref(0);
  const increment = () => count.value++;
  const doubleCount = computed(() => {
    return count.value * 2;
  });
  return {
    count,
    increment,
    doubleCount,
  };
});
  • 이건 좋네 많이

  • 이제 사용할 컴포넌트로 와서 export한 useCountStore를 풀어주면 끝이다.

<script setup>
import { useCountStore } from "@/stores/countStore";

const countStore = useCountStore();
</script>

<template>
  <h2>About view: {{ countStore.count }}</h2>
  <h2>About view: {{ countStore.doubleCount }}</h2>
  <button @click="countStore.increment">증가</button>
</template>
  • 아주아주 좋다(이것만)
  • zustand처럼 비구조화 할당으로 데이터를 사용하는 방법을 생각할 수 있는데 pinia에서 비구조화 할당을 사용하면 상태의 반응성이 깨져버려 업데이트가 바로 되지 않는다.
  • 반응성을 유지하려면 storeToRefs라는 메서드에 함수를 넣어서 사용하면 된다.
  • 그런데 메서드는 또 여기 넣으면 메서드가 작동을 안하는데 메서드는 반응성과 상관없으니 기존 비구조할당 방법으로 가져오면 된다.
<script setup>
import { useCountStore } from "@/stores/countStore";
import { storeToRefs } from "pinia";

const countStore = useCountStore();
const { count, doubleCount } = storeToRefs(countStore);
const { increment } = countStore;
</script>

<template>
  <h2>About view: {{ count }}</h2>
  <h2>About view: {{ doubleCount }}</h2>
  <button @click="increment">증가</button>
</template>

과제 = props드릴링 방식으로 todo 구현 + pinia 사용으로 todo구현 (둘 다 컴포지션 API 사용)

pinia-plugin-persistance

  • 전역상태관리는 새로고침시 그 내용이 초기화 된다.
  • 피니아에서 제공하는 플러그인을 설치하면 zustand의 persist처럼 전역변수를 계속해서 유지할 수 있게 된다.
  • 피니아 깃허브에 들어가면 라이브러리에 대한 설명이 있다(사용하기도 쉬움)

pinia persist 플러그인 사용하기

  • 피니아를 새로고침해도 데이터가 그대로 남아있게 만드는 플러그인이다.
  • 설치
npm i pinia-plugin-persistedstate
  • 초기세팅
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
  • 사용
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useStore = defineStore(
  'main',
  () => {
    const someState = ref('hello pinia')
    return { someState }
  },
  {
    persist: true,
  },
)

json-server

  • json-server이라는 라이브러리를 설치한다.
  • 0.17.0버전을 설치해야 한다.
npm install json-server@0.17.0 --save-dev
  • package.json파일의 script에 다음과 같은 명령어를 추가한다.
  "scripts": {
    "db": "json-server --watch src/db.json",
    "db:delay": "json-server --watch src/db.json --delay 2000"
  },
  • 루트폴더에 db폴더를 생성하고, db.json파일을 안에 생성하고 다음과 같이 코드를 작성해준다.
{
  "posts": [
    {
      "id": 1,
      "title": "json-server",
      "author": "typicode"
    }
  ],
  "comments": [
    {
      "id": 1,
      "body": "some comment",
      "postId": 1
    }
  ],
  "profile": {
    "name": "typicode"
  }
}
  • 이제 npm run db를 실행하면 db서버가 열린다.
npm run db

-url에 도메인/post로 접근시 db.json의 json 파일이 보이면 성공

  • 이걸로 내 파일에서 가상db를 만들어 API 요청 테스트를 해 볼 수 있다.

GET, POST, PUT, PATCH, DELETE - fetch() 사용

<script setup>
import { onBeforeMount, ref } from "vue";

// GET, POST, PUT/PATCH, DELETE
const posts = ref(null);

onBeforeMount(async () => {
  const res = await fetch(`http://localhost:3000/posts`);
  const data = await res.json();
  posts.value = data;
});

const handlePost = async () => {
  try {
    const res = await fetch(`http://localhost:3000/posts`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        title: "new Data",
        author: "sucoding",
      }),
    });

    if (!res.ok) throw new Error("데이터 전송에 실패했습니다.");
    if (res.ok) {
      const data = await res.json();
      posts.value.push(data);
    }
  } catch (error) {
    console.log(error);
  }
};

const handlePut = async () => {
  try {
    const res = await fetch(`http://localhost:3000/posts/2`, {
      method: "PUT",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        title: "new Data -PUT",
        author: "sucoding -PUT",
      }),
    });

    if (!res.ok) throw new Error("데이터 변경에 실패했습니다.");
    if (res.ok) {
      const data = await res.json();
      posts.value = posts.value.map((post) =>
        post.id === data.id ? { ...data } : post
      );
    }
  } catch (error) {
    console.log(error);
  }
};

const handlePatch = async () => {
  try {
    const res = await fetch(`http://localhost:3000/posts/3`, {
      method: "PATCH",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        title: "new Data -PATCHa",
        author: "sucoding -PATCH",
      }),
    });

    if (!res.ok) throw new Error("데이터 변경에 실패했습니다.");
    if (res.ok) {
      const data = await res.json();
      posts.value = posts.value.map((post) =>
        post.id === data.id ? { ...data } : post
      );
    }
  } catch (error) {
    console.log(error);
  }
};

const handleDelete = async (id) => {
  try {
    const res = await fetch(`http://localhost:3000/posts/${id}`, {
      method: "DELETE",
    });

    if (!res.ok) throw new Error("데이터 삭제에 실패했습니다.");
    if (res.ok) {
      posts.value = posts.value.filter((post) => post.id !== id);
    }
  } catch (error) {
    console.log(error);
  }
};
</script>

<template>
  <div>Fetch Basic</div>
  <button @click="handlePost()">POST</button>
  <button @click="handlePut()">PUT</button>
  <button @click="handlePatch()">PATCH</button>
  <button @click="handleDelete(4)">DELETE</button>
  <pre>{{ posts }}</pre>
</template>

GET, POST, PUT, PATCH, DELETE - axios() 사용

  • 가끔씩 백이 res에 응답값을 담지 않고 보내는 경우가 있는데 그거 없으면 불필요한 api콜을 2번 해야 할 수 있으므로 백엔드에게 요청을 해야한다.

  • axios의 경우 status로 전송되는 데이터의 값(200, 400, 401 등)을 백엔드가 설정해서 보낸다.

  • 백엔드와 status의 숫자값을 상호의논한 후에 사용해야한다.

  • tanstackQuery를 쓰면 여러가지 서버를 관리할 수 있는 기능들이 있어서 대규모 프로젝트에서 axios와 fetch등과 더불어? 쓰는데 워낙 방대한 내용이라 axios와 fetch부터 잘 사용해보자

  • api 사용시 확인해야할 오류

  • 서버에 접근조차 못했을때

  • 서버에 접근했지만 적절한 응답을 못 받을때

딜레이 되는 API요청 처리

  • suspense기능

  • 우선 vue에선 await을 setup script 내에서 자유롭게 사용 가능하다.

const data = await fetch("http://localhost:3000/posts")
  • vue는 현재 실험적으로 비동기 컴포넌트 기능을 제공한다.
  • 다음과 같이 defineAsyncComponent에 대상 컴포넌트의 경로를 import 해온다면 그 컴포넌트를 비동기로 생성한다.
const SuspenseWithComp = defineAsyncComponent(() => {
  import("@/components/SuspenseBasic.vue");
});
  • 각각의 컴포넌트들을 비동기로 만들어주면 전체 페이지를 렌더링 할 경우 데이터를 한번에 불러오지 않고 빨리 완성되는 컴포넌트를 그때그떄 바로 렌더링 시킬 수 있다.

리프레시 토큰은 자바스크립트로 조작 못하게 네트워크에서 관리한다?
로그인 시에 백엔드 개발자가 리프레시토큰이 바로 토큰값에 들어갈 수 있도록 조치해줘야 한다.

로그인 페이지 접근 막는법

  • 리액트에선 레이아웃 라우트라는 기능으로 처리했었다.
  • vue에선 네비게이션 가드로 처리한다.
import { createRouter, createWebHistory } from "vue-router";
import HomeView from "../views/HomeView.vue";
import { useAuthStore } from "@/stores/auth";

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: "/",
      name: "home",
      component: HomeView,
    },
    {
      path: "/login",
      name: "login",
      component: () => import("../views/Login.vue"),
      meta: {
        requireAuth: false,
      },
    },
    {
      path: "/register",
      name: "register",
      component: () => import("../views/RegisterView.vue"),
      meta: {
        requireAuth: false,
      },
    },
    {
      path: "/user",
      name: "user",
      component: () => import("../views/UserView.vue"),
      meta: {
        requireAuth: true,
      },
    },
  ],
});

// 라우터 가드
router.beforeEach((to, _, next) => {
  const authStore = useAuthStore();
  if (to.meta?.requireAuth && !authStore.isLoggedIn) {
    // 너가 접근할 페이지의 requireAuth가 true이고 로그인 되어있지 않다면 로그인 페이지로 이동해라.
    next("/login");
  } else if (to.meta?.requireAuth === false && authStore.isLoggedIn) {
    // 너가 접근할 페이지의 requireAuth가 false이고 로그인 되어있다면 홈홈 페이지로 이동해라.
    next("/");
  }
  next();
});
profile
frontend개발자가 되기 위해 노력합니다.

0개의 댓글