http 모듈로 서버 만들기

Haechan Kim·2022년 1월 23일

Node.js

목록 보기
6/15

클라이언트에서 서버로 요청(request)을 보내고, 서버에서는 요청을 읽고 처리한 뒤 클라이언트에 응답(response)을 보냄.

따라서 서버에는 요청 받는 부분과, 응답 보내는 부분이 있어야 함.
요청과 응답은 이벤트 방식이라고 생각하면 됨.
클라이언트로부터 어떤 요청 왔을때 어떤 작업할지 이벤트 리스너 미리 등록해둬야 함.

이벤트 리스너 가진 노드 서버 만들어보자.

// createServer.js
const http = requre('http');

http.createServer((req, res) => {
   // 여기에 어떻게 응답할지 적음 
});

http 서버가 있어야 웹 브라우저의 요청 처리할 수 있으므로 http 모듈 사용.
http 모듈의 createServer 메서드는 인수 요청에 대한 콜백 함수 넣을 수 있으며, 요청 들어올때마다 콜백 함수 실행됨.
=> 이 콜백 함수에 응답 적으면 됨

createServer의 콜백 부분을 보면 req(request) 객체에는 요청 관한 정보들을, res(respense) 객체에는 응답 관한 정보들을 담는다.

아직 코드 실행해도 아무일도 안일어남.
요청에 대한 응답 안넣었고 서버와 연결도 안했기 때문
응답 보내는 부분과 서버 연결을 추가하자.

// server1.js
const http = require('http');

http.createServer((req, res) => {
    res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
    res.write('<h1>Hello Node!!</h1>');
    res.end('<p>Hello Server!!</p>');
})
.listen(8080, () => { // 서버 연결
    console.log('8080번 포트에서 서버 대기 중입니다!');
});

-localhost포트란?

localhost는 현재 컴퓨터의 내부 주소 가리킴.
외부에선 접근할 수 없고 자신의 컴퓨터에서만 접근 가능하기에 서버 개발 시 테스트 용으로 많이 사용됨.
localhost 대신 127.0.0.1 도 같은 주소
이러한 숫자 주소를 IP(Internet Protocol)라고 부름

포트는 서버 내에서 프로세스 구분하는 번호
서버는 프로세스에 포트를 다르게 할달해 들어오는 요청을 구분함.
포트번호는 IP주소 뒤에 콜론(:)과 함께 붙여 사용함.

현 예제에서는 임의의 포트 번호 8080에 노드 서버(프로세스)를 연결함.
이 파일을 실행하면 서버는 8080포트에서 요청이 오기를 기다림.

// server1.js
const http = require('http');

http.createServer((req, res) => {
    // 응답에 대한 정보 기록하는 메서드
    // 첫 인수로 성공적 요청 의미하는 200,(status code) 
    // 두번째로 응답에 대한 정보 보내는데 콘텐츠의 형식이 html임을 알리고있음.
    // 이 정보가 기록되는 부분을 헤더(header) 라고 부름
    res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});

    // 첫 인수는 클라이언트로 보낼 데이터
    // 지금은 html모양의 문자열 보냈지만 버퍼도 보낼 수 있음
    // 보낸 데이터 기록되는 부분을 본문(Body)이라고 부름
    res.write('<h1>Hello Node!!</h1>');

    // 응답 종료하는 메서드
    // 인수 있다면 그 데이터로 클라이언트로 보내고 응답 종료함
    res.end('<p>Hello Server!!</p>');
})
// createServer 뒤에 listen 메서드 붙이고 클라이언트에 공개할 포트 번호(8080)와,
// 포트 연결 완료 후 실행될 콜백 함수를 넣는다.
.listen(8080, () => { // 서버 연결
    console.log('8080번 포트에서 서버 대기 중입니다!');
});

위 예제는 res.write에서 'hello node' 문자열을, res.end에서 'hello server' 문자열을 클라이언트로 보낸 후 응답 종료된 것.
브라우저는 응답 내용 받아서 렌더링 함.

listen 메서드에 콜백 함수 넣는 대신 서버에 listening 이벤트 리스너를 붙여도 됨.

// server1.js
const http = require('http');

