1. view 폴더생성

2. 페이지 생성(About.vue, Home.vue, User.vue, UserDetail.vue)

// About.vue 내용 (Home, User, UserDetail 모두 동일)

<template>
About
</template>

 

3. main.js : routing 내용 설정

import { createApp } from 'vue';
import { createWebHistory, createRouter } from 'vue-router';
import App from './App.vue'
// router 쓰일 Component
import Home from './views/Home.vue';
import About from './views/About.vue';
import User from './views/User.vue';
import UserDetail from './views/UserDetail.vue';

//routes 정하기
const routes = [    
  { path : '/', component: Home },
  { path : '/about', component: About },
  { path : '/user', component: User },
  { path : '/user/:id', component: UserDetail },
]

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

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

 

 

4. App.vue 에 링크 생성

<script setup>
import CardList from './components/CardList.vue'
</script>

<template>
  <!-- <CardList/> -->
  <div>
    <router-link to="/">HOME</router-link>
    <router-link to="/about">About</router-link>
    <router-link to="/user">Users</router-link>
  </div>
  <div>
    <router-view></router-view>
  </div>
</template>

<style>
* {
  padding : 0px;
  margin : 0px;
}
</style>

 

[실행결과]

폴더구조

data.json 파일 내용

{
  "hello" : "world"
}

package.json 만드는 방법

npm init --yes

 

[index.js 작성]

파일을 읽는 javascript 파일 시스템 모듈

import fs from 'fs'

// 프로미스를 리턴
import fsPromise from 'fs/promises'

 

콜백함수로 표현

import fs from 'fs';

fs.readFile('data.json', 'utf-8', (err, data) => {
  if(err) {
    console.log(err)
    return
  }
  console.log("callback", data)
});

 

Promise로 표현

import fsPromise from 'fs/promises'

fsPromise.readFile('data.json', 'utf-8')
  .then(data => console.log("promise", data))
  .catch(err => console.log(err));

 

async-await로 표현

import fsPromise from 'fs/promises'

(async() => {
  try {
    const data = await fsPromise.readFile('data.json', 'utf-8')
    console.log("async", data)
  } catch(err) {
    console.log(err)
  }
})();

 

[실행결과 - 동일]

 

콜백지옥표현

import fs from 'fs'

// callback 형식의 함수 (JSON으로 변경하는 함수)
function JSONParser(data, callback) {
  setTimeout(() => {
    try{
      callback(null, JSON.parse(data)) // callback(에러, 데이터)
    }catch(err) {
      callback(err)
    }
  }, 2000)
}

// callback 형식의 함수 (대문자로 변경함수)
function Capitalize(data, callback) {
  setTimeout(() => {
    try{
      callback(null, data.toUpperCase())
    }catch(err) {
      console.log(err)
    }
  }, 2000)
}

// 데이터를 읽고 -> JSON으로 변환 -> 대문자로 변환(콜백지옥 표현식)
fs.readFile('data.json', 'utf-8', (err, data) => {
  if(err) {
    console.log(err)
    return
  }
  JSONParser(data, (err, data)=> {
    if(err) {
      console.log(err)
      return
    }
    Capitalize(data.hello, (err, data) => {
      if(err) {
        console.log(err)
        return
      }
      console.log('callback', data)
    })
  })
});

[실행결과] 

3초 뒤에 콘솔로그 찍힘

 

Promise로 표현

import fsPromise from 'fs/promises'

// 프로미스 리턴하는 함수 (JSON 변환 함수)
function JSONParserPromise(data) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      try{
        resolve(JSON.parse(data))
      }catch(err) {
        reject(Error)
      }
    }, 2000)
  })
}            
// 프로미스 리턴함수 (대문자로 변환 함수)
function CapitalizePromise(data) {
  return new Promise((resolve, reject) => {
    if (typeof data != 'string') {
      return reject(new Error('input is not String'))
    }
    const capitalizedData = data.toUpperCase()
    resolve(capitalizedData)
  })
}
// 프로미스로 연결(데이터 파일 읽기 -> JSON 변환 -> 대문자 변환)
fsPromise.readFile('data.json', 'utf-8')
  .then(data => JSONParserPromise(data))
  .then(data => CapitalizePromise(data.hello))
  .then(data => console.log("promise", data))
  .catch(err => console.log(err));

