๐ŸŒ HTTP ๋ชจ๋“ˆ

์ง€์€ยท2022๋…„ 10์›” 14์ผ
0

Node.js Library

๋ชฉ๋ก ๋ณด๊ธฐ
6/14

์‹œ์ž‘ํ•˜๊ธฐ์— ์•ž์„œ, nodemon๊ณผ serve๋ผ๋Š” ๋„๊ตฌ์— ๋Œ€ํ•ด ์•Œ์•„๋ณด์ž.

nodemon

: Node.js ๊ธฐ๋ฐ˜์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ฐœ๋ฐœํ•  ๋•Œ, ํŒŒ์ผ์˜ ์ฝ”๋“œ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์ž๋™์œผ๋กœ ์žฌ์‹คํ–‰์‹œ์ผœ์ฃผ๋Š” ๋ชจ๋“ˆ
โžก๏ธ ์›๋ž˜ ํŒŒ์ผ์ด ์ˆ˜์ •๋  ๋•Œ๋งˆ๋‹ค ์„œ๋ฒ„๋ฅผ ์žฌ์‹คํ–‰์‹œ์ผœ์ค˜์•ผ ํ•˜๋Š”๋ฐ, ์ด๋ฅผ ์ž๋™์œผ๋กœ ํ•ด์ค€๋‹ค.
nodemon

nodemon์œผ๋กœ ์„œ๋ฒ„ ์‹คํ–‰ ๋ฐฉ๋ฒ• 1 - npm

  1. nodemon์„ ์„ค์น˜ํ•œ๋‹ค.
npm install nodemon
  1. package.json ํŒŒ์ผ์˜ "script"์— ์•„๋ž˜ ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ , npm start ๋ช…๋ น์–ด๋กœ ์„œ๋ฒ„๋ฅผ ์‹คํ–‰ํ•œ๋‹ค.
"start": "nodemon server.js"

nodemon์œผ๋กœ ์„œ๋ฒ„ ์‹คํ–‰ ๋ฐฉ๋ฒ• 2 - npx

npx nodemon ๋ช…๋ น์–ด๋กœ ์„œ๋ฒ„ ํŒŒ์ผ์„ ์‹คํ–‰ํ•œ๋‹ค.

npx nodemon server.js

serve

: ํŠน์ • ํฌํŠธ์—์„œ ํด๋ผ์ด์–ธํŠธ๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ๋ชจ๋“ˆ

# client/index.html๋ฅผ ํฌํŠธ ๋ฒˆํ˜ธ 80์—์„œ ์‹คํ–‰ํ•œ๋‹ค.
npx serve -l 80 client/

์›น ์„œ๋ฒ„

๋ธŒ๋ผ์šฐ์ €์—๋Š” fetch์™€ ๊ฐ™์ด HTTP ์š”์ฒญ์„ ๋ณด๋‚ด๋Š” ๋„๊ตฌ๊ฐ€ ๊ธฐ๋ณธ์ ์œผ๋กœ ๋‚ด์žฅ๋˜์–ด ์žˆ๋‹ค.
์„œ๋ฒ„๋Š” ํด๋ผ์ด์–ธํŠธ(๋ธŒ๋ผ์šฐ์ €)์˜ HTTP ์š”์ฒญ์— ๋งž๊ฒŒ ์‘๋‹ต์„ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋„๋ก ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด์•ผ ํ•œ๋‹ค.
โžก๏ธ ์ด๋ ‡๊ฒŒ HTTP ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๊ณ  ์‘๋‹ต์„ ๋ณด๋‚ด์ฃผ๋Š” ํ”„๋กœ๊ทธ๋žจ์„ ์›น ์„œ๋ฒ„๋ผ๊ณ  ํ•œ๋‹ค.

HTTP ๋ชจ๋“ˆ

: HTTP ์š”์ฒญ๊ณผ ์‘๋‹ต์„ ๋‹ค๋ฃจ๋Š” ์›น ์„œ๋ฒ„๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ๋ชจ๋“ˆ


HTTP ๋ชจ๋“ˆ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ

const http = require("http");

์„œ๋ฒ„ ์ƒ์„ฑํ•˜๊ธฐ

createServer()