const server = http.createServer((req, res) => {
    res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
    res.write('<h1>Hello Node!!</h1>');
    res.end('<p>Hello Server!!</p>');
});

server.listen(8080);
server.on('listening', () => {
    console.log('8080번 포트에서 서버 대기중입니다!!!');
});
server.on('error', (error) => {
    console.error(error);
});

한 번에 여러 서버를 실행할 수도 있음
createServer 원하는 만큼 호출하면 됨

const http = require('http');

http.createServer((req, res) => {
    res.writeHead(200, {'Content-Type' : 'text/html; charset=utf-8'});
    res.write('<h1>Hello node!! (8080)</h1>');
    res.end('<p>Hello server!!</p>');
})
.listen(8080, () => {
    console.log('8080번 포트에서 서버 대기 중입니다!');
});

http.createServer((req, res) => {
    res.writeHead(200, {'Content-Type' : 'text/html; charset=utf-8'});
    res.write('<h1>Hello node!! (8081)</h1>');
    res.end('<p>Hello server!!</p>');
})
.listen(8081, () => {
    console.log('8081번 포트에서 서버 대기 중입니다!!');
});

이때 포트 번호가 반드시 달라야 함
단 실무에서는 이런 식으로 서버 여러 개 띄우는 일 드물다.

res.write, res.end에 일일이 html을 적는 것은 비효율적이므로
미리 html 파일 만들어 두는것이 좋다.
그 html 파일 fs 모듈로 읽어서 전송할 수 있다.

<!--server2.html-->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Node.js 웹 서버</title>
</head>
<body>
    <h1>Node.js 웹 서버</h1>
    <p>만들준비되셨나요?</p>
</body>
</html>
// server2.js
const http = require('http');
const fs = require('fs').promises;

http.createServer(async (req, res) => {
    try {
        // 요청이 들어오면 먼저 fs 모듈로 html 파일을 읽음
        // data에 저장된 버퍼를 그대로 클라이언트에 보내면 됨
        const data = await fs.readFile('./server2.html');
        res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
        res.end(data);
    } catch (err) {
        console.error(err);
        res.writeHead(200, {'Content-Type': 'text/plain; charset=utf-8'});
        res.end(err.message);
    }
})
.listen(8081, () => {
    console.log('8081번 포트에서 서버 대기 중입니다.');
});
  • HTTP 상태 코드
    200, 500과 같은 숫자는 HTTP 상태 코드이다.
    res.write 첫 인수로 상태 코드 넣었는데 브라우저는 서버에서 보내주는 상태 코드를 보고 요청 성공/실패 판단
    대표적인 상태 코드 알아보자.
    • 2xx: 성공 알리는 코드. 200(성공), 201(작성됨)

    • 3xx: 리다이렉션(다른 페이지로 이동) 알리는 코드.
      어떤 주소 입력했는데 다슨 주소의 페이지로 넘어갈 때 사용
      301(영구 이동), 302(임시 이동), 304(수정되지 않음)는 요청의 응답으로 캐시를 사용했다는 뜻

    • 4xx: 요청 오류 나타냄. 요청 자체에 오류 있을 때 사용
      400(잘못된 요청), 401(권한 없음), 403(금지됨), 404(찾을 수 없음)

    • 5xx: 서버 오류. 요청은 제대로 왔지만 서버에 오류 생겼을 때.
      이 오류 뜨지 않게 주의해야 함.
      500(내부 서버 오류), 502(불량 게이트웨이), 503(서비스 사용할 수 없음)
  • 응답은 무조건 보내야 함
    요청 처리 과정에서 에러 발생했다고 해서 응답 안보내면 안됨
    요청 성공, 실패했든 응답을 클라이언트로 보내서 요청 마무리됐음을 알려야 함.
    응답 안보내면 클라이언트는 사버로부터 응답오기 계속 기다리다가 일정 시간 후 Timeout(시간 초과) 처리 함

지금까지 모든 요청에 대해 한 가지 응답만 했음.
요청별로 다른 응답 하는 방법 알아보자.

  • REST와 라우팅 사용하기
    서버에 요청 보낼 때 주소 통해 요청의 내용 표현.
    /index.html, /about.html 등