[실행결과]

3초 뒤에 콘솔로그 찍힘

 

asnyc-awit 으로 표현

import fsPromise from 'fs/promises'

// 프로미스 리턴하는 함수 (JSON 변환 함수)
function JSONParserPromise(data) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      try{
        resolve(JSON.parse(data))
      }catch(err) {
        reject(Error)
      }
    }, 2000)
  })
}            
// 프로미스 리턴함수 (대문자로 변환 함수)
function CapitalizePromise(data) {
  return new Promise((resolve, reject) => {
    if (typeof data != 'string') {
      return reject(new Error('input is not String'))
    }
    const capitalizedData = data.toUpperCase()
    resolve(capitalizedData)
  })
}
// async-await으로 연결(데이터 파일 읽기 -> JSON 변환 -> 대문자 변환)
(async() => {
  try {
    const data = await fsPromise.readFile('data.json', 'utf-8')
    const parsedData = await JSONParserPromise(data)
    const capitalizedData = await CapitalizePromise(parsedData.hello)
    console.log("async", capitalizedData)
  } catch(err) {
    console.log(err)
  }
})();

[실행결과]

3초 뒤에 콘솔로그 찍힘

computed 안에서 지정한 color 값을 css에서 받아와서 표현하기

 

그냥 조건에 따라 color 값을 바꾸고 싶을 때

 

v-bind를 사용한다

 

[예시코드]

<script>
export default {
  props : {
    imgUrl : String,
    name : String,
    birth : String,
  },
  computed : {
    compareBirth() {
      console.log("ComparedBirth invoked in computed")
      if(Date.parse(this.birth) < Date.parse('1970-01-01')) {
        this.color = 'green'
        return 'older'
      }else {
        this.color = 'red'
        return 'younger'
      }
    }
  },
}
</script>

<template>
<div class="card">
  <img :src="imgUrl" alt="profile image" class="card__img">
  <h2 class="card__name">{{ name }}</h2>
  <h2 class="card__birth">{{ birth }}</h2>
  <h2>{{ compareBirth }}</h2>
  <h2>{{ compareBirth }}</h2>
</div>
</template>

<style scoped>
.card {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap : 10px;
  width : 200px;
  height : 250px;
  background-color: aquamarine;
  box-shadow: 1px 1px 10px grey;
}
.card__img {
  width : 100px;
  height: 100px;
  object-fit: cover;
  border-radius: 100%;
  border : solid 2px;
  padding : 5px;
  background-color: white;
}
.card__name{
  font-weight: 700;
}
.card__birth{
  font-weight: 300;
  color : v-bind(color);
}
</style>

 

[실행결과]

1. computed : 뷰에서 제공

- 캐시가 가능함

- 호출시 이전에 계산했던 것을 사용

- console.log 찍힌 횟수를 확인해보자

- 호출시 compareBirth 형태로 호출

<script>
export default {
  props : {
    imgUrl : String,
    name : String,
    birth : String,
  },
  computed : {
    compareBirth() {
      console.log("ComparedBirth invoked in computed")
      if(Date.parse(this.birth) < Date.parse('1970-01-01')) {
        return 'older'
      }else {
        return 'younger'
      }
    }
  },
  // methods : {
  //   compareBirth() {
  //     console.log("ComparedBirth invoked in method")
  //     if(Date.parse(this.birth) < Date.parse('1970-01-01')) {
  //       return 'older'
  //     }else {
  //       return 'younger'
  //     }
  //   }
  // }
}
</script>

<template>
<div class="card">
  <img :src="imgUrl" alt="profile image" class="card__img">
  <h2 class="card__name">{{ name }}</h2>
  <h2 class="card__birth">{{ birth }}</h2>
  <h2>{{ compareBirth }}</h2>
  <h2>{{ compareBirth }}</h2>
</div>
</template>

