: 버튼을 눌렀을 때 서버 응답을 기다리는 순서의 차이

 

비관적 pessimistic 

체크박스 클릭 -> 서버에 저장 요청 -> 응답 옴 -> 전체 다시 조회 -> 그제서야 체크표시 됨

                                     ㄴ --- 이 동안 화면 멈춘 듯, 다른 체크박스도 잠김 

 

서버가 확인해주기 전엔 화면을 안 믿는다는 태도. 안전하지만 매번 왕복을 기다린다.

 

낙관적 optimistic 

체크박스 클릭 -> 화면에 즉시 체크 표시 -> (뒤에서 조용히) 서버에 저장 요청 - 성공하면 그대로 두고, 실패하면 체크를 다시 풀고 에러표기

 

핵심은 : 진실의 원천은 여전히 서버라는 점. 

로컬 화면은 "서버가 이렇게 답할 것" 이라는 예측일 뿐이고, 틀리면 서버 응답으로 교정된다.

데이터가 실제로 어긋나서 저장되는게 아니라, 잠깐 화면이 앞서 보여줄 뿐임.

 

 

나의 걱정 

 

데이터 정합성에 오류가 생길 수 있을까?

 

정합성 자체는 두 방식 다 지켜짐. 서버에 저장되는 값은 어느쪽이든 동일하다. 차이는 오직 "화면이 잠깐 서버보다 앞서가느냐" 뿐임.

지금 내 상황에서는 비관적 방식이 더 안전한게 아니라, 더 느릴뿐임.

 

따라서 이 상황에서 내가 판단해야하는 기준은 잠깐 틀릴 위험 vs  반응속도 의 거래임.

- 틀릴 위험 낮음 : 체크하는 사람은 이미 배정된 작업자이고, 완료 안된 회차라서 서버가 거절할 경우가 드물다. 

- 반응 속도가 중요함 : 현장에서 휴대폰으로, 그것도 원격 Supabase DB 왕복이 곱해지는 환경이라 체감 지연이 크다. 체크리스트는 연속으로 여러 개 누르는 동작이라 더 두드러질 수 있다.

 

이 조합 (위험도가 낮고, 속도가 중요) 에서는 낙관적 업데이트가 정석임.

실제로 체크박스, 좋아요, 토글류는 업계에서 거의 다 이런식으로 개발을 한다고 함.

 

 

 

 

 

1. 필수 라이브러리 설치하기 

 

화면 하단 터미널 버튼 클릭 후, 설치 진행

 

# python 버전 확인 (3.10 이상)
python --version

 

# 랭체인 설치
pip install langchain>=1.0.0

 

# 모델 제공자별 패키지 설치
pip install langchain-openai

 

나는 옛날에 구매했던 API 토큰들이 남아있어서 open ai 로 선택

# 왜 뒤늦게 알려주는지 이해 안되는 한번에 설치하는 권장 설치 명령어
pip install langchain langchain-openai langchain-anthropic langgraph

 

2. 설치 확인

 

Python 코드는 해당 노트? 영역에 입력 후 실행하면 됨.

import langchain
print(f"LangChain 버전: {langchain.__version__}")

 

 

3. 환경 설정 

 

환경변수를 설정한다. API 키 등 민감한 정보는 환경변수로 저장

 

 

환경변수 설정이 잘 드러간 것으로 보임

 

이제 + 코드 부분을 선택해서 코드를 작성하고 실행하면 됨.

토이 프로젝트를 진행하면서 무료 서버 중

오라클 클라우드에서 1OCPU 서버를 얻어서

spring 을 돌리고 있는데 

맨날 도커로 격리된 환경을 사용하다가 app.jar 로 직접 서비스 돌리려니까

모르는게 너무 많다. ㅠㅠ 

 

우선, systemctl 로 돌리는 service 는 환경변수를 어디에 주입해야하는가? 

 

우선 아래 명령어로 현재 서비스가 어떤 env 파일을 읽는지 확인할 수 있다. 

systemctl cat {서비스 이름}

 

또는 EnvironmentFile 경로만 읽으려면 아래 명령어로 확인 가능하다. 

systemctl show {서비스 이름} -p EnvironmentFiles

 

 