๋ชจ๋“  node ์›น ์„œ๋ฒ„ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ ์›น ์„œ๋ฒ„ ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค์–ด์•ผ ํ•œ๋‹ค.
createServer() ๋ฉ”์†Œ๋“œ๋ฅผ ์ด์šฉํ•ด์„œ ์›น ์„œ๋ฒ„ ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ ๋‹ค.

const server = http.createServer((request, response) => {
  // ์š”์ฒญ ํ•ธ๋“ค๋ง
});
  • ํด๋ผ์ด์–ธํŠธ์—์„œ ์„œ๋ฒ„๋กœ HTTP ์š”์ฒญ์ด ์˜ฌ ๋•Œ๋งˆ๋‹ค, createServer() ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋œ๋‹ค.
  • ์„œ๋ฒ„์— ์š”์ฒญ์ด ์˜ค๋ฉด, node๋Š” createServer()์˜ ์ฝœ๋ฐฑ ํ•จ์ˆ˜์— request์™€ response ๊ฐ์ฒด๋ฅผ ์ „๋‹ฌํ•˜๋ฉฐ, ์š”์ฒญ ํ•ธ๋“ค๋Ÿฌ ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค.
  • ์š”์ฒญ์„ ์‹ค์ œ๋กœ ์ฒ˜๋ฆฌํ•˜๋ ค๋ฉด server ๊ฐ์ฒด์— listen() ๋ฉ”์†Œ๋“œ๋ฅผ ํ˜ธ์ถœํ•ด์•ผ ํ•œ๋‹ค.

์„œ๋ฒ„ ์‹คํ–‰ํ•˜๊ธฐ

server.listen()

listen() ๋ฉ”์†Œ๋“œ๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ๋Š” ์„œ๋ฒ„์—์„œ ์‚ฌ์šฉํ•˜๊ณ ์ž ํ•˜๋Š” ํฌํŠธ ๋ฒˆํ˜ธ๋ฅผ ์ „๋‹ฌ์ธ์ž๋กœ ์ „๋‹ฌํ•œ๋‹ค.

server.listen(4999);

์•„๋ž˜์ฒ˜๋Ÿผ ๋‹ค๋ฅธ ์ „๋‹ฌ์ธ์ž๋„ ํ•จ๊ป˜ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ๋‹ค.

const PORT = 4999;
const ip = 'localhost';
...
server.listen(PORT, ip, () => {
  console.log(`Server is Running at ${ip}:${PORT}`);
  // Server is Running at localhost:4999
});

์š”์ฒญ ์ฒ˜๋ฆฌํ•˜๊ธฐ

request.method & request.url

์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•  ๋•Œ, ์šฐ์„  ๋ฉ”์†Œ๋“œ์™€ URL์„ ํ™•์ธํ•œ ํ›„, ์ด์— ๋Œ€ํ•œ ์ ์ ˆํ•œ ์ž‘์—…์„ ์‹คํ–‰ํ•˜๋ฉด ๋œ๋‹ค.
request ๊ฐ์ฒด์˜ method, url ํ”„๋กœํผํ‹ฐ์— ์ ‘๊ทผํ•˜์—ฌ ์š”์ฒญ์˜ HTTP ๋ฉ”์†Œ๋“œ์™€ URL์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

  • request.method : HTTP ๋ฉ”์†Œ๋“œ/๋™์‚ฌ
  • request.url : ์ „์ฒด URL์—์„œ ์„œ๋ฒ„, ํ”„๋กœํ† ์ฝœ, ํฌํŠธ๋ฅผ ์ œ์™ธํ•œ ๊ฒƒ์œผ๋กœ, ์„ธ ๋ฒˆ์งธ ์Šฌ๋ž˜์‹œ ์ดํ›„์˜ ๋‚˜๋จธ์ง€ ์ „๋ถ€

์š”์ฒญ ํ—ค๋”

request.headers

request ๊ฐ์ฒด์˜ headers ๊ฐ์ฒด๋ฅผ ํ†ตํ•ด ์š”์ฒญ์˜ ํ—ค๋”์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋‹ค.

console.log(request.headers);
// ํ—ค๋” ๊ฐ์ฒด๊ฐ€ ์ถœ๋ ฅ๋œ๋‹ค. {...}