<style scoped>
.card {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap : 10px;
  width : 200px;
  height : 250px;
  background-color: aquamarine;
  box-shadow: 1px 1px 10px grey;
}
.card__img {
  width : 100px;
  height: 100px;
  object-fit: cover;
  border-radius: 100%;
  border : solid 2px;
  padding : 5px;
  background-color: white;
}
.card__name{
  font-weight: 700;
}
.card__birth{
  font-weight: 300;
}
</style>

[실행결과 (computed)]

 

 

2. methods : 뷰에서 제공

- 캐시가 안됨

- 호출시 매번 연산

- console.log 찍힌 횟수를 확인해보자

- 호출시 compareBirth() 형태로 호출

<script>
export default {
  props : {
    imgUrl : String,
    name : String,
    birth : String,
  },
  // computed : {
  //   compareBirth() {
  //     console.log("ComparedBirth invoked in computed")
  //     if(Date.parse(this.birth) < Date.parse('1970-01-01')) {
  //       return 'older'
  //     }else {
  //       return 'younger'
  //     }
  //   }
  // },
  methods : {
    compareBirth() {
      console.log("ComparedBirth invoked in method")
      if(Date.parse(this.birth) < Date.parse('1970-01-01')) {
        return 'older'
      }else {
        return 'younger'
      }
    }
  }
}
</script>

<template>
<div class="card">
  <img :src="imgUrl" alt="profile image" class="card__img">
  <h2 class="card__name">{{ name }}</h2>
  <h2 class="card__birth">{{ birth }}</h2>
  <h2>{{ compareBirth() }}</h2>
  <h2>{{ compareBirth() }}</h2>
</div>
</template>

<style scoped>
.card {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap : 10px;
  width : 200px;
  height : 250px;
  background-color: aquamarine;
  box-shadow: 1px 1px 10px grey;
}
.card__img {
  width : 100px;
  height: 100px;
  object-fit: cover;
  border-radius: 100%;
  border : solid 2px;
  padding : 5px;
  background-color: white;
}
.card__name{
  font-weight: 700;
}
.card__birth{
  font-weight: 300;
}
</style>

 

[실행결과(methods)]

헤더란? HTTP 통신의 일부

 

[공통 헤더]

요청 과 응답에 모두 사용되는 헤더, Content 시리즈는 엔티티 헤더라고 불린다.

(예시 사진에 안나와 있는 헤더도 있음)

  • Date : HTTP 메시지가 만들어진 시각, 자동으로 생성됨
  • Connection : HTTP/2 -> 없음, HTTP/1.1 -> keep-alive
  • Cache-Control : 캐시 관련 헤더***********(매우 중요 - 웹성능을 최대로 높이기위해, 원하는 시점에 캐싱을 하고, 지우고가 중요, 불필요한 통신을 하지 않도록), 아래 옵션들 속성해서 사용 가능
    • no-store  : 아무것도 캐싱하지 않음, 저장하지 않음
    • no-cache : 모든 캐시를 쓰기 전에 서버에 이 캐시 써도 돼? 물어봐라 라는 뜻 = max-age:0으로 설정한 경우 / 캐시를 저장은 하지만, 바로 만료가 된 상태라서, 재검증이 필요함
      • Pragma : 동일 역할의 HTTP1.0의 헤더
      • Pragma : no-cache  = Cache-Control : no-cache 와 동일, 캐시가 캐시복사본을 릴리즈 하기 전에 원격 서버로 요청을 날려서 유효성 검사를 받도록 한다. 
    • must-revalidate : 만료된 캐시만 서버에 확인 받으렴
    • public, private : public -> CDN 같은 중간서버포함 모든사람이 캐시가능 / private -> 브라우저만(사람) 캐시가능
    • s-maxage: CDN같은 중간서버에만 적용되는 캐시 유효시간
    • public, max-age = 3600 : 캐시 유효시간, 초단위 -> Expires 헤더로도 설정가능

StatusCode에서 (from memory cache) 확인 -&gt; max-age 만료전에 요청시 (이미지 출처: 토스테크블로그)

  • Content-length : 요청과 응답 메시지의 본문의 크기를 바이트 단위로 표시, 자동으로 생성됨
  • Content-Type : 컨텐츠타입; 문자열인코딩 명시, 응답의 Accept 헤더 시리즈와 대응됨,
    • Content-Type : text/html; charset=utf-8
      • 최초 페이지 로드시(html 문서를 읽어올 때)

 

  • Content-Type : application/json; charset-UTF-8

