서버(server)

클라이언트에게 정보와 서비스를 제공하는 컴퓨터 시스템

서비스 전체를 제공 == Django Web Service

Django를 통해 전달받은 HTML에는 하나의 웹페이지에 구성할 수 있는 모든 데이터가 포함되어 있다. 즉, 서버에서 모든 내용을 렌더링하고 하나의 HTML 파일로 제공한다.

정보를 포함한 web 서비스를 구성하는 모든 내용을 서버측에서 제공한다.

 

정보를 제공 == DRF API Service

Django를 통해 관리하는 정보만을 클라이언트에게 제공한다.

DRF를 사용하여 JSON으로 변환한다.

 

 

클라이언트(Client)

Server가 제공하는 서비스에 적절한 요청을 통해 Server로 부터 반환받은 응답을 사용자에게 표현하는 기능을 가진 프로그램 혹은 시스템

Server가 제공하는 서비스에 적절한 요청한다. Server는 정상적인 요청에 적합한 응답을 제공한다.

Server로 부터 반환받은 응답을 사용자에게 표현한다. 사용자의 요청에 적합한 data를 server에 요청하여 응답받은 결과로 적절한 화면을 구성한다.

 

 

 

 

 

 

Vue with DRF

$ python -m venv venv

$ source venv/Scripts/activate

$ pip install -r requirements.txt

$ python manage.py migrate

$ python manage.py loaddata articles.json comments.json

$ python manage.py runserver

back-server

가상환경 생성, 실행

requirements 설치

migrate, loaddata

run server

 

 

$ npm install

$ npm run serve

front-server

npm install

run server

 

 

 

 

Article Read

const API_URL = 'http://127.0.0.1:8000/'

store - index.js

 

 

  created() {
    this.getArticles()
  },
  methods: {
    getArticles(){
      this.$store.dispatch('getArticles')
    }
  }

views - ArticleView.vue

 

 

 

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

Vue.use(Vuex)

const API_URL = 'http://127.0.0.1:8000'

export default new Vuex.Store({
  state: {
    articles: [
      {
        id: 1,
        title: '제목',
        content: '내용'
      },
      {
        id: 2,
        title: '제목2',
        content: '내용2'
      },
    ],
  },
  getters: {
  },
  mutations: {
  },
  actions: {
      getArticles(context){
        axios({
          method : 'get',
          url : `${API_URL}/api/v1/articles/`
        })
        .then((response)=>{
          console.log(response, context)
        })
        .catch((error)=>{
          console.log(error)
        })
      }
    },
  modules: {
  }
})

store - index.js

 

 

 

양쪽 서버 실행하고 main 켰을때, 

back (server)쪽에서는 200으로 정상작동하였다.

 

 

front 쪽에서는 응답을 받지 못했다.

CORS 정책에 의해 막혔다는 에러가 뜬다.

 

 

 

브라우저가 요청을 보내고 서버의 응답이 브라우저에 도착했다.

서버의 log는 200 정상 반환으로 server는 정상적으로 응답했지만 브라우저가 막은 것이다.

보안상의 이유로 브라우저는 동일출처정책(SOP)에 의해 다른 출처의 리소스와 상호작용 하는 것을 제한한다.

 

 

SOP(Same - Origin Policy)

동일 출처 정책

불러온 문서나 스크립트가 다른 출처에서 가져온 리소스와 상호작용 하는 것을 제한하는 보안 방식

잠재적으로 해로울 수 있는 문서를 분리함으로써 공격받을 수 있는 경로를 줄인다.

 

 

Origin - 출처 

URL의 protocol, Host, Port를 모두 포함하여 출처라고 부른다.

 

Same Origin 예시

protocol, Host, Port 세 영역이 일치하는 경우에만 동일 출처로 인정한다.

 

 

 

CORS (Cross-Origin Resource Sharing) - 교차 출처 리소스 공유

추가 HTTP Header를 사용하여 특정 출처에서 실행 중인 웹 어플리케이션이 다른 출처의 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제

어떤 출처에서 자신의 컨텐츠를 불러갈 수 있는지 서버에 지정할 수 있는 방법이다.

리소스가 자신의 출처와 다를 때 교차 출처 HTTP 요청을 실행한다.

만약 다른 출처의 리소스를 가져오기 위해서는 이를 제공하는 서버가 브라우저에게 다른 출처지만 접근해도 된다는 사실을 알려야 한다.

 

 

CORS policy - 교차 출처 리소스 공유 정책

다른 출처에서 온 리소스를 공유하는 것에 대한 정책

CORS policy에 위배되는 경우 브라우저에서 해당 응답 결과를 사용하지 않는다.

sesrver에서 응답을 주더라도 브라우저에서 거절한다.

다른 출처의 리소르를 불러오려면 그 출처에서 올바른 CORS header를 포함한 응답을 반환해야 한다.

 

 

CORS 표준에 의해 추가된 HTTP Response Header를 통해 이를 통제 가능하다.

 

 

 

HTTP Response Header

Access-Control-Allow-Origin

Access-Control-Allow-Credentials

