(TypeScript 공부 중)
공부 중인 타입스크립트 index.ts 전문:
// src/index.ts // 1. 필요 모듈 import dotenv from "dotenv"; import express, { Request, Response } from "express"; import cors from "cors"; import helmet from "helmet"; import { itemsRouter } from "./items/items.router"; dotenv.config(); // 2. 앱 변수 선언 // PORT 설정값이 없으면 서버 종료 if (!process.env.PORT) { process.exit(1); } // PORT를 string으로 받고, 10진수로 parseInt함. const PORT: number = parseInt(process.env.PORT as string, 10); const app = express(); // 3. 앱 설정 app.use(helmet()); app.use(cors()) app.use(express.json()); app.use("/api/menu/items", itemsRouter); // 4. 서버 활성화 app.get("/", (req: Request, res: Response) => { res.send("Hi"); }); app.listen(PORT, () => { console.log(`Listening on port http://localhost:${PORT}`) })
여기서 아래의 모든 에러들이 발생했고, 얼결에 CORS에 대해 공부하게 되었다.
그동안 CORS라는 개념을 살짝 엿볼 때마다 굉장히 복잡하다고 느꼈는데 오늘 공부해보니 생각보다 개념이 복잡하지 않았다. 물론 아주 기초적인 부분만 공부해서 그런 것도 있을 테지만, 어렵지 않게 하나로 잘 정리된 글을 발견한 것이 주요한 이유였다고 생각한다. 좋은 글을 공유해준 블로거 Evan Moon님에게 감사를 전한다.
(여기 보는 중)
Error: Cannot find module './*.ts’ 에러 찾는 중…
“type definition 모듈이란” 검색 중
https://velog.io/@ysong0504/TypeScript-Type-이란
타입스크립트 핸드북 모듈 소개 - https://joshua1988.github.io/ts/usage/modules.html#소개
에러 TS1479: The current file is a CommonJS module whose imports will produce 'require' calls; - 원인??
에러 TS1479: The current file is a CommonJS module whose imports will produce 'require' calls; - 원인??
발생 상황: npm i helmet 설치 후 타입스크립트 파일에import helmet from "helmet";
부분에서 컴파일 에러 발생
에러 메세지 전문:
TS1479: The current file is a CommonJS module whose imports will produce 'require' calls; however, the referenced file is an ECMAScript module and cannot be imported with 'require'.
Consider writing a dynamic 'import("helmet")' call instead.
To convert this file to an ECMAScript module, change its file extension to '.mts', or add the field `"type": "module"` to 'C:/Users/USER/Desktop/Sparta/03.2_typescript_practice_restaurant_crud/package.json'.
원인:
시도:
내 package.json 파일에 "type": "module"
을 추가하라는데, tsconfig.json에 작성한 "moduleResolution"
:
"NodeNext"
이랑은 무슨 차이인걸까?
해결: 에러 메세지의 안내대로 package.json에 “type”: “module”
을 추가, 해결함.
미니 에러 TS2307: Cannot find module './items/items.router' or its corresponding type declarations.
package.json에 “type”: “module”
을 추가하고 나니
import { itemsRouter } from "./items/items.router";
이 부분에 빨간 밑줄이 생김.
해결: "./items/items.router.js";
이라고 파일 확장자를 명시해주니 해결됨.
의문: 왜..?
- 타입스크립트에서 “module”의 작동 방식 (인식 방식)
에러 Error: Must use import to load ES Module - 원인??
에러 Error: Must use import to load ES Module - 원인??
발생 상황: package.json에 “type”: “module”
을 추가하고 나니 발생.
// package.json
...
"type": "module",
"scripts": {
"dev": "ts-node-dev --transpile-only src/index.ts",
},
...
// src/index.ts
import express, { Request, Response } from "express";
import helmet from "helmet";
import { itemsRouter } from "./items/items.router";
에러 메세지 전문:
C:\Users\USER\Desktop\Sparta\03.2_typescript_practice_restaurant_crud>npm run dev
> 001_import_test@1.0.0 dev
> ts-node-dev --transpile-only src/index.ts
[INFO] 13:32:26 ts-node-dev ver. 2.0.0 (using ts-node ver. 10.9.1, typescript ver. 4.9.4)
Compilation error in C:\Users\USER\Desktop\Sparta\03.2_typescript_practice_restaurant_crud\src\index.ts
Error: Must use import to load ES Module: C:\Users\USER\Desktop\Sparta\03.2_typescript_practice_restaurant_crud\src\index.ts
at Object.<anonymous> (C:\Users\USER\Desktop\Sparta\03.2_typescript_practice_restaurant_crud\src\index.ts:1:7)
at Module._compile (node:internal/modules/cjs/loader:1159:14)
at Module._compile (C:\Users\USER\Desktop\Sparta\03.2_typescript_practice_restaurant_crud\node_modules\source-map-support\source-map-support.js:568:25)
at Module.m._compile (C:\Users\USER\AppData\Local\Temp\ts-node-dev-hook-0559505840986263.js:69:33)
at Module._extensions..js (node:internal/modules/cjs/loader:1213:10)
at require.extensions..jsx.require.extensions..js (C:\Users\USER\AppData\Local\Temp\ts-node-dev-hook-0559505840986263.js:114:20)
at require.extensions.<computed> (C:\Users\USER\AppData\Local\Temp\ts-node-dev-hook-0559505840986263.js:71:20)
at Object.nodeDevHook [as .ts] (C:\Users\USER\Desktop\Sparta\03.2_typescript_practice_restaurant_crud\node_modules\ts-node-dev\lib\hook.js:63:13)
at Module.load (node:internal/modules/cjs/loader:1037:32)
at Function.Module._load (node:internal/modules/cjs/loader:878:12)
[ERROR] 13:32:26 Error: Must use import to load ES Module: C:\Users\USER\Desktop\Sparta\03.2_typescript_practice_restaurant_crud\src\index.ts
원인: 모르겠음.
시도:
해결: package.json에서 다시 “type”: “module”
을 제거하고 실행하니
// src/index.ts
import helmet from "helmet";
이렇게 빨간 줄은 쳐지는데 컴파일 에러가 뜨지 않고 서버가 잘 실행됨.
- 왜지..?
에러 [브라우저] No 'Access-Control-Allow-Origin' header is present on the requested resource. ✔️
에러 [브라우저] No 'Access-Control-Allow-Origin' header is present on the requested resource. ✔️
발생 상황: 간단한 타입스크립트 CRUD 작성 튜토리얼에서 제공한 프론트 애플리케이션에서 내 로컬 서버로 HTTP PUT 요청을 보냄.
에러 메세지 전문:
Access to XMLHttpRequest at 'http://localhost:6600/api/menu/items/3' from origin 'https://dashboard.whatabyte.app' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
PUT http://localhost:6600/api/menu/items/3 net::ERR_FAILED
원인:
자바스크립트 엔진 표준스펙의 동일출처 정책(SOP:same-origin-policy) 보안 규칙 때문에 발생한 에러이다 (아래 SOP 글로 이어짐).
위 요청은 다른 ‘출처(origin)’로 보내는 요청이라, 응답으로 온 헤더에 Access-Control-Allow-Origin 항목이 있어서 ‘지금 요청하는 origin이 응답 받기에 허용되는 origin이다’라고 명시가 되어있어야 하는데 그게 없어서 발생한 문제.
한 마디로 CORS 정책 위반으로 인해 발생한 문제.
시도: Express의 미들웨어 cors와 helmet을 끼워넣어 봄.
해결: Express의 미들웨어 cors를 설정하는 것이 문제 해결의 핵심이었다. (helmet은 다른 역할)
// src/index.ts
import cors from "cors";
...
app.use(cors());
cors()의 디폴트 설정값은 다음과 같다:
{
"origin": "*",
"methods": "GET,HEAD,PUT,PATCH,POST,DELETE",
"preflightContinue": false,
"optionsSuccessStatus": 204
}
- origin: Access-Control-Allow-Origin CORS 헤더를 설정함. 예를 들어 “http://naver.com”이라고 설정해놨으면 “http://naver.com”에서 온 요청만 허용하겠다는 의미가 된다. (실은 서버에서는 일단 응답을 해주고, 응답을 받은 브라우저 쪽에서 본인이 ‘허용된 출처’가 아니면 응답을 폐기하게 되는 원리지만).
- methods: Access-Control-Allow-Methods CORS 헤더를 설정함. 예를 들어 ['GET', 'PUT', 'POST']라고 설정하면 이 세 HTTP 요청만 허용됨을 뜻한다.
- preflightContinue: CORS 사전 요청(시나리오)의 응답을 다음 핸들러로 넘길지 여부
- optionsSuccessStatus: CORS 사전 요청(OPTIONS 요청)의 성공 응답 코드를 사전 지정할 수 있다. IE11같은 오래된 브라우저같이 204 코드를 쓰면 문제가 발생하게 되는 경우에 설정해준다.
⇒ 서버가 응답해줄 때 cors() 미들웨어가 설정 디폴트 값인 Access-Control-Allow-Origin: “*” 헤더를 세팅해 줌으로써 요청 헤더에서의 Origin 값과 응답 헤더에서의 Access-Control-Allow-Origin 값이 맞지 않아서 생긴 CORS 에러를 해결하게 된 것이다.
(출처: Express Middleware 공식 문서 https://expressjs.com/en/resources/middleware/cors.html)
SOP (Same-origin Policy, 동일 출처 정책)
SOP (Same-origin Policy, 동일 출처 정책)
: 자바스크립트에서 XMLHttpRequest로 외부서버 접근시 같은 출처(Same Origin)의 페이지로만 접근이 가능도록 제한하는 정책.
‘같은 출처(origin)’란: 프로토콜, 호스트명(서비스명), 포트번호가 일치하는 것.
예시:
이 에러 메세지를 보면, 요청하는 쪽(클라이언트, Origin) url은 “http://dashboard.whatabyte.app”이고 요청을 받는 쪽(서버) url은 “http://localhost:6600/api/menu/items/3”로, 호스트와 포트번호가 다르다. 그래서 자바스크립트 엔진의 디폴트 설정인 동일 출처 정책(SOP)에 걸려서 에러가 난 것이다.
CORS(Cross-Origin Resource Sharing) 정책
CORS(Cross-Origin Resource Sharing) 정책
: 다른 출처(Origin)끼리 요청이 HTTP 요청이 가능하게 하는, SOP의 예외 정책.
: 서버에서 ‘허용하는 (외부)Origin’을 특정해주는 것.
: 서버에서 특정 외부 요청을 허용해주는 것.
CORS 정책의 동작 원리
CORS 정책의 동작 원리
- 다른 출처로의 리소스 요청을 제한하는 기본 정책은 2011년 RFC 6454에서 제정된 SOP(Same-Origin Policy)이다. 이 때 다른 출처로의 리소스 요청이 허용되는 몇 가지 예외 사항이 있는데 이중 하나가 바로 “CORS 정책을 지킨 리소스 요청”이다.
⇒ 우리가 다른 출처로 리소스를 요청한다면 일단 SOP 정책을 위반한 것이 된다. 거기다가 SOP의 예외 조항인 CORS 정책까지 지키지 않는다면 아예 다른 출처의 리소스를 사용할 수 없게 되는 원리이다.
- 출처를 비교하는 로직이 서버가 아니라 브라우저에 구현되어 있다.
⇒ 만약 우리가 CORS 정책을 위반하는 리소스 요청을 하더라도 해당 서버가 같은 출처에서 보낸 요청만 받겠다는 로직을 가지고 있는 경우가 아니라면 서버는 정상적으로 응답을 하고, 이후 브라우저가 이 응답을 분석해서 CORS 정책 위반이라고 판단되면 그 응답을 사용하지 않고 그냥 버리는 순서로 동작한다.
⇒ 그래서(CORS가 브라우저의 구현 스펙에 포함되는 정책이라서) 브라우저를 통하지 않고 서버 간 통신을 할 때는 이 정책이 적용되지 않음 주의! Postman 등으로 API 요청 테스트를 할 때 정상 동작하더라도 실제 브라우저에서는 CORS 정책에 걸려 에러가 날 수 있다.
CORS 정책의 동작 순서
CORS 정책의 동작 순서
기본적인 골자는 이렇다:
- 웹 클라이언트 어플리케이션이 다른 출처의 리소스를 요청함 (HTTP 프로토콜 사용). 이 때 요청 헤더에
Origin: 요청을_보내는_출처
를 함께 담아 보낸다.
- 서버가 응답할 때 헤더에
Access-Control-Allow-Origin: 이_리소스를_접근하는_것이_허용된_출처(들)
필드를 담아 보내줌.
- 응답을 받은 브라우저가 자신이 보냈던 요청 헤더의
Origin
항목과 서버가 보내온 응답의Access-Control-Allow-Origin
항목을 비교하여 파기 여부를 결정한다.
구체적인 동작 순서 시나리오는 3가지로 나뉜다:
- 사전 요청 Preflight Request
: 브라우저가 본 요청을 보내기 전에 보내는 HTTP의 OPTIONS 메소드 예비 요청이 바로 Preflight. 본 요청을 보내기 전에 브라우저 스스로 이 요청을 보내는 것이 안전한지 확인하는 것이 목적이다.
- 요청:
1) http method: OPTIONS 2) http header : Access-Control-Request-Method, //실제요청하려는 메서드 Access-Control-Request-Headers, //실제요청 헤더 Origin //출처 (요청을 보내는 페이지의 도메인)
- 응답:
1) response header : Access-Control-Allow-Origin //요청을 허용하는 출처 (* 와일드카드 설정시 모든요청 허용) Access-Control-Allow-Methods //요청을 허용하는 메서드 (디폴트는 GET / POST) Access-Control-Allow-Headers //요청을 허용하는 헤더 Access-Control-Max-Age //Preflight 결과를 캐시에 저장하는 시간. 저장된 시간동안 사전요청 암함.
- 요청:
- 단순 요청 Simple Request
: 예비 요청 없이 본 요청을 곧바로 보내고 나서, 되돌아온 응답의 헤더에서
Access-Control-Allow-Origin
값을 체크하는 방식.다음 조건을 만족할 때 예비 요청을 생략하게 된다:
1) http method : GET , POST , HEAD => 요청의 메소드는 GET, HEAD, POST 중 하나여야 한다. 2) http header : Accept, Accetp-Language, Content-Language, Content-Type => Accept, Accept-Language, Content-Language, Content-Type, DPR, Downlink, Save-Data, Viewport-Width, Width를 제외한 헤더를 사용하면 안된다. 3) Content-Type : Application/x-www.-form-urlencoded , text/plain, multipart/form-data => 만약 Content-Type를 사용하는 경우에는 application/x-www-form-urlencoded, multipart/form-data, text/plain만 허용된다.
- 인증 요청 Credentialed Request
: 인증이 얽혀있는 시나리오.
작동 방식:
- 클라이언트쪽에서 요청을 보낼 때 credentials 옵션을 ‘include’로 설정해 보낸다.
fetch('https://localhost:6600/api/menu/itmes', { credentials: 'include', // 모든 요청에 인증 정보를 담을 수 있다. // 'same-origin' // (구글 크롬)기본값. 같은 출처간 요청에만 인증 정보를 담을 수 있다. // 'omit' // 모든 요청에 인증 정보를 담지 않는다. });
- 그러면 브라우저의 쿠키 정보같이 인증과 관련되 헤더가 요청에 담겨 전달되게 된다.
- 서버 측에
Access-Control-Allow-Origin
값으로 모든 출처를 허용한다는 의미인*
가 설정되어있다고 할 때, 이전까지는 CORS 정책 위반으로 인한 제약을 받지 않으나
- 이제는 브라우저가 다음 두가지 룰을 더 검사하게 된다.
1. Access-Control-Allow-Origin에는 *를 사용할 수 없으며, 명시적인 URL이어야한다. 2. 응답 헤더에는 반드시 Access-Control-Allow-Credentials: true가 존재해야한다.
⇒ 3번에서
Access-Control-Allow-Origin: “*”
가 설정되어있었으므로 실제로 다음과 같은 CORS 에러가 뜨게 된다: - 클라이언트쪽에서 요청을 보낼 때 credentials 옵션을 ‘include’로 설정해 보낸다.
출처:
(생각보다 짧고 이해되는) RFC 6454 원본 - https://www.rfc-editor.org/rfc/rfc6454#section-3.4.2
(출처가 탄탄하고 잘 읽히는) CORS 정리글 - https://evan-moon.github.io/2020/05/21/about-cors/
가벼운 CORS 정리글 - https://cheershennah.tistory.com/229
Uploaded by N2T