TIL 06-09 Vue와 DRF 결합하기 1 - CORS Policy

김덕협·2026년 6월 9일

TIL

목록 보기
22/43

Vue with DRF 1 - CORS Policy

Vue(프론트엔드)와 DRF(백엔드)를 분리된 서버로 두고 요청·응답을 주고받을 때 마주치는 CORS Policy 문제와 그 해결 과정을 정리한다.


0. 앞으로 진행할 프로젝트

  1. Vue와 DRF 간 기본적인 요청과 응답
  2. Vue와 DRF에서의 인증 시스템
  3. User 커스터마이징

이번 글은 1번, 그중에서도 두 서버를 연결할 때 가장 먼저 만나게 되는 CORS 에 대한 내용이다.

  • DRF 프로젝트: 스켈레톤 프로젝트 django-pjt 제공 → 주석을 해제하며 진행
  • Vue 프로젝트: 스켈레톤 프로젝트 vue-pjt 제공 → 코드를 직접 작성하며 진행

1. DRF 프로젝트 세팅

1-1. Model

# articles/models.py
class Article(models.Model):
    # user = models.ForeignKey(
    #     settings.AUTH_USER_MODEL, on_delete=models.CASCADE
    # )
    title = models.CharField(max_length=100)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
# accounts/models.py
class User(AbstractUser):
    pass

user 필드는 인증 시스템을 다룰 때 사용하므로 지금은 주석 처리된 상태로 둔다.

1-2. URL

# my_api/urls.py
urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/v1/', include('articles.urls')),
    # path('accounts/', include('dj_rest_auth.urls')),
    # path('accounts/signup/', include('dj_rest_auth.registration.urls')),
]
# articles/urls.py
urlpatterns = [
    path('articles/', views.article_list),
    path('articles/<int:article_pk>/', views.article_detail),
]

1-3. Serializer

# articles/serializers.py
class ArticleListSerializer(serializers.ModelSerializer):
    class Meta:
        model = Article
        fields = ('id', 'title', 'content')


class ArticleSerializer(serializers.ModelSerializer):
    class Meta:
        model = Article
        fields = '__all__'
        # read_only_fields = ('user',)
  • ArticleListSerializer: 목록 조회용 (일부 필드만)
  • ArticleSerializer: 단일 조회·생성용 (전체 필드)

1-4. View

# articles/views.py
from rest_framework.response import Response
from rest_framework.decorators import api_view
from rest_framework import status

# permission Decorators
# from rest_framework.decorators import permission_classes
# from rest_framework.permissions import IsAuthenticated

from django.shortcuts import get_object_or_404, get_list_or_404
from .serializers import ArticleListSerializer, ArticleSerializer
from .models import Article
@api_view(['GET', 'POST'])
# @permission_classes([IsAuthenticated])
def article_list(request):
    if request.method == 'GET':
        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)
@api_view(['GET'])
def article_detail(request, article_pk):
    article = get_object_or_404(Article, pk=article_pk)

    if request.method == 'GET':
        serializer = ArticleSerializer(article)
        print(serializer.data)
        return Response(serializer.data)

1-5. settings.py

# my_api/settings.py
INSTALLED_APPS = [
    'articles',
    'accounts',
    'rest_framework',
    # 'rest_framework.authtoken',
    # 'dj_rest_auth',
    # 'corsheaders',
    # 'django.contrib.sites',
    # 'allauth',
    # 'allauth.account',
    # 'allauth.socialaccount',
    # 'dj_rest_auth.registration',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

# REST_FRAMEWORK = {
#     # Authentication
#     'DEFAULT_AUTHENTICATION_CLASSES': [
#         'rest_framework.authentication.TokenAuthentication',
#     ],
#     # permission
#     'DEFAULT_PERMISSION_CLASSES': [
#         'rest_framework.permissions.AllowAny',
#     ],
# }
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    # 'corsheaders.middleware.CorsMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    # 'allauth.account.middleware.AccountMiddleware',
]

# CORS_ALLOWED_ORIGINS = [
#     'http://127.0.0.1:5173',
#     'http://localhost:5173',
# ]