html 말고 css, js또는 이미지 파일도 요청 가능
특정 동작 행하는것도 요청 가능
요청의 내용이 주소로 표현되므로 서버가 이해하기 쉬운 주소 사용하는 것이 좋다. => REST

  • REST
    REpresentational State Transfer
    서버의 자원 정의하고 자원에 대한 주소 지정하는 방법 가리킴
    일종의 약속, 규칙.
    자원은 꼭 파일일 필요는 없고 서버가 행항 수 있는 것들 통틀어 의미

주소는 의미 명확히 전달하기 위한 명사로 구성됨
/user : 사용자 정보에 관한 자원 요청
/post : 게시글에 관한 자원이라고 추측 가능

단순 명사만 있으면 우슨 동작 원하는지 알기 어려우므로 REST에서는 주소 외에도 HTTP 요청 메서드를 사용
폼 데이터 전송시에 사용한 GET, POHTTP 요청 메서드 메서드가 바로 요청 메서드.
PUT, PATCH, DELETE, OPTIONS 등의 메서드도 자주 사용.

HTTP 통신 사용하면 클라이언트가 누구든 상관없이 같은 방식으로 서버와 소통 가능.
ios, 안드로이드, 웹, 다른 서버가 모두 같은 주소로 요청 보냄
즉 서버와 클라이언트가 분리되어 있다는 뜻
분리하면 추후에 서버 확장할 때 클라이언트에 구애받지 않는다.

Rest를 따르는 서버를 RESTful 하다고 표현.
RESTful한 웹 서버를 만들어 보자.
코드 작성하기 전 대략적인 주소를 먼저 설계하는 것이 좋다.

restServer.js 가 핵심 모듈.

const http = require('http');
const fs = require('fs').promises;

const users = {}; // 데이터 저장용 (사용자 정보)

http.createServer(async (req, res) => {
    try {
        console.log(req.method, req.url);
        // req.method로 http 요청 메서드를 구분하고 있음
        if (req.method === 'GET') { // http 요청 메서드가 GET 일때
            
            if (req.url === '/') { // 첫 화면 일때 (주소 구분)
                // 주소 '/' 일때 restFront.html 제공
                const data = await fs.readFile('./restFront.html');
                res.writeHead(200, {'Content-Type' : 'text/html; charset=utf-8'});
              
                // res.end 앞에 return 붙이는 이유
                // 노드도 일반적은 js 문법 따르므로 return 붙이지 않는 한 함수는 종료되지 않음
                // 따라서 뒤에 코드 이어질때 return 써서 명시적으로 함수 종료
                return res.end(data);
            } 
            
            else if (req.url == '/about') { // 주소가 about 일때 about.html 제공
                const data = await fs.readFile('./about.html');
                res.writeHead(200, {'Content-Type' : 'text/html; charset=utf-8'});
                return res.end(data);
            }

            else if (req.url === '/users') {
                res.writeHead(200, {'Content-Type' : 'text/html; charset=utf-8'});
                return res.end(JSON.stringify(users));
            }

            // 주소가 /도 /about도, /users도 아니면
            try {
                const data = await fs.readFile(`.${req.url}`);
                return res.end(data)
            } catch (err) {
                // 주소에 해당하는 라우트를 못 찾았다는 404 Not Found error 발생
            }
        }

        else if (req.method === 'POST') { // 사용자를 새로 저장
            if (req.url === '/user') {
                let body = '';
                // 요청의 body를 stream 형식으로 받음
                req.on('data', (data) => { // 요청의 본문에 들어 있는 데이터를 꺼내기 위한 작업
                    // req와 res도 내부적으로는 스트림으로 되어 있으므로 요청/응답의 데이터가 스트림 형식으로 전달됨
                    body += data;
                });
                // 요청의 body를 다 받은 후 실행 됨
                return req.on('end', () => {
                    console.log('POST 본문(Body):', body);
                    const {name} = JSON.parse(body);
                    const id = Date.now();
                    users[id] = name;
                    res.writeHead(201);
                    res.end('등록 성공');
                });
            }
        }

        else if (req.method === 'PUT') { // 해당 아이디의 사용자 데이터를 수정
            if (req.url.startsWith('/user/')) {
                const key = req.url.split('/')[2];
                let body = '';
                req.on('data', (data) => {
                    body += data;
                });
                return req.on('end', () => {
                    console.log('PUT 본문(Body):', body);
                    users[key] = JSON.parse(body).name;
                    return res.end(JSON.stringify(users));
                });
            }
        }

        else if (req.method === 'DELETE') { // 해당 아이디의 사용자 제거
            if (req.url.startsWith('/user/')) {
                const key = req.url.split('/')[2];
                delete users[key];
                return res.end(JSON.stringify(users));
            }
        }

        res.writeHead(404); // Not Found error
        return res.end('NOT FOUND');
    } catch (err) { // 예기치 못한 에러 발생 시 500 에러가 응답으로 전송 됨
        console.error(err);
        res.writeHead(500, {'Content-Type' : 'text/html; charset=utf-8'});
        res.end(err.message);
    }
})
.listen(8082, () => {
    console.log('8082번 포트에서 서버 대기 중입니다.');
});

