Unit.10 [Web Server] 기초

2023. 2. 6. 18:46코드스테이츠/코드스테이츠S2: Chapter & Unit

 웹 개발을 하다 보면, 이 에러를 적어도 한 번 쯤은 겪게 되고, 그리고 높은 확률로 이 에러 때문에 골머리를 앓는 경험을 하게 하는 그것. 바로 CORS Error

CORS가 대체 뭐길래 이런 에러를 띄우는 건지 알아보기 전에, CORS가 필요하게 된 배경인 SOP에 대해서 먼저 알아보도록 합시다.


SOP

SOP은 Same-Origin Policy의 줄임말로, 동일 출처 정책을 뜻합니다.

한 마디로 ‘같은 출처의 리소스만 공유가 가능하다’라는 정책인데요. 여기서 말하는 ‘출처(Origin)’는 다음과 같습니다.

출처는 프로토콜, 호스트, 포트의 조합으로 되어있습니다. 이 중 하나라도 다르면 동일한 출처로 보지 않습니다. 예시를 들어 이해해봅시다.


SOP은 잠재적 위험으로부터 문서를 분리함으로써 공격받을 수 있는 경로를 줄여준다.

origin이 일치하는지를 판단하여 동일출처인지 확인하고 정보를 내준다.

하지만 이러한 SOP만으로는 현실적인 어려움이 많았다.

다른 출처의 리소스의 사용은 사실상 불가피하고, 다른출처의 리소스를 족족 막아버리면 사실상 웹으로써의 정상적인 기능을 한다고 보기 어렵다.

그래서 나온것이 바로 CORS다.


CORS

위 문제 상황에서 필요한 것이 바로 CORS 입니다. CORS는 Cross-Origin Resource Sharing의 줄임말로 교차 출처 리소스 공유를 뜻합니다.
MDN에서는 CORS를 다음과 같이 정의하고 있습니다.

교차 출처 리소스 공유(Cross-Origin Resource Sharing, CORS)는 추가 HTTP 헤더를 사용하여, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제입니다.

즉, 브라우저는 SOP에 의해 기본적으로 다른 출처의 리소스 공유를 막지만, CORS를 사용하면 접근 권한을 얻을 수 있게 되는 것입니다. 다시 처음에 봤던 에러를 다시 한 번 살펴볼까요?

지금까지 공부한 내용을 바탕으로 이 에러를 쉽고 친절하게 풀어서 쓰면 다음과 같을 것입니다.

다른 출처의 리소스를 가져오려고 했지만 SOP 때문에 접근이 불가능합니다. CORS 설정을 통해 서버의 응답 헤더에 ‘Access-Control-Allow-Origin’을 작성하면 접근 권한을 얻을 수 있습니다.

즉, 이 에러는 CORS 때문이 아니라, SOP 때문입니다. CORS는 오히려 이 에러를 해결해줄 수 있는 방안이었던 것이죠! 그럼, CORS는 어떻게 동작하는지, 어떻게 설정할 수 있는지 알아봅시다.


CORS 동작 방식

1. 프리플라이트 요청 (Preflight Request)

실제 요청을 보내기 전, OPTIONS 메서드로 사전 요청을 보내 해당 출처 리소스에 접근 권한이 있는지부터 확인하는 것을 프리플라이트 요청이라고 합니다.

은행창구로 생각하면 이해가 쉽다. 클라이언트=고객 / 브라우저 = 창구직원/ 서버 = 전산시스템

고객(클라이언트)가 창구직원(브라우저)에게 통장을 들고가서 안에있는 돈 전부 인출해달라고 부탁했다.

창구직원은 신분증과 통장을 건내받아 전산망에 조회 요청을 한다.

전산망에서 인증이 끝나고 대조한 정보가 맞다며 Access-Control-Allow-Origin으로 요청에대한 응답을 하고, 현금인출을 시작한다.

만약 고객이 건낸 신분증과 통장이 일치하지않다면 전산망은 날강도놈이라고 생각하여 Access-Control-Allow-Origin을 응답없음으로 처리하고 이를 본 창구지원은 경보를 울린다.


Express 서버

Express 프레임워크를 사용해서 서버를 만드는 경우에는, cors 미들웨어를 사용해서 보다 더 간단하게 CORS 설정을 해줄 수 있습니다.

const cors = require("cors");
const app = express();

//모든 도메인
app.use(cors());

//특정 도메인
const options = {
  origin: "https://codestates.com", // 접근 권한을 부여하는 도메인
  credentials: true, // 응답 헤더에 Access-Control-Allow-Credentials 추가
  optionsSuccessStatus: 200, // 응답 상태 200으로 설정
};

app.use(cors(options));

//특정 요청
app.get("/example/:id", cors(), function (req, res, next) {
  res.json({ msg: "example" });
});

이 외 다양한 개발 환경에서도, 헤더의 값을 설정하는 방법만 알면 CORS 설정을 해줄 수 있습니다.


미들웨어란?