어떤 env 파일을 읽는지 보니 EnvironmentFile=/etc/{서비스이름}.env 파일에 환경변수가 기록되고 있음을 확인할 수 있다.

❯ ubuntu@instance:~$ systemctl cat {서비스이름}
  # /etc/systemd/system/{서비스이름}.service
  [Unit]
  Description={서비스이름} Spring Boot Backend
  After=network.target

  [Service]
  User=ubuntu
  EnvironmentFile=/etc/{서비스이름}.env
  ExecStart=/usr/bin/java -Xmx512m -jar /home/ubuntu/app.jar
  SuccessExitStatus=143
  Restart=on-failure
  RestartSec=5

  [Install]
  WantedBy=multi-user.target

음... 파일 위치가 너무 나에게 멀게 느껴지니까 가까운 곳으로 옮기고 싶다.

그리고 /etc 경로는 root 권한의 파일이라 수정할 때마다 sudo 로 접근해야하는 것도 마음에 들지 않는다.

 

/home/ubuntu/{서비스이름}.env 경로에 둬야겠다. 

# 기존 파일을 홈으로 복사 (원본은 검증 전까지 보존)
  sudo cp /etc/{서비스 이름}.env /home/ubuntu/{서비스 이름}.env
  sudo chown ubuntu:ubuntu /home/ubuntu/{서비스 이름}.env
  chmod 600 /home/ubuntu/{서비스 이름}.env        # 시크릿 → 본인만 읽기


sudo 명령어로 옮겼기에 실행 권한은 root 인 걸 알 수 있다.

 

chown 명령어로 소유자, 소유자와 소유그룹을 변경하자.

 

이미 권한은 600으로 ubuntu 만 읽고 쓸 수 있게 되어있으므로 chmod 는 안해줘도 될 것 같음.


파일은 옮겼으니, 이제 system 이 이 환경변수 파일을 읽도록 수정을 해줘야 한다.

 

그 전에 안전하게 백업 먼저 해두자.

# 안전하게 백업 먼저
  sudo cp /etc/systemd/system/{서비스이름}.service /etc/systemd/system/{서비스이름}.service.bak

 

그리고 파일 내용을 수정하는 명령어로 파일을 수정해준다. 새로 옮긴 파일내용으로!

# EnvironmentFile 경로만 한 줄 교체
  sudo sed -i 's#EnvironmentFile=/etc/{서비스이름}.env#EnvironmentFile=/home/ubuntu/{서비스이름}.env#' \
    /etc/systemd/system/{서비스이름}.service

 

 # 바뀐 내용 확인
  systemctl cat {서비스이름} | grep EnvironmentFile
  #  → EnvironmentFile=/home/ubuntu/{서비스이름}.env 나오면 성공

이제 반영 -> 재시작 -> 확인을 해보자

  sudo systemctl daemon-reload
  sudo systemctl restart {서비스이름}
  sleep 5
  sudo systemctl --no-pager status {서비스이름} | head -n 15

active 상태임


검증 후, 옛 파일을 제거한다. 

rm /etc/{서비스이름}.env

끝!

 

소감 : 리눅스 배우고 나서 서버 만지니까 너무 재미있다.

회사에서 최근 RAG 관련 서비스를 고도화하는 작업에 필요해서 RAG를 접하게 되었다. 

RAG의 성능은 LLM 모델의 성능도 중요하지만, 무엇보다 opensearch 검색엔진 성능에 많은 것이 달렸다는 생각이 들어서 youtube로 Elasitcsearch 강의를 들었던 내용을 정리하고 있다. 

 

* Elasticsearch? opensearch ?

: Elasticsearch 의 무료 오픈소스 버전이 opensearch 라고 생각하면 될 거 같다.

 

인덱스 생성

보통은 인덱스를 생성하면서 데이터 저장/검색 동작 방식 +  스키마 정의까지 함께 설정하는 것 같다. 