REST 서버에서 주의할 점은 데이터가 메모리에 저장되므로 서버를 종료하면 데이터가 소실된다는 것.
데이터를 영구적으로 저장하려면 데이터베이스를 사용해야 함.

  • 쿠키와 세션
    클라이언트에서 보내는 요청에는 한 가지 큰 단점이 있다
    누가 요청을 보내는지 모른다는 것.
    요청을 보내는 IP 주소나 브라우저 정보 받아올 수는 있지만
    여러 컴퓨터가 공통 IP 주소 갖거나, 한 컴퓨터 여러 사람이 사용할 수도 있음.

로그인 구현 시 쿠키와 세션에 대해 알아야 함.
클라이언트가 누군지 기억하기 위하여 서버는 요청에 대한 응답할때 쿠키를 같이 보냄
쿠키는 유효 기간 있고 name=gildong 같이 단순한 '키-값'의 쌍이다.

서버로부터 쿠키가 오면 브라우저는 저장했다가 다음에 요청시마다 쿠키 동봉해보냄.
서버는 요청에 들어있는 쿠키 읽어서 사용자 누군지 파악.

브라우저에 쿠키 있다면 자동 동봉하므로 따로 처리할 필요 없음
서버에서 브라우저로 보낼대만 코드 작성해 처리하면 됨.

즉 서버는 미리 클라이언트에 요청자를 추정할 만한 정보를 쿠키로 만들어 보내고, 그 다음부터는 클라이언트로부터 쿠키를 받아 요청자를 파악.

쿠키가 요청자가 누구인지 추적하고 있는것.

쿠키는 요청 헤더에 담겨 전송 됨.
브라우저는 응답의 헤더(set-Cookie)에 따라 쿠키 저장함

서버에서 직접 쿠키 만들어 요청자의 브라우저에 넣어보자.

const http = require('http');

// createServer 메서드의 콜백에서는 req 객체에 담겨있는 쿠키를 가져옴
http.createServer((req, res) => {
    // 쿠키는 req.headers.cookie 에 들어있다.
    // req.headers는 요청의 헤더를 의미함.
    // req.url은 주소의 path와 search 부분을 알림
    console.log(req.url, req.headers.cookie); 

    // 응답의 헤더에 쿠키 기록하기 위해 writeHead 사용
    // Set-Cookie는 브라우저한테 다음과 같은 값의 쿠키를 저장하라는 의미
    // 실제로 응답을 받은 브라우저는 'mycookie=test'라는쿠키를 저장
    res.writeHead(200, {'Set-Cookie' : 'mycookie=test'});
    res.end('Hello Cookie');
})
.listen(8083, () => {
    console.log('8083번 포트에서 서버 대기 중입니다.');
})

쿠키는 name=tom;year=2022 처럼 문자열 형식으로 존재하고 세미콜론으로 구분됨.

파비콘(favicon)은 웹 사이트 탭에 보이는 이미지를 뜻함
브라우저는 파비콘이 뭔지 html에서 유추할 수 없으면 /favicon.ico를 요청한다.

첫번째 요청(/)을 보내기 전에는 브라우저는 쿠키 정보 모름.

요청 두개를 통해 서버가 제대로 쿠키를 심었음을 확인할 수 있다.
서버는 응답의 헤더에 mycookie=test 라는 쿠키를 심도록 브라우저에 명령(Set-Cookie)했다.