corsheaders 관련 코드는 아직 주석 상태. CORS 에러를 직접 마주한 뒤에 해제할 예정.

1-6. Fixtures & 실행

articles/fixtures/articles.json 에 테스트용 게시글 데이터가 들어있다.

# 가상환경 생성 및 활성화
$ python -m venv venv
$ source venv/Scripts/activate

# 패키지 설치
$ pip install -r requirements.txt

# Migration
$ python manage.py makemigrations
$ python manage.py migrate

# Fixtures 데이터 로드
$ python manage.py loaddata articles.json

# 서버 실행
$ python manage.py runserver

서버 실행 후 http://127.0.0.1:8000/api/v1/articles/ 로 전체 게시글 조회가 정상 동작하는지 확인한다. (Postman으로도 200 OK 확인)


2. Vue 프로젝트 세팅

2-1. 프로젝트 안내

  • 스켈레톤 프로젝트 vue-pjt 제공
  • Vite 사용, PiniaVue Router 추가되어 있음
  • pinia-plugin-persistedstate 설치 및 등록되어 있음

2-2. 컴포넌트 구조

App
├── ArticleView   ── ArticleList ── ArticleListItem
├── DetailView
├── CreateView
├── SignUpView
└── LogInView      (각 View는 Router로 연결)
vue-project/
├── src/
│   ├── components/
│   │   ├── ArticleList.vue
│   │   └── ArticleListItem.vue
│   ├── router/
│   ├── stores/
│   ├── views/
│   │   ├── ArticleView.vue
│   │   ├── CreateView.vue
│   │   ├── DetailView.vue
│   │   ├── LogInView.vue
│   │   └── SignUpView.vue
│   ├── App.vue
│   └── main.js
└── ...

2-3. 스켈레톤 코드

<!-- App.vue -->
<template>
  <header>
    <nav>
    </nav>
  </header>
  <RouterView />
</template>

<script setup>
  import { RouterView } from 'vue-router'
</script>

<style scoped>
</style>
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
// import ArticleView from '@/views/ArticleView.vue'
// import DetailView from '@/views/DetailView.vue'
// import CreateView from '@/views/CreateView.vue'
// import SignUpView from '@/views/SignUpView.vue'
// import LogInView from '@/views/LogInView.vue'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    // {
    //   path: '/',
    //   name: 'ArticleView',
    //   component: ArticleView
    // },
// store/articles.js
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'

export const useArticleStore = defineStore('article', () => {
  return { }
}, { persist: true })
// src/main.js
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'

const app = createApp(App)
const pinia = createPinia()

pinia.use(piniaPluginPersistedstate)
app.use(pinia)
app.use(router)

app.mount('#app')
$ npm install
$ npm run dev

3. 게시글 목록 출력 (임시 데이터)

DRF와 연결하기 전에, 임시 데이터로 화면에 게시글 목록이 출력되는 흐름부터 만든다.

3-1. ArticleView route 주석 해제

// router/index.js
import ArticleView from '@/views/ArticleView.vue'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'ArticleView',
      component: ArticleView
    },
  ]
})
<!-- App.vue -->
<template>
  <header>
    <nav>
      <RouterLink :to="{ name:'ArticleView' }">Articles</RouterLink>
    </nav>
  </header>
  <RouterView />
</template>

<script setup>
  import { RouterView, RouterLink } from 'vue-router'
</script>

3-3. ArticleView에 ArticleList 등록

<!-- views/ArticleView.vue -->
<template>
  <div>
    <h1>Article Page</h1>
    <ArticleList />
  </div>
</template>

<script setup>
  import ArticleList from '@/components/ArticleList.vue'
</script>

3-4. store에 임시 데이터 작성

// store/articles.js
export const useArticleStore = defineStore('article', () => {
  const articles = ref([
    { id: 1, title: 'Article 1', content: 'Content of article 1' },
    { id: 2, title: 'Article 2', content: 'Content of article 2' }
  ])

  return { articles }
}, { persist: true })

3-5. ArticleList에서 목록 출력 (props 전달)

  • store의 articles 데이터를 참조
  • v-for로 하위 컴포넌트(ArticleListItem)에 article 단일 객체를 props로 전달