자동차 공장에서는 컨베이어 벨트위에 올려진 자동차의 프레임에, 강 공정마다 부품을 추가한다.

모든 부품이 추가되어 자동차가 완성되면, 불량품이 있는지 검수하고 불량품이 있다면 결과물로 나오게 된다.

미들웨어(Middleware)는 자동차 공장의 공정과 비슷하다. 컨베이어 벨트위에 올라가 있는 요청에 필요한 기능을 더하거나, 문제가 발생된 불량품을 밖으로 걷어내는 역할을 한다.

미들웨어는 express의 큰 장점이라고 할 수 있다.

 

자주 사용하는 미들웨어

미들웨어를 사용하는 상황은 다음과 같습니다.

  1. POST 요청 등에 포함된 body(payload)를 구조화할 때(쉽게 얻어내고자 할 때)
  2. 모든 요청/응답에 CORS 헤더를 붙여야 할 때
  3. 모든 요청에 대해 url이나 메서드를 확인할 때
  4. 요청 헤더에 사용자 인증 정보가 담겨있는지 확인할 때

  • res.app: 똑같이 res 객체를 통해 app 객체에 접근한다. res.app.get('')같이 사용 가능.
  • res.set(헤더, 값) / res.setHeader(헤더, 값): 응답의 헤더를 설정한다. req.get()이 헤더값을 가져오는거라면 이건 헤더 설정
  • res.status(코드) 
// http모듈에서 두줄이 코드가 하나로 줄어들었다고 보면 된다.
// res.writeHead(200, { 'Content-Type', 'text/html'});
// res.end("안녕하세요");
 
res.status(403).end()
res.status(400).send('Bad Request')
res.status(404).sendFile('/absolute/path/to/404.png')
  • res.sendStatus(코드): 응답 시의 HTTP 상태 코드를 지정한다.
res.sendStatus(200) // == res.status(200).send('OK')
res.sendStatus(403) // == res.status(403).send('Forbidden')
res.sendStatus(404) // == res.status(404).send('Not Found')
res.sendStatus(500) // == res.status(500).send('Internal Server Error')
  • res.type(type) : Contents-Type 헤더를 설정할 수 있는 간단한 메서드.
  • res.cookie(키, 값, 옵션): 쿠키를 응답에 설정하는 메서드이다. (cookie-parser 패키지가 필요)
  • res.clearCookie(키, 값, 옵션): 쿠키를 응답에서 제거하는 메서드이다.

  • res.end(): 데이터 없이 응답을 보낸다.
  • res.json(JSON): JSON 형식의 응답을 보낸다.
// 이 부분을 하나로 짧게 합친 express메소드
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringfy({hello: 'nomad'}));
 
// ↓
 
res.json({hello: 'nomad'})​​
  • res.redirect(주소): 리다이렉트할 주소와 함께 응답을 보낸다.
// 기존 http모듈 코드
res.writeHead(301, {
	Location: 'http://example.com',
    // 'Set-Cookie': '~',
});
res.end();
 
// 익스프레스 코드
res.redirect(301, 'http://example.com') // 301로 해당페이지로 강제이동
res.redirect('/foo/bar')
res.redirect('http://example.com')
res.redirect('../login')
  • res.locals / res.render(뷰, 데이터):
    res.locals는 뷰를 렌더링하는 기본 콘텍스트를 포함하는 객체다.
    res.render는 jade와 같은 템플릿 엔진을 사용하여 뷰를 렌더링한다.
// send the rendered view to the client
res.render('index')
 
// if a callback is specified, the rendered HTML string has to be sent explicitly
res.render('index', function (err, html) {
  res.send(html)
})
 
// pass a local variable to the view
res.render('user', { name: 'Tobi' }, function (err, html) {
  // ...
})
  • res.send(body), res.send(status, body) : 클라이언트에 응답을 보냄. 상태 코드는 옵션.
    기본 콘텐츠 타입은 text/html이므로 text/plain을 보내려면 res.set(‘Content-Type’, ‘text/plain’)을 먼저 호출 해야한다. 
  • res.sendFile(경로): 경로에 위치한 파일을 응답한다.

res.send(Buffer.from('whoop'))
res.send({ some: 'json' }) // res.status(200).send({ some: 'json' }) 와 같다. 200이 생략되어있음
res.send('<p>some html</p>')
res.status(404).send('Sorry, we cannot find that!')
res.status(500).send({ error: 'something blew up' })
  • res.attachment([filename]), res.download(path, [filename], [callback]) : 클라이언트에게 파일을 표시하지 말고 다운로드 받으라고 전송함. filename을 주면 파일 이름이 명시되며, res.attachment는 헤더만 설정하므로 다운로드를 위한 node 코드가 따로 필요하다.

res.end()
res.json(JSON)
res.redirect(주소)
res.render(뷰, 데이터)
res.send(데이터)
res.sendFile(경로)

는 각 라우터에 반드시 한번만 써야되는건 잊지말자.