아직까지는 단순히 쿠키만 심었을 뿐 그 쿠키가 나인지 식별할 수 없다.
사용자를 식별하는 방법을 알아보자.

// cookie2.js
const http = require('http');
const fs = require('fs').promises;
const url = require('url');
const qs = require('querystring');

// 쿠키는 문자열. 이를 쉽게 사용하기 위해 js객체 형식으로 바꾸는 함수
// 문자열을 객체로 바꾸는 함수! 내부 내용 이해 안해도 됨
// mycookie=test 를 {mycookie: 'test'} 로 바꾼다
const parseCookies = (cookie = '') =>
    cookie
    .split(';')
    .map(v => v.split('='))
    .reduce((acc, [k,v]) => {
        acc[k.trim()] = decodeURIComponent(v);
        return acc;
    }, {});

http.createServer(async (req, res) => {
    const cookies = parseCookies(req.headers.cookie);
   // 주소가 /login으로 시작할 경우에는 url과 querystring 모듈로 각각 주소와
   // 주소에 딸려오는 query를 분석함. 
    if (req.url.startsWith('/login')) { 
        const {query} = url.parse(req.url);
        const {name} = qs.parse(query);
        const expires = new Date();
        // 쿠키 만료시간 지금으로부터 5분 뒤로 설정
        expires.setMinutes(expires.getMinutes() + 5);
        // 302 응답 코드, 리다이렉트 주소와 함께 쿠키를 헤더에 넣음
        res.writeHead(302, {
            Location: '/',
            // 브라우저는 이 응답 코드 보고 페이지를 해당 주소로 리다이렉트 함
            // 헤더에 한글 못 쓰므로 인코딩
            'Set-Cookie': `name=${encodeURIComponent(name)}; Expires= ${expires.toGMTString()}; HttpOnly; Path=/`,
        });
        res.end();
    } 
    
    // 그 외 (/로 접속했을 때 등) 먼저 쿠키가 있는지 확인
    // 없다면 로그인 할 수 있는 페이지 보냄
    
    // 쿠키 있다면 로그인 한 상태로 간주하여 인사말 보냄
    // name 이라는 쿠키가 있는 경우
    else if (cookies.name) {
        res.writeHead(200, {"Content-Type" : 'text/plain; charset=utf-8'});
        res.end(`${cookies.name}님 안녕하세요`);
    } 
    else { // 처음 방문한 겨우 쿠키가 없으므로 cookie2.html 이 전송 됨
        try {
            const data = await fs.readFile('./cookie2.html');
            res.writeHead(200, {"Content-Type" : 'text/html; charset=utf-8'});
            res.end(data);
        } catch (err) {
            res.writeHead(500, {"Content-Type" : 'text/plain; charset=utf-8'});
            res.end(err.message);
        }
    }
})
.listen(8084, () => {
    console.log('8084번 포트에서 서버 대기 중입니다.');
})

Set-Cookie로 쿠키를 설정할 때 만료 시간(Expires), HttpOnly, Path 같은 옵션을 부여할 수 있다.
옵션 사이에 ;를 써서 구분한다.

로그인을 하고 새로고침을 해도 로그인이 유지된다.
이 방법은 상당히 위험하다.
쿠키도 노출되어있고 쿠키가 조작될 위험도 있다.
따라서 개인정보를 쿠키에 넣어두는 것은 적절하지 못한 방법이다.

// session.js
const http = require('http');
const fs = require('fs');
const url = require('url');
const qs = require('querystring');

const parseCookies = (cookie = '') =>
    cookie
    .split(';')
    .map(v => v.split('='))
    .reduce((acc, [k,v]) => {
        acc[k.trim()] = decodeURIComponent(v);
        return acc;
    }, {});

const session = {};