<!-- components/ArticleList.vue -->
<template>
  <div>
    <h3>Article List</h3>
    <ArticleListItem
      v-for="article in store.articles"
      :key="article.id"
      :article="article"
    />
  </div>
</template>

<script setup>
  import { useArticleStore } from '@/stores/articles'
  import ArticleListItem from '@/components/ArticleListItem.vue'

  const store = useArticleStore()
</script>

3-6. ArticleListItem에서 props 정의 및 출력

<!-- components/ArticleListItem.vue -->
<template>
  <div>
    <h5>{{ article.id }}</h5>
    <p>{{ article.title }}</p>
    <p>{{ article.content }}</p>
    <hr>
  </div>
</template>

<script setup>
  defineProps({
    article: Object
  })
</script>

여기까지 하면 메인 페이지에서 임시 데이터 기반 게시글 목록이 출력된다.


4. DRF와의 요청과 응답 → CORS 에러 발생

이제 임시 데이터를 DRF 서버에 실제 요청해서 받아온 데이터로 대체한다.

4-1. axios 설치 및 store 코드 작성

# Vue 서버 종료 → 설치 → 서버 재실행
$ npm install axios

Axios: Promise 기반의 HTTP 클라이언트 라이브러리
Promise: JavaScript에서 비동기 작업의 결과를 나타내는 객체

// store/articles.js
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
import axios from 'axios'

export const useArticleStore = defineStore('article', () => {
  const articles = ref([])
  const API_URL = 'http://127.0.0.1:8000'
}, { persist: true })

4-2. getArticles 함수 작성

// store/articles.js
export const useArticleStore = defineStore('article', () => {
  const articles = ref([])
  const API_URL = 'http://127.0.0.1:8000'

  const getArticles = function () {
    axios({
      method: 'get',
      url: `${API_URL}/api/v1/articles/`
    })
      .then(res => {
        console.log(res)
        console.log(res.data)
      })
      .catch(err => console.log(err))
  }
  return { articles, API_URL, getArticles }
}, { persist: true })

4-3. 컴포넌트 마운트 시 getArticles 실행

  • ArticleView 컴포넌트가 마운트될 때 getArticles가 실행되도록 한다.
  • 해당 컴포넌트가 렌더링될 때마다 항상 최신 게시글 목록을 불러오기 위함. (lifecycle hook)
<!-- views/ArticleView.vue -->
<script setup>
  import { onMounted } from 'vue'
  import { useArticleStore } from '@/stores/articles'
  import { RouterLink } from 'vue-router'
  import ArticleList from '@/components/ArticleList.vue'

  const store = useArticleStore()

  onMounted(() => {
    store.getArticles()
  })
</script>

4-4. 에러 발생 🚨

Vue와 DRF 서버를 모두 실행한 후 응답 데이터를 확인하면 다음과 같은 에러가 발생한다.

Access to XMLHttpRequest at 'http://127.0.0.1:8000/api/v1/articles/'
from origin 'http://localhost:5173' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

AxiosError {message: 'Network Error', code: 'ERR_NETWORK', ...}

4-5. 그런데 DRF 서버는 정상 응답함 (200 OK)

[..] "GET /api/v1/articles/ HTTP/1.1" 200 216
[..] "GET /api/v1/articles/ HTTP/1.1" 200 216
  • 서버는 응답했으나, 브라우저 측에서 거절한 것.
  • localhost:5173 에서 127.0.0.1:8000/api/v1/articles/ 로의 XMLHttpRequest 접근이 CORS policy에 의해 차단되었기 때문.

5. CORS Policy 개념

5-1. 웹 브라우저의 동일 출처 정책과 보안

  • 기본적으로 웹 브라우저는 같은 출처에서만 요청하는 것을 허용
  • 다른 출처로의 요청은 보안상의 이유로 차단
  • 이는 SOP(동일 출처 정책) 에 의해 다른 출처의 리소스와 상호작용하는 것이 기본적으로 제한됨

5-2. SOP (Same-origin policy, 동일 출처 정책)