요청 헤더와 전송 정보
응답 헤더와 응답 정보

 

  • Content-Type : multipart/form-data; boundardy=hello -> 모든 문자를 인코딩 하지 않음, hello라는 문자열을 기준으로 전송되는 파일의 데이터를 자름, 바이너리 파일 전송시 사용
    • -- 로 시작하는 문자열인 boundary로 구분되는 서로 다른 파트들로 구성
// Ant-Design을 사용한 Form 소스코드, encType에 multipart/form-data로 명시
// content와 image를 전송하는 폼

<Form style={{ margin : '10px 0 20px'}} encType='multipart/form-data' onFinish ={onSubmit}>
    <Input.TextArea
        value={text}
        onChange={onChangeText}
        maxLength={140}
        placeholder="어떤 일이 있었나요?"
    />
    <div>
        <input type="file" name="image" multiple hidden ref={imageInput} onChange={onChangeImages}/>
        <Button onClick={onClickImageUpload} >이미지 업로드</Button>
        <Button type='primary' style={{ float: 'right'}} htmlType='submit' loading={addPostLoading}>짹짹저장</Button>
    </div>
    {/* 이미지 미리보기 */}
    <div>
        {imagePaths.map((v, i)=>(
            <div key={v} style= {{ display: 'inline-block'}}>
                <img src={`http://localhost:3065/${v}`} style={{width:'200px'}} alt={v}/> 
                <div>
                    <Button onClick={onRemoveImage(i)}>제거</Button>
                </div>
            </div>
        ))}
    </div>
</Form>

multipart/form-data 헤더와 페이로드에서 boundary의 역할

  • Content-Type : application/x-www-form-urlencoded; charset=UTF-8 -> default 값으로, 모든 문자들을 서버로 보내기 전에 인코딩 됨, 일반적인 html form태그, 바디 형태 : key=value&key2=value2
// 예시코드 "/test" 라는 없는 url로 전송 테스트

<form action="/test" method="post">
    <div>
        <input type="text" name="name" id="name" required />
    </div>
    <div>
        <input type="text" name="email" id="email" required />
    </div>
    <div>
        <input type="submit" value="Subscribe!" />
    </div>
</form>

헤더와 페이로드 데이터 형식

  • 둘다 form 형태이지만, x-www-form-urlencoded는 대용량 바이너리 데이터 전송을 하기에 비효율적이라서 대부분 첨부파일은 multipart/form-data 사용
  • Content-language : 사용자의 언어
  • Content-Encoding : 압축 방식 (ex) gzip, deflate), 브라우저가 알아서 해제해서 사용한다. 컨텐츠 용량이 줄어들기 때문에 권장, 요청이나 응답 전송속도 향상, 데이터 소모량도 감소
  • Expires : 응답 컨텐츠 만료 시간, Cache-Control : max-age 가 있는 경우 이 헤더는 무시된다.
  • ETag : HTTP 컨텐츠가 바뀌었는지 검사할 수 있는 태그, 같은 주소의 자원이더라도 컨텐츠가 달라지면 ETag가 바뀐다.

 

 

 

[요청헤더]