# 인덱스 생성 + 매핑 정의 + Custom Analyzer 적용
PUT /boards
{
  "settings" : {
    "analysis": {
      "analyzer": {
        "boards_content_analyzer":{
          "char_filter" : [],
          "tokenizer": "standard",
          "filter" : ["lowercase", "stop", "stemmer"] # 추가된 필터 -> stop : 불용어, stemmer : 영어를 기본형으로 변환
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "content": {
        "type" : "text",
        "analyzer": "boards_content_analyzer"
      }
    }
  }
}

 

  • 인덱스 이름 : boards
  • "settings" 정의 : 커스컴 분석기인 boards_content_analyzer 를 새로 정의
  • "mappings" 정의 : content 라는 필드에 text type으로 지정하고 analyzer 를 위에서 선언한 boards_content_analyzer 를 사용하겠다는 의미

 

데이터 인덱싱 (문서 색인)

생성한 인덱스에 데이터를 저장하는 쿼리

POST /boards/_doc
{
  "content" : "백화점에서 쇼핑을 하다가 친구를 만났다."
}
  • content 라는 필드에 "백화점에서 쇼핑을 하다가 친구를 만났다." 라는 text 타입의 문장을 저장

 

데이터 검색

데이터를 검색하기 위한 쿼리

GET /boards/_search
{
  "query": {
    "match": {
      "content": "백화점"
    }
  }
}
  • boards 인덱스에서 content 필드를 대상으로 "백화점"이라는 문장을 검색

이렇게까지 진행을 했을 때 문제가 아마 있어야 정상인데

한국어인 경우에는 절대... 검색 쿼리에서 "백화점"을 검색하면 절대 검색 결과에 내가 처음에 저장한 문서가 나오지 않을 것이다. 

 

왜냐하면, 인덱스를 생성할 때 한국어 전용 토크나이저가 아닌 기본 토크나이저 (영어 기본) 을 설정했기 때문이다. 

 

그럼 기본 토크나이저로는 어떻게 문장이 토크나이징이 되는지 확인을 하고 싶을 수 있다.

그럴 때는 아래의 분석 쿼리를 날리면 된다. 


 

텍스트 분석 쿼리 (Analyze API)

형태소 분석 요청, 텍스트 분석 테스트를 목적으로 사용하는 쿼리

내가 정의한 analyzer가 텍스트를 어떻게 토크나이징하는지 확인할 때나 검색이 예상대로 안될 때 디버깅 용도로 사용

GET /boards/_analyze
{
  "field": "content",
  "text" : "백화점에서 쇼핑을 하다가 친구를 만났다."
}

 


한국어를 보다 잘 토크나이징을 하기 위해서는 nori analyzer 플러그인을 설치해서 사용하면 된다. 

opensearch를 Docker compose 로 돌리고 있어서 잠시 Docker container를 멈추고

Dockerfile을 작성한다. 

 

Nori Analyzer 플러그인 설치 

FROM opensearchproject/opensearch:latest

RUN /usr/share/opensearch/bin/opensearch-plugin install --batch analysis-nori

Nori Analyzer 의 구성 

nori analyzer는 다음과 같이 구성되어있다. 

analyzer는 char_filter, tokenizer, filter 이 세가지로 구성되는데 이 속성을 custom tokenizer를 구성할때 넣어주고 

더 필요한 것들을 추가해주면 한국어를 보다 잘 분석해서 역인덱싱이 된다.

# Nori Analyzer 의 구성 
# nori_part_of_speech : 의미없는 조사 제거
# nori_reading_form : 한자를 한글로 변환 후 저장
GET /_analyze
{
  "text" : "백화점에서 쇼핑을 하다가 친구를 만났다.",
  "char_filter": [],
  "tokenizer": "nori_tokenizer",
  "filter": ["nori_part_of_speech", "nori_readingform", "lowercase"]
}

 

한국어 분석기를 사용해서 다시 인덱스를 생성 (이전 인덱스 삭제 필요)

PUT /boards
{
  "settings": {
    "analysis": {
      "analyzer": {
        "boards_content_anlyzer" : {
          "char_filter" : [],
          "tokenizer" : "nori_tokenizer",
          "filter" : ["nori_part_of_speech", "nori_readingform", "lowercase"]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "content" : {
        "type": "text",
        "analyzer": "boards_content_anlyzer"
      }
    }
  }
}

 

문서 인덱싱

POST /boards/_doc
{
  "content" : "백화점에서 쇼핑을 하다가 친구를 만났다."
}

 

문서 조회

GET /boards/_search
{
  "query": {
    "match": {
      "content": "쇼핑"
    }
  }
}

 

조회 결과 

{
  "took": 12,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 1,
      "relation": "eq"
    },
    "max_score": 0.13076457,
    "hits": [
      {
        "_index": "boards",
        "_id": "wmPpRp0BXrEJOwWF5_b9",
        "_score": 0.13076457,
        "_source": {
          "content": "백화점에서 쇼핑을 하다가 친구를 만났다."
        }
      }
    ]
  }
}

 

정상적으로 문서가 잘 나오고 있다. 

 

그럼 영어랑 한국어가 동시에 있는 문서라면 어떻게 인덱싱이 될까?

이럴때 분석 쿼리를 날려서 확인해볼 수 있다. 

GET /_analyze
{
  "text" : "오늘 영어 책에서 'It depends on the results.' 라는 문구를 봤다.",
  "char_filter": [],
  "tokenizer": "nori_tokenizer",
  "filter": ["nori_part_of_speech", "nori_readingform", "lowercase"]
}
{
  "tokens": [
    {
      "token": "영어",
      "start_offset": 3,
      "end_offset": 5,
      "type": "word",
      "position": 1
    },
    {
      "token": "책",
      "start_offset": 6,
      "end_offset": 7,
      "type": "word",
      "position": 2
    },
    {
      "token": "it",
      "start_offset": 11,
      "end_offset": 13,
      "type": "word",
      "position": 4
    },
    {
      "token": "depends",
      "start_offset": 14,
      "end_offset": 21,
      "type": "word",
      "position": 5
    },
    {
      "token": "on",
      "start_offset": 22,
      "end_offset": 24,
      "type": "word",
      "position": 6
    },
    {
      "token": "the",
      "start_offset": 25,
      "end_offset": 28,
      "type": "word",
      "position": 7
    },
    {
      "token": "results",
      "start_offset": 29,
      "end_offset": 36,
      "type": "word",
      "position": 8
    },
    {
      "token": "이",
      "start_offset": 39,
      "end_offset": 41,
      "type": "word",
      "position": 9
    },
    {
      "token": "문구",
      "start_offset": 42,
      "end_offset": 44,
      "type": "word",
      "position": 11
    },
    {
      "token": "보",
      "start_offset": 46,
      "end_offset": 47,
      "type": "word",
      "position": 13
    }
  ]
}

 

한국어는 나름대로 잘 되는데,,,

영어가 results, depends 같은 단어가 기본형으로 저장되지 않고,

on, it 등과 같은 의미없는 단어(불용어)가 포함되어서 저장이 되고 있다. 

 

그냥 기본형 분석기를 사용하면 영어같은 경우에는 처리를 해주는데 한국어 전용 분석기를 사용하니

영어를 그냥 띄어쓰기를 기준으로 토크나이징하는 이슈가 있었다.

 

그렇다면 이럴 때 nori analyzer를 커스텀할 수 있다.

# 불용어, 영단어 기본 형태 저장됨
GET /_analyze
{
  "text" : "오늘 영어 책에서 'It depends on the results.' 라는 문구를 봤다.",
  "char_filter": [],
  "tokenizer": "nori_tokenizer",
  "filter": ["nori_part_of_speech", "nori_readingform", "lowercase", "stop", "stemmer"]
}

 이런 형태로 filter 에 stop(불용어 저장 X), stemmer(영어 기본형으로 저장) 옵션을 추가해주면 된다. 

가상화는 실제 Apple 하이퍼바이저를 사용해서 실제 mac 을 빌려서 사용하지만 

에뮬레이션은 다소 느리긴 하지만, Rocky linux 8버전과 호환이 가능함.

 

가상화는 linux 9.x 버전부터 가능하다고 한다.

 

나는 리눅스마스터 시험 환경과 동일한 8.x 버전을 사용해보고 싶었기에...

이 문제는 ... 아주 간단한 오류였으나 2시간을 헤멨다

1. NIC (Network Iterface Card) - "네트워크 포트"

: 컴퓨터가 네트워크에 연결되는 출입구

 

- 노트북의 유선 LAN 포트

- Wi-Fi 칩

- 가상머신 안의 "가짜 랜카드"

 

전부 NIC 라고 부름

 

비유 : 집에서 인터넷을 쓰려면 콘센트가 있어야 하듯, 컴퓨터엔 NIC가 있어야 네트워크 가능

 

2. 네트워크 드라이버 - "출입구 사용 설명서"

: 운영체제가 NIC를 어떻게 쓰는지 알려주는 소프트웨어

 

- NIC만 있어도

- 드라이버가 없으면 못 씀

 

네트워크 드라이버가 운영체제 설치 단계에서 중요한 이유

- 리눅스 설치 초기에는 드라이버가 아직 다 안 올라옴

- 가상 NIC는 실제 하드웨어가 아니라서 드라이버 호환이 더 중요함

 

minimal ISO는 이 드라이버가 설치 초기에 바로 필요해서 실패하기 쉽다. 

 

3. DHCP(Dynamic Host Configuration Protocol) - "IP 주소 자동 배정기"

: 네트워크에 연결되면 자동으로 IP를 주는 시스템

: 네트워크에 접속한 장치에 필요한 설정을 자동으로 내려주는 통신 규칙

 

- 카페 와이파이 접속하면 DHCP가 IP, 게이트퀘이, DNS를 자동으로 처리해줌

 

 

4. 설치 mirror - "리눅스 앱스토어 서버"

: 리눅스 패키지를 내려받는 서버

 

minimal ISO 구조

- ISO 안에 패키지가 없음

- 설치 중 mirror 접속이 필수임

 

만일, mirror 접근 실패 시, 설치도 함께 실패되는 것임

 

 

 

1. 문제 상황

Mac(OS)에서 UTM을 사용해 Rocky Linux 8.8(aarch64) 가상 머신을 생성했다.
Rocky-8.8-aarch64-minimal.iso 이미지를 연결한 뒤 부팅하자 설치 메뉴가 정상적으로 표시되었고,
“Install Rocky Linux 8.8 in basic graphics mode” 항목을 선택했다.
하지만 설치가 시작되는 듯하다가 별도의 오류 메시지 없이 다시 설치 메뉴 화면으로 되돌아왔다.

 

2. 원인

사용한 ISO 이미지가 minimal ISO였다는 점이 원인이었다.
Rocky Linux의 minimal ISO는 설치 과정에서 필요한 패키지를 네트워크를 통해 추가로 다운로드하는 구조다.
UTM + Apple Silicon 환경에서는 이 과정에서 그래픽 초기화 또는 네트워크 설정 단계가 정상적으로 처리되지 않으면
설치 커널이 바로 종료되고 부트 메뉴로 복귀하는 문제가 발생할 수 있다.

 

3. 근거

  • 설치 실패 시 에러 메시지 없이 부트 메뉴로 즉시 복귀
  • ISO 파일 재다운로드, CPU/메모리 설정 변경 후에도 동일 현상 반복
  • 같은 환경에서 DVD ISO 사용 시 설치가 정상적으로 진행됨
  • 강의 자료에서도 minimal ISO가 아닌 DVD ISO 사용을 권장

이를 통해 UTM 환경 자체의 문제라기보다는
minimal ISO와 가상화 환경 간의 호환성 문제로 판단했다.

 

4. 해결 방안

Rocky-8.8-aarch64-dvd.iso 이미지로 변경하여 설치를 진행했다.
DVD ISO는 설치에 필요한 패키지가 모두 포함되어 있어
네트워크 의존 없이 설치가 가능하고, UTM 환경에서도 안정적으로 동작했다.
ISO 변경 후 설치 화면이 정상적으로 진행되었고, 문제 없이 Rocky Linux 설치를 완료할 수 있었다.

 

5. 정리

UTM + Apple Silicon 환경에서 Rocky Linux를 설치할 경우,
입문자나 강의 따라가기 목적이라면 minimal ISO보다는 DVD ISO 사용이 안전하다.
설치 메뉴에서 선택 후 다시 원래 화면으로 돌아오는 현상이 발생한다면
환경 설정 이전에 ISO 종류부터 점검하는 것이 우선이다.

respository settings 에서 default 브랜치를 develop 으로 변경하니까

PR -> Merge 시, 자동으로 Close 처리가 된다.

 

 

기본적으로 default 브랜치가 main 으로 되어있는데

개인 프로젝트 및 연습할 때는 develop 에 merge 해도 close를 하고 싶을 경우에는

위와 같이 해결이 가능하다!

특정 디렉토리 제외하고 압축하기

sudo tar -czvf ... \
  new_project \
  --exclude='new_project/_File'

 

하지만 어떤 이유에서인지 exclude 구분이 먹히지 않아 _File 디렉토리가 압축 내용에 포함이 되어버렸다. 

 

그래서 exclude 문을 줄바꿈 되지 않도록 앞쪽에 배치해서 다시 시도! 

 

특정 디렉토리 제외하고 압축하기

sudo tar --exclude='new_project/_File' \
  -czvf new_project_backup_$(date +%Y%m%d_%H%M).tar.gz \
  new_project

 

특정 디렉토리 제외하고 압축하기 (와일드카드 추가 .ver)

sudo tar --exclude='new_gmicesupport/_File' --exclude='new_project/_File/*' \
  -czvf new_project_backup_$(date +%Y%m%d_%H%M).tar.gz \
  new_project

 

 

++ 보너스 )

디렉토리 삭제 명령어 

sudo rm -f new_project_backup_20260122_1055.tar.gz

 

프론트를 React 로 두고, 백엔드를 Python으로 둔 뒤에 서로 통신할 수 있도록 설정을 하는 과정에서 CORS 관련 오류를 마주쳐서
해당 내용을 정리해보려고 한다.

 

✍️ CORS를 이해하게 된 계기

 

웹 프론트엔드와 백엔드 서버를 분리한 구조에서
프론트엔드가 Python 기반 백엔드 API를 호출하는 과정에서
의도한 대로 통신이 이루어지지 않는 문제가 발생했다.

서버 로그와 네트워크 요청을 확인해보면
백엔드 애플리케이션 자체는 정상적으로 요청을 처리하고 있었고,
API 응답 역시 정상적으로 반환되고 있었다.

그러나 브라우저 환경에서만 응답이 차단되며,
콘솔에는 CORS 관련 오류 메시지가 출력되고 있었다.

이 과정에서 문제의 원인이
서버 간 통신 실패가 아니라, 브라우저의 보안 정책(CORS) 에 있음을 확인하게 되었다.

 

🔍 문제의 본질

 

브라우저는 보안상의 이유로,
현재 페이지의 출처(Origin)와 다른 출처로 요청을 보낼 경우
해당 요청에 대한 응답을 무조건 신뢰하지 않는다.

 

따라서 서버는 단순히 데이터를 반환하는 것만으로는 충분하지 않고,
응답 헤더를 통해 어떤 출처의 요청을 허용할 것인지
브라우저에 명시적으로 알려주어야 한다.

 

즉, 백엔드 서버는 응답 시 다음과 같은 정보를 함께 전달해야 한다.

  • 허용할 출처 (Access-Control-Allow-Origin)
  • 허용할 메서드 (Access-Control-Allow-Methods)
  • 허용할 헤더 (Access-Control-Allow-Headers)
  • (필요한 경우) 인증 정보 포함 여부 (Access-Control-Allow-Credentials)

브라우저는 이 응답 헤더를 기준으로
해당 API 응답을 애플리케이션 코드에 전달할지, 차단할지를 판단한다.


주요 개념

  • 출처 (Origin)
  • 교차 출처 요청 (Cross-Origin Request)
  • 단순 요청 (Simple Request)
  • 사전 요청 (Preflighted Request)

 

 

1. 출처 (Origin)

 

Origin 이란? 출처(Origin) 는 다음 세 가지 조합을 의미한다.

 
프로토콜 + 도메인 + 포트
 

예를 들어:

이 세가지 중 하나라도 다르면 다른 출처(Cross-Origin) 로 간주된다.


2. 교차 출처 요청 (Cross-Origin Request)

 

브라우저에서 현재 페이지의 출처와 다른 출처의 서버로 요청을 보내는 경우,
이를 교차 출처 요청이라고 합니다.

 

 

💡 CORS는 서버 보안 정책이 아니라 브라우저가 응답을 차단할지 결정하는 보안 정책을 의미한다.

 


3. 단순 요청 (Simple Request)

 

아래 조건을 모두 만족하면 브라우저는 사전 요청(바로 다음에 설명할 개념) 없이 바로 요청을 보냅니다.

 

✅ 허용 메서드

  • GET
  • POST
  • HEAD

✅ 허용 헤더 (Safe Headers)

  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type
    (단, 값은 application/x-www-form-urlencoded, multipart/form-data, text/plain 중 하나)

이 조건을 벗어나면 사전 요청(Preflight) 이 발생합니다.

 

즉, GET, POST, HEAD를 제외한 PATCH, DELETE 등의 메서드는 단순요청(Simple Request) 조건에 해당하지 않기 때문에

브라우저는 서버에 바로 요청을 보내지 않고, 사전 요청(Preflighted Request)을 통해 해당 요청이 허용되는지 먼저 확인한다.

 

동일한 메커니즘으로 허용 헤더(Safe Headers)가 아닌 헤더 정보를 포함한 요청 역시 브라우저는 사전 요청을 통해 서버의 허용 여부를 먼저 확인하게 된다.


4. 사전 요청 (Preflighted Request)

 

단순 요청 조건을 만족하지 않는 경우, 브라우저는 본 요청을 보내기 전에 OPTIONS 요청을 먼저 보낸다. 

이를 Preflight Request 라고 한다.

 


 

📌 사전 요청에서 확인하는 항목

 

① 출처 (Origin)

<요청 헤더>
Origin: http://localhost:5173
 

브라우저가 요청을 보낼 때 자동으로 현재 페이지의 출처를 Origin 헤더에 담아 전송한다.

 

예시 :

 

 

⚠️ 프론트엔드 코드에서 직접 넣는 것이 아니라 브라우저가 자동으로 추가한다.

 


<응답 헤더>
Access-Control-Allow-Origin: http://localhost:5173

 

서버는 응답 시 Access-Control-Allow-Origin 헤더를 통해 이 출처의 요청을 허용할지 여부를 브라우저에 알려준다.

 

예시 : 

 

➡️ 브라우저는 요청의 Origin 값과 응답의 Access-Control-Allow-Origin 값을 비교해
응답을 사용할지, 차단할지를 결정합니다.


② 메서드 (Method)

 

Access-Control-Request-Method

Access-Control-Allow-Methods

<사전 요청 (OPTIONS)>
Access-Control-Request-Method: PUT

 

브라우저는 이 메서드를 사용해도 되는지 서버에 먼저 물어본다.

 

<서버 응답>
Access-Control-Allow-Methods: GET, POST, PUT
 

서버가 허용 가능한 메서드를 응답으로 내려주면,
브라우저는 해당 메서드를 사용하는 본 요청을 전송한다.


③ 헤더 (Header)

Access-Control-Request-Headers

Access-Control-Allow-Headers

 

단순 요청에서 허용되지 않은 헤더를 사용할 경우,
브라우저는 사전 요청에서 사용하려는 헤더 목록을 전달

<요청 헤더>
Access-Control-Request-Headers: Authorization, Content-Type

 

서버는 아래와 같이 응답

<응답 헤더>
Access-Control-Allow-Headers: Authorization, Content-Type

 

➡️ 브라우저는 이 응답을 보고 해당 헤더를 포함한 요청을 보낼 수 있는지 판단하게된다. 


④ 캐시 (Preflight Cache)

 

Access-Control-Max-Age

 
Access-Control-Max-Age: 5
  • 사전 요청 결과를 5초 동안 캐시
  • 같은 조건의 요청에 대해
    • OPTIONS 요청을 생략
    • 바로 본 요청 전송 가능

➡️ 잦은 사전 요청으로 인한 네트워크 비용 감소


✅ 정리 한 문장

CORS는 브라우저가 서버 응답을 사용할 수 있는지를 판단하는 정책이며,
서버는 허용 여부를 응답 헤더로 알려주고,
최종 결정은 브라우저가 한다.

 

+ Recent posts