동일 출처 정책(SOP)은 '같은 출처에서만 리소스를 자유롭게 공유할 수 있다'는 웹 브라우저의 가장 기본적인 보안 규칙이다. 한 출처에서 실행된 스크립트가 다른 출처의 데이터를 마음대로 읽어오지 못하도록 막아, 악의적인 사이트가 개인 정보를 탈취하는 것을 방지한다.

  • 어떤 출처(Origin)에서 불러온 문서나 스크립트가 다른 출처에서 가져온 리소스와 상호작용하는 것을 제한하는 보안 방식
  • 핵심: "다른 곳에서 가져온 자료는 일단 막는다."
  • 웹 애플리케이션의 도메인이 다른 도메인의 리소스에 접근하는 것을 제어 → 사용자의 개인 정보·데이터 보안 보호, 잠재적 위협 방지
  • 잠재적으로 해로울 수 있는 문서를 분리함으로써 공격받을 수 있는 경로를 줄임

5-3. Origin (출처)

URL의 Protocol(Scheme), Host, Port 를 모두 포함하여 "출처"라고 부른다.

Scheme/Protocol   Host         Port   Path
http://           localhost    :3000  /posts/3
  • 세 영역(Protocol, Host, Port)이 모두 일치할 때만 동일 출처(Same-origin)로 인정
  • Path는 출처 판단에 영향을 주지 않음

http://localhost:3000/articles/3/ 기준 비교 예시

URL결과이유
http://localhost:3000/articles/✅ 성공Path만 다름
http://localhost:3000/comments/3/✅ 성공Path만 다름
https://localhost:3000/articles/3/❌ 실패Protocol 다름
http://localhost:80/articles/3/❌ 실패Port 다름
http://yahuua:3000/articles/3/❌ 실패Host 다름

5-4. CORS Policy의 등장

  • 기본적으로 웹 브라우저는 같은 출처에서만 요청 허용, 다른 출처 요청은 보안상 차단 (SOP)
  • 하지만 현대 웹 애플리케이션은 다양한 출처로부터 리소스를 요청하는 경우가 많기 때문에 CORS 정책이 필요해졌다.
  • CORS: 웹 서버가 리소스에 대한 서로 다른 출처 간 접근을 허용하도록 선택할 수 있는 기능을 제공

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

CORS는 다른 출처의 자원 공유를 허용하기 위해 서버가 발급하는 '허가증'과 같은 정책이다. 서버는 자신의 응답에 "이 출처에서 온 요청은 내 데이터를 읽어가도 좋아"라고 브라우저에게 알려줌으로써, 동일 출처 정책(SOP)을 안전하게 우회하고 서로 다른 서버 간의 통신을 가능하게 만든다.

  • 특정 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제
  • 다른 출처의 리소스를 가져오려면, 이를 제공하는 서버가 브라우저에게 "다른 출처지만 접근해도 된다"는 사실을 알려야 함

5-6. CORS Policy 정리

  • 다른 출처에서 온 리소스를 공유하는 것에 대한 정책
  • 서버에서 설정되며, 브라우저가 해당 정책을 확인하여 요청이 허용되는지 여부를 결정
  • 다른 출처의 리소스를 불러오려면, 그 다른 출처에서 올바른 CORS header를 포함한 응답을 반환해야 함

6. CORS Headers 설정 (서버에서 해결)

6-1. CORS 적용 방법

  • Server (도메인 B): HTTP Header에 Access-Control-Allow-Origin: 도메인A 가 포함되면, 이제 도메인 A에서의 요청은 서버의 자원에 접근할 수 있음
  • Browser (도메인 A): Server(도메인 B)에 등록됐기 때문에 안전하게 Server(도메인 B)의 데이터를 사용할 수 있음
Browser(도메인 A)  ──── 요청 ────▶  Server(도메인 B)
                  ◀─── 응답 + "HTTP Response Header"
                       (Access-Control-Allow-Origin)

6-2. CORS policy 정리

  • 웹 애플리케이션이 다른 도메인에 있는 리소스에 안전하게 접근할 수 있도록 허용 또는 차단하는 보안 메커니즘
  • 서버가 약속된 CORS Header를 포함하여 응답한다면, 브라우저는 해당 요청을 허용
  • 결론: 서버에서 CORS Header를 만들어야 한다.