console.log(request.headers['user-agent']);
// ํ—ค๋” ๊ฐ์ฒด์˜ user-agent ์†์„ฑ์˜ ๊ฐ’์ด ์ถœ๋ ฅ๋œ๋‹ค.
// Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36
  • ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์–ด๋–ป๊ฒŒ ํ—ค๋”๋ฅผ ์„ค์ •ํ–ˆ๋Š”์ง€์™€ ๊ด€๊ณ„ ์—†์ด ๋ชจ๋“  ํ—ค๋”๋Š” ์†Œ๋ฌธ์ž๋กœ๋งŒ ํ‘œํ˜„๋œ๋‹ค.

์š”์ฒญ ๋ฐ”๋””

'data' & 'end' ์ด๋ฒคํŠธ

POST๋‚˜ PUT ์š”์ฒญ์„ ๋ฐ›์„ ๋•Œ๋Š”, ์š”์ฒญ ๋ฐ”๋””์— ์žˆ๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์™€์•ผ ํ•œ๋‹ค.
request ๊ฐ์ฒด๋Š” ReadableStream ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ  ์žˆ๋Š”๋ฐ, ์ด ์ŠคํŠธ๋ฆผ์— 'data'์™€ 'end' ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋ฅผ ๋“ฑ๋กํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์˜ฌ ์ˆ˜ ์žˆ๋‹ค.

let body = [];
request.on('data', (chunck) => { // 'data' ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
  body.push(chunck);
}).on('end', () => { // 'end' ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
  body = Buffer.concat(body).toString();
  // body์— ์š”์ฒญ ๋ฐ”๋”” ์ „์ฒด๊ฐ€ ๋‹ด๊ธด๋‹ค.
});
  1. 'data' ์ด๋ฒคํŠธ๋Š” ์ฒญํฌ(chunck)๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๊ณ , ์ด ์ฒญํฌ๋Š” ๋ฌธ์ž์—ด ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ด๊ณ  ์žˆ๋Š” Buffer ๊ฐ์ฒด์ด๋‹ค.
  2. ์ด ์ฒญํฌ๋ฅผ ๋ฐฐ์—ด์— ์ˆ˜์ง‘ํ•œ ํ›„,
  3. 'end' ์ด๋ฒคํŠธ์—์„œ Buffer์— ์ด์–ด ๋ถ™์ธ ๋‹ค์Œ,
  4. toString() ๋ฉ”์†Œ๋“œ๋ฅผ ์ด์šฉํ•ด ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค.
    โžก๏ธ ์ด์ œ ๋ณ€์ˆ˜ body์—๋Š” ์š”์ฒญ ๋ฐ”๋””์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฌธ์ž์—ด๋กœ ๋‹ด๊ธฐ๊ฒŒ ๋œ๋‹ค.

Buffer ๊ฐ์ฒด

๋ฒ„ํผ ํ˜•ํƒœ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์›๋ž˜์˜ ๋ฌธ์ž๋กœ ๋‹ค์‹œ ๋ฐ”๊พธ๋ ค๋ฉด toString() ๋ฉ”์†Œ๋“œ๋ฅผ ์ด์šฉํ•˜๋ฉด ๋œ๋‹ค.

data.toString()

์‘๋‹ต์˜ HTTP ์ƒํƒœ ์ฝ”๋“œ ๋ณ€๊ฒฝํ•˜๊ธฐ

response.statusCode

HTTP ์ƒํƒœ ์ฝ”๋“œ๋ฅผ ๋”ฐ๋กœ ์„ค์ •ํ•˜์ง€ ์•Š์œผ๋ฉด, HTTP ์ƒํƒœ ์ฝ”๋“œ๋Š” ํ•ญ์ƒ 200์ด๋‹ค.
์‘๋‹ต์˜ HTTP ์ƒํƒœ ์ฝ”๋“œ๋ฅผ ๋ณ€๊ฒฝํ•˜๋ ค๋ฉด response ๊ฐ์ฒด์˜ statusCode ํ”„๋กœํผํ‹ฐ๋ฅผ ์„ค์ •ํ•˜๋ฉด ๋œ๋‹ค.

response.statusCode = 404;
// 404 Not Found, ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ฆฌ์†Œ์Šค๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†๋‹ค๊ณ  ์•Œ๋ ค์ค€๋‹ค.

์‘๋‹ต ํ—ค๋”

response.setHeader()

response ๊ฐ์ฒด์— setHeader() ๋ฉ”์†Œ๋“œ๋กœ ํ—ค๋”๋ฅผ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