Access-Control-Allow-Headers

Access-Control-Allow-Methods

 

 

 

Access-Control-Allow-Origin

단일 출처를 지정하여 브라우저가 해당 출처가 리소스에 접근하도록 허용한다.

 

 

django-cors-headers library

https://github.com/adamchainz/django-cors-headers 

 

GitHub - adamchainz/django-cors-headers: Django app for handling the server headers required for Cross-Origin Resource Sharing (

Django app for handling the server headers required for Cross-Origin Resource Sharing (CORS) - GitHub - adamchainz/django-cors-headers: Django app for handling the server headers required for Cross...

github.com

응답에 CORS header를 추가해주는 라이브러리

다른 출처에서 Django 어플리케이션에 대한 브라우저 내 요청을 허용한다.

 

$ python -m pip install django-cors-headers

$ pip freeze > requirements.txt

django-cors-headers 설치

back(django) 환경에서 설치하고, requirements.txt에 freeze로 추가한다.

 

 

INSTALLED_APPS = [
    ...,
    "corsheaders",
    ...,
]
MIDDLEWARE = [
    ...,
    "corsheaders.middleware.CorsMiddleware",
    "django.middleware.common.CommonMiddleware",
    ...,
]

settings.py의 INSTALLED_APPS과 MIDDLEWARE 에 추가한다.

CorsMiddleware는 가능한 CommonMiddleware보다 먼저 정의되도록 한다.

 

 


이제 허용할 다른 출처를 작성해주어야 한다.

github에 다른 출처를 작성할 세가지 방법이 있다.

 

1. CORS_ALLOWED_ORIGINS: Sequence[str]

배열안에 문자열로 목록을 작성한다.

 

 

예시

CORS_ALLOWED_ORIGINS = [
    "https://example.com",
    "https://sub.example.com",
    "http://localhost:8080",
    "http://127.0.0.1:9000",
]

 

 

2. CORS_ALLOWED_ORIGIN_REGEXES: Sequence[str | Pattern[str]]

정규표현식으로 작성한다.

 

예시

CORS_ALLOWED_ORIGIN_REGEXES = [
    r"^https://\w+\.example\.com$",
]

 

 

3. CORS_ALLOW_ALL_ORIGINS: bool

모든 origin을 허용하고자 할 때 사용한다.

true 하면 모든 출처를 승인하는 것이다. 기본값은 false이다.

 


CORS_ALLOWED_ORIGINS = [
    'http://localhost:8080',
]

첫번째 방법으로 settings.py에 추가했다.

 

 

다시 서버를 키고 확인한다.

 

vue에서도 정상동작 확인했다.

첫번째 Object는 response이고 두번째 Object는 context이다.

 

 

응답에 Access-Control-Allow-Origin 헤더가 있다.

해당 출처가 허용된 것이다.

 

 

 

 

response 구조를 보면 data에 각 게시글 객체가 있다. 게시글은 id, title, content로 구성되어있다.

 

 

 

  mutations: {
    GET_ARTICLES(state,articles){
      state.articles = articles
    }
  },
  actions: {
      getArticles(context){
        axios({
          method : 'get',
          url : `${API_URL}/api/v1/articles/`
        })
        .then((response)=>{
          // console.log(response, context)
          context.commit('GET_ARTICLES', response.data)
        })
        .catch((error)=>{
          console.log(error)
        })
      }
    },
  modules: {
  }

state를 저장하는 일은 mutation이 해야하기 때문에 context.commit()으로 mutation을 호출한다.

response.data가 articles이기 때문에 mutation에 보내준다.

이 데이터를 articles라는 이름으로 인자를 지정하고 state.articles에 받아온 데이터를 저장한다.

 

 

받아온 articles 데이터가 화면에 반영되었다.

 

 

 

Article Create

const routes = [
 
  {
    path: '/create',
    name: 'CreateView',
    component: CreateView
  },

createView 컴포넌트를 만들고 route에 path, name을 지정해주었다.

 

 

 

    path('articles/', views.article_list),
@api_view(['GET', 'POST'])
# @permission_classes([IsAuthenticated])
def article_list(request):
    if request.method == 'GET':
        # articles = Article.objects.all()
        articles = get_list_or_404(Article)
        serializer = ArticleListSerializer(articles, many=True)
        return Response(serializer.data)

    elif request.method == 'POST':
        serializer = ArticleSerializer(data=request.data)
        if serializer.is_valid(raise_exception=True):
            serializer.save()
            # serializer.save(user=request.user)
            return Response(serializer.data, status=status.HTTP_201_CREATED)

django articles/views.py

django에서 api/v1/articles/ 주소의 method가 GET일 경우 article을 조회하고 (위에서 했던 작업)

POST일 경우 article를 만든다.

필요한 주소를 확인했다. (read article과 동일)

 

 

 

<template>
  <div>
    <h1>Article Page</h1>
    <router-link :to="{name : 'CreateView'}">CreateArticle</router-link>
    <ArticleList/>
    <hr>
    <router-view/>
  </div>
</template>

ArticleView.vue에 createArticle로 렌더링 될 router-link를 연결했다.

 

 

 

<template>
  <div>
    <h1>게시글 작성</h1>
    <form @submit.prevent="createArticle">
      <label for="title">제목 : </label>
      <input type="text" id="title" v-model.trim="title"><br>
      <label for="content">내용 : </label>
      <textarea id="content" cols="30" rows="10" v-model="content"></textarea><br>
      <input type="submit" id="submit">
    </form>
  </div>
</template>
  data() {
    return {
      title : null,
      content : null,
    }
  },

views/CreateView.vue

게시글 생성을 위한 form을 제공한다.

v-model.trim으로 사용자 입력 데이터에서 공백을 제거하고, v-model으로 title 데이터와 연결한다.

.prevent를 활용해서 form의 기본 이벤트 동작을 막고 메서드를 실행하도록 한다.

 

 

  methods: {
    createArticle(){
      const title = this.title
      const content = this.content
      if(!title){
        alert('제목 입력하세요')
        return
      } else if (!content){
        alert('내용을 입력해주세요')
        return
      }
      axios({
        method : 'post',
        url : `${API_URL}/api/v1/articles/`,
        data : {title,content}
      })
      .then(()=>{
        this.$router.push({name:'ArticleView'})
      })
      .catch(err => console.log(err))
    }
  }

title, content가 없다면 alert를 통해 경고창을 띄우고 return으로 함수를 종료한다.

axios를 사용해 server에 게시글 생성을 요청한다.

state를 변화시키는 것이 아니고, DB에 게시글 생성후 ArticleView로 이동할 것이기 때문에 actions 대신에 methods에서 직접 처리한다.

 

게시글을 작성하면, db에 저장된다. ArticleView가 create될 때 마다 server에 게시글 전체 데이터를 요청하기 때문에 바로 반영이 된다.

 

지금은 전체 게시글을 요청해야, 새로 생성된 게시글을 확인할 수 있다.

 vuex state를 통해 전체 게시글을 관리하도록 구성한다면, 내가 새롭게 생성한 게시글은 확인할 수 있지만 나 이외의 유저들이 새롭게 생성한 게시글은 어떻게 불러올지 아직 모르기 때문에 비효율적인 부분이 존재한다.

내가 구성하는 서비스에 따라 데이터 관리 방식을 고려해야 한다.

 

 

 

Article Detail

<template>
  <div>
    <h1>Detail</h1>
    <p>글 번호 : {{ article?.id }}</p>
    <p>제목 : {{ article?.title }}</p>
    <p>내용 : {{ article?.content }}</p>
    <p>작성시간 : {{ article?.created_at }}</p>
    <p>수정시간 : {{ article?.updated_at }}</p>
  </div>
</template>

views/DetailView.vue

 

  {
    path: '/:id',
    name: 'DetailView',
    component: DetailView,
  },

router/index.js

DetailView 컴포넌트 만들고, 불러와서 route에 등록한다.

 

 

<template>
  <div>
    <router-link
    :to="{ name : 'DetailView', params: {id:article.id} }">
      DETAIL
    </router-link>
  </div>
</template>

<script>
export default {
  name: 'ArticleListItem',
  props: {
    article : Object
  }
}
</script>

components/ArticleListItem.vue

DetailView는 ArticleListItem에서 확인할 수 있도록 한다.

router-link를 통해 특정게시글의 id 값 (article.id)을 동적인자로 파라미터 id에 전달한다.

 

 

    path('articles/<int:article_pk>/', views.article_detail),

django에서 article detail 주소

 

 

<template>
  <div>
    <h1>Detail</h1>
    <p>글 번호 : {{ article?.id }}</p>
    <p>제목 : {{ article?.title }}</p>
    <p>내용 : {{ article?.content }}</p>
    <p>작성시간 : {{ article?.created_at }}</p>
    <p>수정시간 : {{ article?.updated_at }}</p>
  </div>
</template>

<script>
import axios from 'axios'
const API_URL = 'http://127.0.0.1:8000'

export default {
  name: 'DetailView',
  data() {
    return{
      article:null
    }
  },
  created() {
    this.getArticleDetail()
  },
  methods: {
    getArticleDetail(){
      axios({
        method : 'get',
        url : `${API_URL}/api/v1/articles/${this.$route.params.id}/`
      })
      .then((res) =>{
        this.article = res.data
      })
      .catch(err=>console.log(err))
    }
  }
}
</script>

views/DetailView.vue

this.$route.params를 활용해서 컴포넌트가 create될 때, 넘겨받은 id로 상세정보를 AJAX요청한다.

router-link로 DetailView를 불러올 때 파라미터 id를 함께 보내주는데, this.$route.params.id 으로 가져온 것이다.

created()에 해당 메서드를 실행한다.

 

this.article에 받아온 res의 data를 저장해준다. 정상적으로 화면에 표시된다.

data에 담기까지 시간이 걸리므로 optional chaining(? 표시하기) 을 활용해 데이터를 표기한다.

article?.id로 작성해야 article이 없을 때도 에러가 발생하지 않는다.