6-3. Django에서 CORS Headers 설정하기

Django에서는 django-cors-headers 라이브러리를 활용한다. (손쉽게 응답 객체에 CORS header를 추가해주는 라이브러리)

# requirements.txt로 인해 사전에 설치되어 있음
$ pip install django-cors-headers

settings.py 관련 코드 주석 해제 및 허용할 Vue 프로젝트 Domain 등록

# settings.py
INSTALLED_APPS = ['corsheaders',]

MIDDLEWARE = ['corsheaders.middleware.CorsMiddleware',
    'django.middleware.common.CommonMiddleware',]
# settings.py
CORS_ALLOWED_ORIGINS = [
    'http://127.0.0.1:5173',
    'http://localhost:5173',
]

⚠️ CorsMiddleware는 가능한 한 위쪽(특히 CommonMiddleware보다 위)에 위치시키는 것이 권장된다.

6-4. 응답 데이터 재확인

설정 후 메인 페이지에서 DRF 응답 데이터를 다시 확인하면 정상적으로 게시글 데이터가 출력된다.

개발자 도구 → Network → Fetch/XHR 에서 응답 객체의 Response Headers를 보면:

Access-Control-Allow-Origin: http://localhost:5173

헤더가 포함된 것을 확인할 수 있다. → CORS 문제 해결 완료


7. 게시글 생성/조회 구현

CORS 문제를 해결했으니, 실제 CRUD 동작을 구현한다.

7-1. 전체 게시글 조회 (목록 저장 및 출력)

getArticles의 응답 데이터를 store의 articles에 저장한다.

// store/articles.js
const getArticles = function () {
  axios({
    method: 'get',
    url: `${API_URL}/api/v1/articles/`
  })
    .then(res => {
      articles.value = res.data
    })
    .catch(err => console.log(err))
}
  • 응답 받은 데이터에서 각 게시글의 구성 확인 (id, title, content)
  • store에 저장된 목록이 출력되는지 확인
  • pinia-plugin-persistedstate에 의해 브라우저 Local Storage에 저장됨

7-2. 단일 게시글 조회

(1) DetailView route 주석 해제

// router/index.js
import DetailView from '@/views/DetailView.vue'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'ArticleView',
      component: ArticleView
    },
    {
      path: '/articles/:id',
      name: 'DetailView',
      component: DetailView
    },]
})

(2) ArticleListItem에 DetailView로 가는 RouterLink 작성

<!-- components/ArticleListItem.vue -->
<template>
  <div>
    <h5>{{ article.id }}</h5>
    <p>{{ article.title }}</p>
    <p>{{ article.content }}</p>
    <RouterLink :to="{ name: 'DetailView', params: { id: article.id } }">
      [DETAIL]
    </RouterLink>
    <hr>
  </div>
</template>

<script setup>
  import { RouterLink } from 'vue-router'</script>

(3) DetailView 마운트 시 단일 게시글 조회 AJAX 요청

// views/DetailView.vue (script 부분)
import axios from 'axios'
import { onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { useArticleStore } from '@/stores/articles'

const store = useArticleStore()
const route = useRoute()

onMounted(() => {
  axios({
    method: 'get',
    url: `${store.API_URL}/api/v1/articles/${route.params.id}/`,
  })
    .then((res) => {
      console.log(res.data)
    })
    .catch(err => console.log(err))
})

route.params.id 로 URL의 동적 파라미터(:id)를 가져온다.

(4) 응답 데이터 저장 후 출력

<!-- views/DetailView.vue -->
<template>
  <div>
    <h1>Detail</h1>
    <div v-if="article">
      <p>글 번호 : {{ article.id }}</p>
      <p>제목 : {{ article.title }}</p>
      <p>내용 : {{ article.content }}</p>
      <p>작성시간 : {{ article.created_at }}</p>
      <p>수정시간 : {{ article.updated_at }}</p>
    </div>
  </div>
</template>

<script setup>
import axios from 'axios'
import { onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { useArticleStore } from '@/stores/articles'

const store = useArticleStore()
const route = useRoute()
const article = ref(null)

onMounted(() => {
  axios({
    method: 'get',
    url: `${store.API_URL}/api/v1/articles/${route.params.id}/`,
  })
    .then((res) => {
      article.value = res.data
    })
    .catch(err => console.log(err))
})
</script>

응답 데이터가 도착하기 전에는 articlenull이므로, v-if="article"로 데이터가 있을 때만 렌더링한다.

localhost:5173/articles/1 접속 → 단일 게시글 정보(글 번호/제목/내용/작성·수정 시간)가 정상 출력되면 완료.

7-3. 게시글 작성

(1) CreateView route 주석 해제

// router/index.js
import CreateView from '@/views/CreateView.vue'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [,
    {
      path: '/create',
      name: 'CreateView',
      component: CreateView
    }
  ]
})