response.setHeader('Content-Type', 'application/json');
response.setHeader('X-Powered-By', 'bacon');
  • ์‘๋‹ต ํ—ค๋”๋ฅผ ์„ค์ •ํ•  ๋•Œ, ํ—ค๋” ์ด๋ฆ„์˜ ๋Œ€์†Œ๋ฌธ์ž๋Š” ์ค‘์š”ํ•˜์ง€ ์•Š๋‹ค.
  • ํ—ค๋”๋ฅผ ์—ฌ๋Ÿฌ ๋ฒˆ ์„ค์ •ํ•œ๋‹ค๋ฉด ๋งˆ์ง€๋ง‰์— ์„ค์ •ํ•œ ๊ฐ’์œผ๋กœ ์„ค์ •๋œ๋‹ค.

response.writeHead()

๋ช…์‹œ์ ์œผ๋กœ ์‘๋‹ต ์ŠคํŠธ๋ฆผ์— ํ—ค๋”๋ฅผ ์ž‘์„ฑํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด writeHead() ๋ฉ”์†Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์ƒํƒœ ์ฝ”๋“œ์™€ ํ—ค๋”๋ฅผ ์ž‘์„ฑํ•œ๋‹ค.

response.writeHead(200, {
  'Content-Type': 'application/json',
  'X-Powered-By': 'bacon'
});

โžก๏ธ ํ—ค๋”๋ฅผ ์ž‘์„ฑํ•˜๊ณ  ๋‚˜๋ฉด, ์‘๋‹ต ๋ฐ”๋””(๋ฐ์ดํ„ฐ)๋ฅผ ์ „์†กํ•  ์ˆ˜ ์žˆ๋‹ค.
์ฃผ์˜ํ•  ์ โ—๏ธ ์‘๋‹ต ๋ฐ”๋””์— ๋ฐ์ดํ„ฐ ์ฒญํฌ๋ฅผ ์ž‘์„ฑํ•˜๊ธฐ ์ „์—๋Š” ํ•ญ์ƒ ์ƒํƒœ ์ฝ”๋“œ์™€ ํ—ค๋”๋ฅผ ์„ค์ •ํ•ด์•ผ ํ•œ๋‹ค.


์‘๋‹ต ๋ฐ”๋””

response.write()

response ๊ฐ์ฒด๋Š” WritableStream์ด๋ฏ€๋กœ, ํด๋ผ์ด์–ธํŠธ๋กœ ๋ณด๋‚ด๋Š” ์‘๋‹ต ๋ฐ”๋””๋Š” ์ผ๋ฐ˜์ ์ธ ์ŠคํŠธ๋ฆผ ๋ฉ”์†Œ๋“œ write()๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์ž‘์„ฑํ•œ๋‹ค.

response.write('<html>'); // ์‘๋‹ต ๋ฐ”๋”” ์ž‘์„ฑ
response.write('<body>');
response.write('<h1>Hello, World!</h1>');
response.write('</body>');
response.write('</html>');
response.end(); // ์‘๋‹ต ์ข…๋ฃŒ

response.end() ๋ฉ”์†Œ๋“œ๋Š” ๋ฐ˜๋“œ์‹œ ์‘๋‹ต๋งˆ๋‹ค ํ˜ธ์ถœ๋˜์–ด์•ผ ํ•œ๋‹ค.

response.end()

response.end() ์•ˆ์— ๋ฐ์ดํ„ฐ๋ฅผ ์ž‘์„ฑํ•˜๋ฉด, response.write(data)๋ฅผ ํ˜ธ์ถœํ•œ ๋‹ค์Œ์— response.end()๋ฅผ ํ˜ธ์ถœํ•œ ๊ฒƒ๊ณผ ๊ฐ™๋‹ค.

response.end(<html><body><h1>Hello, World!</h1></body></html>);
// ์‘๋‹ต ๋ฐ”๋””๋ฅผ ์ž‘์„ฑํ•˜๊ณ  ์ข…๋ฃŒ

CORS ์„ค์ •ํ•˜๊ธฐ

ํด๋ผ์ด์–ธํŠธ์—์„œ ํ”„๋ฆฌํ”Œ๋ผ์ดํŠธ ์š”์ฒญ(preflight request)์„ ๋ณด๋‚ด๋ฉด, ์‘๋‹ต ํ—ค๋”์— CORS๋ฅผ ์„ค์ •ํ•˜๋Š” ํ—ค๋”๋ฅผ ๋„ฃ์–ด์„œ ์‘๋‹ตํ•œ๋‹ค.