(예시 사진에 안나와 있는 헤더도 있음)

  • Host : 서버의 도메인네임:포트
  • User-Agent : 현재 사용자가 어떤 클라이언트로 요청을 보냈는지(운영체제 + 브라우저 정보 담겨있음)
  • Accept 시리즈 : 요청시 이런 타입의 데이터를 보내주렴 하고 명시하는 것,
    • Accept : text/html
    • Accept : image/png, image/gif
    • Accept : text/*
    • Accept-Charset : utf-8
    • Accept-Language : ko, en-US,
    • Accept-Encoding : br, gzip, deflate
  • Authentication : 토큰종류(Basic/Bearer) + 토큰문자열
  • Origin : POST 요청시 요청이 어느 주소에서 시작된 것인지, 요청을 보낸 주소와 받는 주소가 다르면 CORS문제 발생
  • Referer : 이전 페이지의 주소, 통계분석에 많이 사용된다, 어디로부터 우리 페이지에 접속했는지?
  • If-None-Match : 서버보고 ETag가 달라졌는지 확인해보고, 달라졌으면 새로 보내달라는 뜻, ETag 가 같으면 서버는 304 Not Modifed를 응답하여 캐시를 그대로 사용
  • If-Modified-Since : 캐시된 리소스의 Last-Modified 값 이후에 서버 리소스가 수정되었는지 확인한다.
    • 만료된 캐시의 경우 전체 새로 받아오는 것이 아니라, 수정이 있었는지만 판단한다
    • ETag -> If-None-Match & Last-Modified -> If-Modified-Since 판단하여 변화없으면 304 로 적은 리소스 통신, 변화 있으면 새로 내려주고 성공시 200 (많은/정상적 리소스 통신)

이미지 출처 : 토스테크블로그

  • Cookie : 반대로 클라이언트가 서버한테 쿠키를 보낼때는 여기에 담아서 요청을 보냄, 서버는 이 쿠키값을 파싱해서 사용한다. CSRF 공격같은 것을 막기 위해서 반드시 서버는 쿠키가 제대로 된 상황에서 온 것인지를 확인하는 로직을 갖춰야한다.

 

(배경지식) 캐시의 위치

- 캐시가 저장되는 곳은 여러곳 이다. 사용자의 브라우저 일 수 도 있고, 중간 서버를 사용하는 경우는 중간 서버에도 캐시가 될 수 있다. 

- 서비스를 제공하는 입장에서는, 브라우저 캐시기준, 중간서버 캐시기준을 각각 따로 민감하게 설정할 필요가 있다.

- CDN Invalidation : 중간서버에 있는 캐시를 없애는 작업. 

  - max-age = 0 으로 사용자 브라우저는 매번 요청마다 리소스에 변화가 있었는지 재검증을 받는다

  - s-maxage = 31536000 으로 중간서버는 1년(대략)에 한번씩 재검증을 받는다. 대신 그 사이에 서비스 새로운 배포가     있었다면 CDN Invalidation 을 발생시켜 CDN이 새로운 버전을 캐시해 두도록 한다.

이미지출처 : 토스테크블로그

 

[응답 헤더]

  • Access-Control-Allow-Origin : 프론트주소와 백엔드 주소가 다르면 CORS 에러발생, 서버에서 이 헤더에 프론트 주소를 적어주어야 에러가 나지 않는다. CORS 요청시 OPTIONS 주소로 서버가 CORS를 허용하는지 물어본다. 이때 아래의 request-allow 짝꿍이 맞으면 CORS 요청이 이루어진다.
    • Access-Control-Request-Method, : 실제로 보내려는 메서드를 알림
    • Access-Control-Request-Headers, : 실제로 보내려는 헤더를 알림
    • Access-Control-Allow-Methods, : 받아도 되는 메서드 명시
    • Access-Control-Allow-Headers : 받아도 되는 헤더 명시 
  • Allow : Access-Control-Allow-Method 와 비슷하지만, CORS 요청 외에도 적용된다는 데 차이가 있다.
  • Content-Disposition : 응답 본문을 브라우저가 어떻게 표시해야하 할지 알려주는 헤더, inline은 웹페이지 화면에 표시되고, attechment인 경우 다운로드한다.
  • Location : 300번대 응답이나, 201 created 응답일때 어느 페이지로 이동할지 알려주는 헤더
  • Content-Security-Policy : 다른 외부 파일들을 불러오는 경우, 차단할 소스와 불러올 소스를 명시, 
    • Content-Security-Policy: default-src 'self' :self로 지정하면 자신의 도메인의 파일들만 가능
    • Content-Security-Policy: default-src https: :https로 지정시 https 통해서만 가져올 수 있음
    • Content-Security-Policy: default-src 'none' : 외부 파일 가져오기 불가
  • Set-Cookie : 키=밸류, 서버에서 클라이언트(브라우저) 한테 어떤 쿠키를 저장할지 명령
    • ex) Set-Cookie: zerocho=babo; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly

 

Content-Type : application/x-www-form-urlencoded
Content-Type : multipart/form-data
두개의 차이점은?

form 데이터를 전송한다는 맥락은 동일,
application/x-www-form-urlencoded는 default 값으로 html <form> 태그를 사용한 제출시 사용되는 content-type 이다. 전송되는 데이터의 형태는 키=밸류&키=밸류 

multipart/form-data는 이미지파일과같은 바이너리 파일들을 함께 전송할때 사용
boundary의 역할은 위에서 & 랑 비슷, 각각의 요소들의 바운더리를 의미함.

 

 

참고

https://toss.tech/article/smart-web-service-cache

 

웹 서비스 캐시 똑똑하게 다루기

웹 성능을 위해 꼭 필요한 캐시, 제대로 설정하기 쉽지 않습니다. 토스 프론트엔드 챕터에서 올바르게 캐시를 설정하기 위한 노하우를 공유합니다.

toss.tech

https://devowen.com/275

 

[TroubleShooting] Content-type: x-www-form-urlencoded에서 POST 데이터 보내기

오늘은 최근에 내가 개발을 하면서 몇 시간동안 고민한 이슈에 대한 정리를 해 보려고 한다. 이 이슈는 내가 어떤 회사에 입사하기 위해 치뤘던 코딩 과제를 하는 도중에 발생하였다. 다행히도

devowen.com

https://neph3779.github.io/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC/HTTPBasicWithSwift/

 

HTTP 기본 개념 정리 with swift - Neph's iOS blog

HTTP란?

neph3779.github.io

 

1. 프로젝트 내에 components/Card.vue 파일 생성

# components/Card.vue

<script>
export default {
  props : {
    imgUrl : String,
    name : String,
    description : String,
  }
}
</script>

<template>
<div class="card">
  <img :src="imgUrl" alt="profile image" class="card__img">
  <h2 class="card__name">{{ name }}</h2>
  <h2 class="card__description">{{ description }}</h2>
</div>
</template>

<style scoped>
.card {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap : 10px;
  width : 200px;
  height : 250px;
  background-color: aquamarine;
  box-shadow: 1px 1px 10px grey;
}
.card__img {
  width : 100px;
  height: 100px;
  object-fit: cover;
  border-radius: 100%;
  border : solid 2px;
  padding : 5px;
  background-color: white;
}
.card__name{
  font-weight: 700;
}
.card__description{
  font-weight: 300;
}
</style>

 

2. 프로젝트 내에 components/CardList.vue 파일 생성

# components/CardList.vue

<script>
import Card from "./Card.vue"
export default {
  data() {
    return {
      heros: [
        {
          id : 1,
          imgUrl : 'loki.jpg',
          name : "loki",
          description : 'Younger Brother'
        },
        {
          id : 2,
          imgUrl : 'thor.jpg',
          name : "thor",
          description : 'Older Brother'
        }
      ]
    }
  },
  components : { Card }
}
</script>

<template>
  <div class="card-list">
    <Card
      v-for="hero of heros"
      :imgUrl="hero.imgUrl"
      :name="hero.name"
      :description="hero.description"
      :key="hero.id"
    ></Card>
  </div>
</template>

<style>
.card-list{
  display: flex;
  gap : 15px;
}
</style>

 

3. 최상위 컴포넌트 App.vue 수정

# App.vue
<script setup>
import CardList from './components/CardList.vue'
</script>

<template>
  <CardList/>
</template>

<style>
* {
  padding : 0px;
  margin : 0px;
}
</style>

 

4. 원하는 이미지 파일 넣기(loki.png, thor.png)

/public 폴더 밑에 이미지 파일 추가

5. 최종 파일트리

6. dev 서버 돌리기

npm run dev

 

[실행 결과]

https://vitejs-kr.github.io/guide/

 

시작하기 | Vite

시작하기 들어가기 전에 Vite(프랑스어로 "빠르다(Quick)"를 의미하며, 발음은 "veet"와 비슷한 /vit/ 입니다.)은 빠르고 간결한 모던 웹 프로젝트 개발 경험에 초점을 맞춰 탄생한 빌드 도구이며, 두

vitejs-kr.github.io

vite로 시작하기 (Vue 에서 권장)

$ npm create vite
npx: 6개의 패키지를 3.697초만에 설치했습니다.
√ Project name: ... Vue-Study
√ Package name: ... vue-study
√ Select a framework: » vue
√ Select a variant: » vue

Scaffolding project in C:\workspace\vuejs\Vue-Study...

Done. Now run:

  cd Vue-Study
  npm install
  npm run dev

 

VSCODE extension 설치

 

 

설치 후 dir 구조

 

1. components 안에 필요한 컴포넌트들을 정의한다. => HelloWorld.vue

2. App.vue => 최상위 부모 컴포넌트

3. main.js => App.vue를 html에 마운트

4. index.html => 정적 html 문서

 

dev 서버 돌려보기

npm install
npm run dev

 

개발 이후 build

$ npm run build

> vue-study@0.0.0 build C:\vuejs\Vue-Study
> vite build

vite v2.8.6 building for production...
✓ 14 modules transformed.
dist/assets/logo.03d6d6da.png    6.69 KiB
dist/index.html                  0.48 KiB
dist/assets/index.e9286135.js    1.87 KiB / gzip: 1.00 KiB
dist/assets/index.16c4fe9c.css   0.20 KiB / gzip: 0.17 KiB
dist/assets/vendor.65715d52.js   50.24 KiB / gzip: 20.19 KiB

새로 생긴 폴더 구조 (dist)

bundler가 만들어준다.

왜?

1) 사이즈를 줄이기 위해

2) 커넥션 수를 줄이기 위해 (파일 여러개를 하나로)

3) index / vendor 나눌 수 있다.

  - index : 내가 짠 코드들이 들어가 있음, 바뀌는 경우가 많다. 

  - vendor : 사용한 패키지들, 바뀌는 경우가 별로 없음 -> 캐시를 적용 가능

코드 수정 후 다시 build 하면 해시값이 바뀌는 것을 확인할 수 있다. (App.vue 파일만 수정했음)

브라우저에서 캐시(저장)해둔 값과 달라진 경우만 다운로드

 

1. v-for 로 map이나 forEach 역할을 할 수 있다.

<Card
    v-for="person of people"
	v-bind:key="person.id"
    v-bind:name="person.name"
    v-bind:hobby="person.hobby"
  />

2. Card 컴포넌트 구현

 const Card = {
  props: {
    name: String,
    hobby: String,
  },
  template: `
    <h1>{{ name }}</h1>
    <h2>{{ hobby }}</h2>
  `,
};

3. 부모App에서 Card 컴포넌트 사용

const App = {
  data() {
    return {
      people: [
        {
          id: 1,
          name: 'Wendy',
          hobby: '자전거',
        },
        {
          id: 2,
          name: '박유진',
          hobby: '노래',
        },
        {
          id: 3,
          name: '한수빈',
          hobby: '독서',
        },
      ],
    };
  },
  components: {
    Card,
  },
  methods: {
    addPerson() {
      this.people.push({
        id: 4,
        name: '장원영',
        hobby: '춤',
      });
    },
  },
};

 

[전체 예시 코드]

<!DOCTYPE html>
<html>
  <head>
    <title>Example 05</title>
  </head>
  <body>
    <div id="app">
      <Card
        v-for="person of people"
        v-bind:name="person.name"
        v-bind:hobby="person.hobby"
      />
    </div>
  </body>

  <script src="https://unpkg.com/vue"></script>
  <script>
    const Card = {
      props: {
        name: String,
        hobby: String,
      },
      template: `
        <h1>{{ name }}</h1>
        <h2>{{ hobby }}</h2>
      `,
    };

    const App = {
      data() {
        return {
          people: [
            {
              id: 1,
              name: 'Wendy',
              hobby: '자전거',
            },
            {
              id: 2,
              name: '박유진',
              hobby: '노래',
            },
            {
              id: 3,
              name: '한수빈',
              hobby: '독서',
            },
          ],
        };
      },
      components: {
        Card,
      },
      methods: {
        addPerson() {
          this.people.push({
            id: 4,
            name: '장원영',
            hobby: '춤',
          });
        },
      },
    };
    Vue.createApp(App).mount('#app');
  </script>
</html>

 

[실행 결과]

1. 컴포넌트들의 생명 주기별로 콘솔을 찍어본다. -> hook 으로 각각 콘솔 찍어보기 가능

const Display = {
        props: {
          emoji: String,
        },
        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');
        },
        template: `
          <h1>{{ emoji }}</h1>`,
      };

 

2. v-if 속성 사용

boolean 값으로 컴포넌트를 display / undisplay 한다.

const Toggle = {
        data() {
          return {
            isOn: true,
            emoji: '🌙',
          };
        },
        components: {
          Display,
        },
        template: `
        <div v-if=isOn><Display :emoji="emoji" /></div>
        <button @click="emoji='☀️'">sun</button>
        <button @click="isOn=!isOn">toggle</button>`,
      };

 

[전체 예시 코드]

<!DOCTYPE html>
<html>
  <head> </head>
  <body>
    <div id="app"></div>
    <script src="https://unpkg.com/vue@next"></script>
    <script>
      const Display = {
        props: {
          emoji: String,
        },
        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');
        },
        template: `
          <h1>{{ emoji }}</h1>`,
      };

      const Toggle = {
        data() {
          return {
            isOn: true,
            emoji: '🌙',
          };
        },
        components: {
          Display,
        },
        template: `
        <div v-if=isOn><Display :emoji="emoji" /></div>
        <button @click="emoji='☀️'">sun</button>
        <button @click="isOn=!isOn">toggle</button>`,
      };

      Vue.createApp(Toggle).mount('#app');
    </script>
  </body>
</html>

 

[실행 결과]

1. 최초 랜더링

2. Sun 버튼 클릭

3. toggle 버튼 클릭

부모 - 자식 관계의 컴포넌트 구현

 

1. 자식 요소를 태그로 사용 가능

v-bind -> : 으로 축약표현

왜? bind를 하면 Vue에서 정의한 데이터를 찾음 -> count 라는 데이터(Parent에서 정의) 를 찾아서 Child에게 props로 넘김

<body>
    <div id="app">
      <Child :something="count" />
    </div>
</body>

2. 자식 요소 생성 (순서 중요 : 자식먼저 작성)

<script src="https://unpkg.com/vue"></script>
  <script>
    const Child = {
      props: { somthing: Number },
      data() {
        return {
          color: 'red',
        };
      },
      methods: {
        changeColor: function () {
          if (this.color === 'red') return (this.color = 'blue');
          this.color = 'red';
        },
      },
      template: `
        <h1 v-bind:style="{ color : color }"> Child Componont </h1>
        <h2> {{ something }} </h2>
        <button @click="changeColor">Switch Color</button>
        `,
    };

3. 부모 요소 생성

const Parent = {
      data() {
        return {
          count: 0,
        };
      },
      components: {
        Child,
      },
    };

    Vue.createApp(Parent).mount('#app');
  </script>

 

 

전체 예시 코드

<!DOCTYPE html>
<html>
  <head>
    <title>Example 03</title>
  </head>
  <body>
    <div id="app">
      <Child :something="test" />
    </div>
  </body>

  <script src="https://unpkg.com/vue"></script>
  <script>
    const Child = {
      props: { somthing: Number },
      data() {
        return {
          color: 'red',
        };
      },
      methods: {
        changeColor: function () {
          if (this.color === 'red') return (this.color = 'blue');
          this.color = 'red';
        },
      },
      template: `
        <h1 v-bind:style="{ color : color }"> Child Componont </h1>
        <h2> {{ something }} </h2>
        <button @click="changeColor">Switch Color</button>
        `,
    };

    const Parent = {
      data() {
        return {
          count: 0,
        };
      },
      components: {
        Child,
      },
    };

    Vue.createApp(Parent).mount('#app');
  </script>
</html>

 

실행 결과

 

+ Recent posts