(2) ArticleView에 CreateView로 가는 RouterLink 작성

<!-- views/ArticleView.vue -->
<template>
  <div>
    <h1>Article Page</h1>
    <RouterLink :to="{ name:'CreateView' }">
      [CREATE]
    </RouterLink>
    <ArticleList />
  </div>
</template>

<script setup>
  import { onMounted } from 'vue'
  import { useArticleStore } from '@/stores/articles'
  import { RouterLink } from 'vue-router'
  import ArticleList from '@/components/ArticleList.vue'
</script>

(3) v-model로 양방향 바인딩 (.trim으로 공백 제거)

<!-- views/CreateView.vue -->
<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" v-model.trim="content"></textarea><br>
      <input type="submit">
    </form>
  </div>
</template>

<script setup>
  import { ref } from 'vue'

  const title = ref(null)
  const content = ref(null)
</script>

Vue 개발자 도구로 입력값이 title, content ref에 양방향 바인딩되는지 확인 가능.

(4) createArticle 함수 작성

  • 게시글 생성 요청 담당
  • 생성이 성공하면 ArticleView 컴포넌트로 이동시킴
<!-- views/CreateView.vue (script) -->
<script setup>
import axios from 'axios'
import { ref } from 'vue'
import { useArticleStore } from '@/stores/articles'
import { useRouter } from 'vue-router'

const store = useArticleStore()
const router = useRouter()
const title = ref(null)
const content = ref(null)

const createArticle = function () {
  axios({
    method: 'post',
    url: `${store.API_URL}/api/v1/articles/`,
    data: {
      title: title.value,
      content: content.value,
    },
  })
    .then(() => {
      router.push({ name: 'ArticleView' })
    })
    .catch(err => console.log(err))
}
</script>

(5) submit 이벤트 처리

  • submit 이벤트가 발생하면 createArticle 함수를 호출
  • v-on.prevent 수식어를 사용해 submit 이벤트의 기본 동작(새로고침) 취소
<form @submit.prevent="createArticle">

게시글 생성 후 DB(articles_article 테이블)에 새 레코드가 추가된 것을 확인하면 완료.


8. 정리 (핵심 요약)

개념핵심 내용
SOP동일 출처 정책. 같은 출처(Protocol+Host+Port)에서만 리소스 공유 허용 (브라우저 보안 규칙)
OriginURL의 Protocol + Host + Port. 세 가지가 모두 같아야 동일 출처
CORS서로 다른 출처 간 리소스 공유를 허용하는 메커니즘. 서버가 응답 헤더로 허가를 내줌
해결책서버 응답에 Access-Control-Allow-Origin 헤더 포함 → Django는 django-cors-headers 사용

기억할 흐름

  1. Vue(5173)에서 DRF(8000)로 요청 → 출처가 다름
  2. 브라우저가 SOP에 따라 응답을 차단 (CORS policy 에러) — 서버는 200 OK를 보냈지만 브라우저가 거절
  3. 서버(DRF) 에서 django-cors-headers로 허용 출처를 등록하고 CORS Header를 응답에 추가
  4. 브라우저가 헤더를 확인하고 요청 허용 → 정상 통신

결론: CORS 문제는 프론트가 아니라 서버에서 해결한다.

profile
뭘봐

0개의 댓글