// CORS ์„ค์ • ํ—ค๋”
const defaultCorsHeader = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type, Accept',
  'Access-Control-Max-Age': 10
};

if(request.method === 'OPTIONS') {
  response.writeHead(200, defaultCorsHeader); // ์‘๋‹ต ํ—ค๋” ์ž‘์„ฑ
  response.end(); // ์‘๋‹ต ์ข…๋ฃŒ
  return; // if๋ฌธ ์ข…๊ฒฐ์‹œํ‚ค๊ธฐ
}

์˜ˆ์ œ

ํด๋ผ์ด์–ธํŠธ์—์„œ ํ…์ŠคํŠธ๋ฅผ ์ž…๋ ฅํ•œ ํ›„,
upper ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ํ…์ŠคํŠธ๋ฅผ ๋Œ€๋ฌธ์ž๋กœ ๋ณ€ํ™˜์‹œ์ผœ์ฃผ๊ณ ,
lower ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ํ…์ŠคํŠธ๋ฅผ ์†Œ๋ฌธ์ž๋กœ ๋ณ€ํ™˜์‹œ์ผœ์ฃผ๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์„œ๋ฒ„

const http = require('http');
const PORT = 4999;
const ip = 'localhost';

// CORS ์„ค์ • ํ—ค๋”
const defaultCorsHeader = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type, Accept',
  'Access-Control-Max-Age': 10
};

// ์„œ๋ฒ„ ์ƒ์„ฑ
const server = http.createServer((request, response) => {
  let body = [];
  
  // ๋ชจ๋“  ์‘๋‹ต์˜ ํ—ค๋”์— CORS ์„ค์ • ํ—ค๋” ์ž‘์„ฑ
  response.writeHead(200, defaultCorsHeader);

  // POST ์š”์ฒญ + url์ด /upper ์ผ๋•Œ
  if(request.method === 'POST' && request.url === '/upper') {
    request.on('data', (chunck) => {
      body.push(chunck);
    }).on('end', () => {
      body = Buffer.concat(body).toString().toUpperCase();
      response.end(body);
    });
    
  // POST ์š”์ฒญ + url์ด /lower ์ผ๋•Œ
  } else if(request.method === 'POST' && request.url === '/lower') {
    request.on('data', (chunck) => {
    body.push(chunck);
    }).on('end', () => {
      body = Buffer.concat(body).toString().toLowerCase();
      response.end(body);
    });
  
  // ๊ทธ ์™ธ์˜ ์š”์ฒญ์€ ์—๋Ÿฌ ์ฒ˜๋ฆฌ
  } else {
    response.statusCode = 404;
    response.end('404 Not Found');
  }
});

// ์„œ๋ฒ„ ์‹คํ–‰
server.listen(PORT, ip, () => {
  console.log(`Server is Running at ${ip}:${PORT}`);
});

๋‚˜์ค‘์— ์ฝ์–ด๋ณด๋ฉด ์ข‹์€ ๊ธ€
https://leejabba.gitbooks.io/node-js/content/http-baa8-b4c8.html#3
https://jongik.tistory.com/31

โ” ํ•™์Šต ํ›„ ๊ถ๊ธˆํ•œ ์ 

  • serve ๋ชจ๋“ˆ์ด ์ •ํ™•ํžˆ ๋ฌด์—‡์ธ์ง€ ์ œ๋Œ€๋กœ ์ดํ•ดํ•˜์ง€ ๋ชปํ–ˆ๋‹ค.
  • ReadableStream, WritableStream ์ด๋ž€?
  • ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ .on()์„ ์ด์šฉํ•˜๋Š” ๊ฑด์ง€?
  • ์š”์ฒญ/์‘๋‹ต ํ—ค๋”์— ์–ด๋–ค ๋‚ด์šฉ๋“ค์ด ์žˆ๋Š”์ง€ ๊ณต๋ถ€ํ•˜๊ณ  ๋ธ”๋กœ๊น…ํ•˜๊ธฐ ๐Ÿ–‹
profile
๋ธ”๋กœ๊ทธ ์ด์ „ -> https://janechun.tistory.com

0๊ฐœ์˜ ๋Œ“๊ธ€