http.createServer(async (req, res) => {
    const cookies = parseCookies(req.headers.cookie);
    if (req.url.startsWith('/login')) {
        const {query} = url.parse(req.url);
        const {name} = qs.parse(query);
        const expires = new Date();
        // 쿠키 만료시간 지금으로부터 5분 뒤로 설정
        expires.setMinutes(expires.getMinutes() + 5);

        const uniqueInt = Date.now();
        session[uniqueInt] = {
            name,
            expires,
        };
        res.writeHead(302, {
            Location: '/',
            'Set-Cookie' : `session=${uniqueInt}; Expires=${expires.toGMTString()}; HttpOnly; Path=/`,
        });
        res.end();
    }
    // 세션 쿠키가 존재하고, 만료기간이 지나지 않았다면
    else if (cookies.session && session[cookies.session].expires > new Date()) {
        res.writeHead(200, {"Content-Type" : 'text/plain; charset=utf-8'});
        res.end(`${session[cookies.session].name}님 안녕하세요`);
    }
    else {
        try {
            const data = await fs.readFile('./cookie2.html');
            res.writeHead(200, {"Content-Type" : 'text/plain; charset=utf-8'});
            res.end(data);
        } catch (err) {
            res.writeHead(500, {"Content-Type" : 'text/plain; charset=utf-8'});
            res.end(err.message);
        }
    }
})
.listen(8085, () => {
    console.log('8085번 포트에서 서버 대기중입니다');
})

cookie2.js와 달라진 부분은 쿠키에 이름을 담아서 보내는 대신,
uniqueInt 라는 숫자 값을 보낸다.

사용자의 이름과 만료시간은 uniqueInt 속성명 아래에 있는 session 객체에 대신 저장한다.

이 방식이 세션이다.
서버에 사용자 정보를 저장하고 클라이어트와는 세션 아이디로만 소통한다.
세션 아이디는 꼭 쿠키를 사용해서 주고 받지 않아도 된다.
하지만 쿠키가 제일 간단하기 때문에 많은 웹 사이트가 쿠키를 사용한다.
앞으로도 쿠키를 사용해 세션 아이디를 주고 받는 식으로 진행.
세션을 위해 사용하는 쿠키를 세션 쿠키라고 부른다.

실제 배포용 서버에서는 서버가 멈추거나 재시작되면 메모리에 저장된 변수가 초기화되게 때문에 세션에 이와같이 변수 저장하지는 않는다.
서버 메모리 부족하면 세션 저장 못하는 경우도 생길수 있음.

따라서 보통 Redis, Memcached 같은 데이터베이스에 넣어둔다.

서비스를 만들때마다 쿠키와 세션을 직접 구현할 수는 없고 이미 검증된 모듈을 사용하는 것이 좋다.

  • cluster
    cluster 모듈은 기본적으로 싱글 프로세스로 동작하는 노드가 CPU 코어를 모두 사용할 수 있게 해준다.
    포트를 공유하는 노드 프로세스를 여려 개 둘 수 있으므로 요청 많이 들어왔을 때 병렬로 실행된 서버의 개수만큼 요청 분산 됨.
    즉 서버에 무리가 덜 간다.
    클러스터링을 해보자.
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
  console.log(`마스터 프로세스 아이디: ${process.pid}`);
  // CPU 개수만큼 워커를 생산
  for (let i = 0; i < numCPUs; i += 1) {
    cluster.fork();
  }
  // 워커가 종료되었을 때
  cluster.on('exit', (worker, code, signal) => {
    console.log(`${worker.process.pid}번 워커가 종료되었습니다.`);
    console.log('code', code, 'signal', signal);
  });
} else {
  // 워커들이 포트에서 대기
  http.createServer((req, res) => {
    res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
    res.write('<h1>Hello Node!</h1>');
    res.end('<p>Hello Cluster!</p>');
    setTimeout(() => { // 워커가 존재하는지 확인하기 위해 1초마다 강제 종료
        process.exit(1);
    }, 1000);
  }).listen(8086);

  console.log(`${process.pid}번 워커 실행`);
}


클러스터에는 마스터 프로세스와 워커 프로세스가 존재한다.
마스터 프로세스는 CPU 개수만큼 워커 프로세스 만들고 8086 포트에서 대기.
요청 들어오면 워커 프로세스들에게 요청을 분배.
워커 프로세스가 실질적으로 일을 하는 프로세스이다.

실무에서는 직접 cluster 모듈을 구현하기보다 pm2등의 모듈로 cluster 기능을 사용한다.

  • Express 모듈: 서버의 여러 주소들을 관리하고 쿠키, 세션등을 쉽게 관리하도록 도와주는 모듈이다.

0개의 댓글