CRA 설치 및 eslint & pritere 설치
$ create-react-app ./
$ npm install -D prettier eslint-config-prettier eslint-plugin-prettierroot 경로에
.eslintrc파일 생성 및 아래 내용 입력(window && mac 같이 사용하는 경우)// .eslintrc
{
"extends": ["react-app", "plugin:prettier/recommended"],
"rules": {
"no-var": "warn", // var 금지
"no-multiple-empty-lines": "warn", // 여러 줄 공백 금지
"no-console": ["warn", { "allow": ["warn", "error"] }], // console.log() 금지
"eqeqeq": "warn", // 일치 연산자 사용 필수
"dot-notation": "warn", // 가능하다면 dot notation 사용
"no-unused-vars": "warn", // 사용하지 않는 변수 금지
"react/destructuring-assignment": "warn", // state, prop 등에 구조분해 할당 적용
"react/jsx-pascal-case": "warn", // 컴포넌트 이름은 PascalCase로
"react/no-direct-mutation-state": "warn", // state 직접 수정 금지
"react/jsx-no-useless-fragment": "warn", // 불필요한 fragment 금지
"react/no-unused-state": "warn", // 사용되지 않는 state
"react/jsx-key": "warn", // 반복문으로 생성하는 요소에 key 강제
"react/self-closing-comp": "warn", // 셀프 클로징 태그 가능하면 적용
"react/jsx-curly-brace-presence": "warn", // jsx 내 불필요한 중괄호 금지
"prettier/prettier": [
"error",
{
"endOfLine": "auto"
}
]
}
}root 경로에
.prettierrc파일 생성 및 아래 내용 입력// .prettierrc
{
"tabWidth": 2,
"endOfLine": "lf",
"arrowParens": "avoid",
"singleQuote": true
}.vscode폴더 생성 뒤 VSCode에 적용할 설정 JSON 추가// .vscode/settings.json
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.tabSize": 2,
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"javascript.format.enable": false,
"eslint.alwaysShowStatus": true,
"files.autoSave": "onFocusChange"
}라우터 && 스타일 컴포넌트 설치(선택사항)
$ npm i react-router-dom
$ npm i styled-components
[카테고리:] IT
다양한 IT 기술에 대한 내용을 공유합니다.
-
![[React] ESlint, Prettier 초기 설정 샘플](https://susukkekki.kr/wp-content/uploads/2017/02/web.png)
[React] ESlint, Prettier 초기 설정 샘플
-
![[JavaScript] 문자열이 공백인지 체크](https://susukkekki.kr/wp-content/uploads/2017/02/web.png)
[JavaScript] 문자열이 공백인지 체크
문자열이 공백인지 체크하는 함수
검색창 구현하다 필요해서 구글링해서 줍줍
/** * 정규표현식을 활용하여 문자열이 공백일경우 'true' 반환 * @param {string} str * @returns {boolean} 공백일경우 'true' 아닐경우 'false' */ function checkSpaceStr(str) { let bln = false; let blankPattern = /^\s+|\s+$/g; if (str.replace(blankPattern, '') === '') { bln = true; } return bln; }References
-
![[Algorithm] 코딩테스트 풀이 – 해시](https://susukkekki.kr/wp-content/uploads/2017/02/web.png)
[Algorithm] 코딩테스트 풀이 – 해시
코딩테스트 플랫폼인 프로그래머스에서 해시 문제 및 풀이를 포스팅해보았다. 언어는 자바스크립트를 기준으로 작성하였다.
해시
해시란 데이터를 다루는 기술중 하나로서 해시함수를 이용하여 임의의 길이를 갖는 데이터를 고정된 길이의 데이터로 매핑하는 것을 말한다.완주하지 못한 선수
문제 설명
수많은 마라톤 선수들이 마라톤에 참여하였습니다. 단 한 명의 선수를 제외하고는 모든 선수가 마라톤을 완주하였습니다.
마라톤에 참여한 선수들의 이름이 담긴 배열 participant와 완주한 선수들의 이름이 담긴 배열 completion이 주어질 때, 완주하지 못한 선수의 이름을 return 하도록 solution 함수를 작성해주세요.제한 사항
- 마라톤 경기에 참여한 선수의 수는 1명 이상 100,000명 이하입니다.
- completion의 길이는 participant의 길이보다 1 작습니다.
- 참가자의 이름은 1개 이상 20개 이하의 알파벳 소문자로 이루어져 있습니다.
- 참가자 중에는 동명이인이 있을 수 있습니다.
입출력 예
participant completion return [“leo”, “kiki”, “eden”] [“eden”, “kiki”] “leo” [“marina”, “josipa”, “nikola”, “vinko”, “filipa”] [“josipa”, “filipa”, “marina”, “nikola”] “vinko” [“mislav”, “stanko”, “mislav”, “ana”] [“stanko”, “ana”, “mislav”] “mislav” 입출력 예 설명
예제 #1 “leo”는 참여자 명단에는 있지만, 완주자 명단에는 없기 때문에 완주하지 못했습니다.
예제 #2 “vinko”는 참여자 명단에는 있지만, 완주자 명단에는 없기 때문에 완주하지 못했습니다.
예제 #3 “mislav”는 참여자 명단에는 두 명이 있지만, 완주자 명단에는 한 명밖에 없기 때문에 한명은 완주하지 못했습니다.
풀이
코드
const participant = ["mislav", "stanko", "mislav", "ana"]; const completion = ["stanko", "ana", "mislav"]; function solution(participant, completion) { let answer = ""; // 각 배열을 이름순으로 정렬 participant.sort(); completion.sort(); // 루프를 돌며 이름이 일치하지 않을 경우 결과를 담고 리턴 for (let i = 0; i < participant.length; i++) { if (participant[i] !== completion[i]) { answer = participant[i]; break; } } return answer; } console.log(solution(participant, completion));설명
반복문을 돌며 이름을 비교할 것이며 그 전에 동명이인이 있을 수 있기 때문에, 각 배열에
sort함수를 사용하여 이름순으로 정렬한다. 반복문이 돌면서 이름을 매칭해보고 이름이 다를경우 해당 참여자를 응답값에 담고, 완주하지 못한 선수는 단 한명이기 때문에 값을 리턴하고 로직을 빠져나온다.위장
문제 설명
스파이들은 매일 다른 옷을 조합하여 입어 자신을 위장합니다.
예를 들어 스파이가 가진 옷이 아래와 같고 오늘 스파이가 동그란 안경, 긴 코트, 파란색 티셔츠를 입었다면 다음날은 청바지를 추가로 입거나 동그란 안경 대신 검정 선글라스를 착용하거나 해야 합니다.종류 이름 얼굴 동그란 안경, 검정 선글라스 상의 파란색 티셔츠 하의 청바지 겉옷 긴 코트 스파이가 가진 의상들이 담긴 2차원 배열 clothes가 주어질 때 서로 다른 옷의 조합의 수를 return 하도록 solution 함수를 작성해주세요.
제한 사항
- clothes의 각 행은 [의상의 이름, 의상의 종류]로 이루어져 있습니다.
- 스파이가 가진 의상의 수는 1개 이상 30개 이하입니다.
- 같은 이름을 가진 의상은 존재하지 않습니다.
- clothes의 모든 원소는 문자열로 이루어져 있습니다.
- 모든 문자열의 길이는 1 이상 20 이하인 자연수이고 알파벳 소문자 또는 ‘_’ 로만 이루어져 있습니다.
- 스파이는 하루에 최소 한 개의 의상은 입습니다.
입출력 예
clothes return [[“yellowhat”, “headgear”], [“bluesunglasses”, “eyewear”], [“green_turban”, “headgear”]] 5 [[“crowmask”, “face”], [“bluesunglasses”, “face”], [“smoky_makeup”, “face”]] 3 입출력 예 설명
예제 #1 headgear에 해당하는 의상이 yellow_hat, green_turban이고 eyewear에 해당하는 의상이 blue_sunglasses이므로 아래와 같이 5개의 조합이 가능합니다.
예제 #2 face에 해당하는 의상이 crow_mask, blue_sunglasses, smoky_makeup이므로 아래와 같이 3개의 조합이 가능합니다.
- crow_mask
- blue_sunglasses
- smoky_makeup
풀이
코드
const clothes = [ ["yellowhat", "headgear"], ["bluesunglasses", "eyewear"], ["green_turban", "headgear"], ]; function solution(clothes) { let answer = 1; // 리턴값, 마지막에 곱셉을 하기 때문에 0으로 하면 안됨 let obj = {}; // 의상 종류마다 갯수를 담을 객체 for (let i = 0; i < clothes.length; i++) { // 중복되는 키값이 존재한다면 1 더함 if (obj[clothes[i][1]] >= 1) { obj[clothes[i][1]] += 1; // 처음 등장하는 의상이면 1로 초기화 } else { obj[clothes[i][1]] = 1; } } // 경우의 수 곱셈, (2 + 1) X (1 + 1) for (let key in obj) { answer *= obj[key] + 1; } // 아무것도 입지 않는 경우는 빼줌 return answer - 1; } console.log(solution(clothes));설명
리턴값은 다른 옷의 조합수로서 이럴땐 경우의 수 공식을 사용하면 된다. 예제 #1로 예를 들면 모자의 경우의수는 2개며 안경의 경우의수는 1개다.
- headgear: yellowhat, green_turban
- eyewear: bluesunglasses
경우의 수 공식에 따라,
2 X 1 = 2가 되야하지만, 모자나 안경을 안쓰는 경우도 있으니 각 경우의 수에 1씩 더해주서(2 + 1) X (1 + 1) = 6이 된다. 여기서 아무것도 입지 않는 경우는 빼야 되므로 최종 경우의 수에 1을 빼준다. 최종적으로(2 + 1) X (1 + 1) - 1 = 5수식이 된다. 이 방법으로 코드를 위처럼 작성하면 된다. -
![[React] Puppeteer, React, Express를 활용한 크롤러 및 Heroku에 배포](https://susukkekki.kr/wp-content/uploads/2017/02/web.png)
[React] Puppeteer, React, Express를 활용한 크롤러 및 Heroku에 배포
Puppeteer를 활용하면 웹 크롤러 패널을 만들 수 있다. 클라이언트는 React, 서버는 Express를 사용하고 로컬에서 작업이 끝나면 Heroku에 배포까지 해보자. 결과물과 소스는 아래에서 확인할 수 있다.
결과물은 헤로쿠에 배포되었기 때문에 처음 페이지가 열릴때 로딩시간이 10초에서 30초정도 걸릴수 있다.
Puppeteer
Puppeteer는 Google Chrome 개발팀에서 직접 개발한 Chrome(혹은 Chromium) 렌더링 엔진을 이용하는 node.js 라이브러리이다. Puppeteer는 headless 모드를 지원하며, 이는 브라우저를 띄우지 않고 렌더링 작업을 가상으로 진행하고 실제 브라우저와 동일하게 동작한다. Puppeteer는 다양한 기능을 가지고 있으며 아래와 같은 기능들이 있다.
- 웹페이지의 스크린샷과 PDF를 생성한다.
- SPA(단일 페이지)를 크롤링하고 미리 렌더링된 콘텐츠(예: SSR)를 생성한다.
- 폼 입력, UI 테스트, 키보드 입력 등을 자동화 할 수 있다.
- 최신 자바스크립트 및 브라우저 기능을 이용해 최신버전의 크롬에서 직접 테스트할 수 있다.
- 사이트의 Timeline Trace를 기록하여 성능이나 문제를 진단할 수 있다.
- 크롬 확장 프로그램을 테스트 할 수 있다.
프로젝트 초기화
이 부분은 [Express] Express + React 연동 및 Heroku에 배포하기 포스팅과 비슷한 부분이 많기 때문에 각 단계의 추가 설명 없이 진행하도록 하겠다.
디렉토리 생성 및 필요 모듈 설치
디렉토리를 생성하고 이그노 파일을 생성한 뒤 npm 초기화 및 필요한 모듈을 설치한다.
$ mkdir my-app $ cd my-app $ echo node_modules > .gitignore $ npm init -y $ npm install express nodemon concurrently이제 서버로 사용할
index.js파일을 생성하고 아래 내용을 입력한다.// express 모듈 불러오기 const express = require("express"); // express 객체 생성 const app = express(); // 기본 포트를 app 객체에 설정 const port = process.env.PORT || 5000; app.listen(port); // 미들웨어 함수를 특정 경로에 등록 app.use("/api/data", function (req, res) { res.json({ greeting: "Hello World" }); }); console.log(`server running at http ${port}`);package.json파일을 열고scripts항목에"start": "nodemon index.js"를 추가한다."scripts": { "start": "nodemon index.js" }리액트 초기화
이제 클라이언트로 사용할 리액트를 생성하며, 이름은
client로 한다.$ create-react-app client --use-npm --template typescript프록시 설정
설치가 완료되면
client디렉토리로 이동해서에 아래 모듈을 설치한다.$ cd client $ npm install http-proxy-middleware설치한 뒤
/client/src/디렉토리로 가서setupProxy.js파일을 생성하고 아래 코드를 입력해준다.const { createProxyMiddleware } = require("http-proxy-middleware"); module.exports = function (app) { app.use( createProxyMiddleware("/api/data", { target: "http://localhost:5000", changeOrigin: true, }) ); };서버(express), 클라이언트(react) 동시 시작 설정
루트로 가서
package.json의scripts항목을 아래처럼 수정해준다."scripts": { "start": "nodemon index.js", "dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"", "dev:server": "npm start", "dev:client": "cd client && npm start" }이제 아래 명령어로 서버와 클라이언트를 동시에 시작할 수 있다.
$ npm run dev이제 작업하기 위한 전반적인 준비가 끝났다. 우선 클라이언트 영역부터 작업해보자.
클라이언트에서 요청 작업
검색 폼 및 리스트 추가
/client/src/디렉토리에components폴더를 생성하고SearchForm.tsx,SearchList.tsx파일을 생성하고 각각 아래처럼 입력해 준다.SearchForm.tsx
import React from "react"; const SearchForm = () => { return ( <div className="form"> <input type="text" className="form-text" /> <button type="button" className="form-btn" onClick={() => { fetch("api/data") .then((res) => { return res.json(); }) .then((data) => { console.log(data); }); }} > search </button> </div> ); }; export default SearchForm;SearchList.tsx
import React from "react"; const SearchList = () => { return ( <div className="card-list"></div> ); }; export default SearchList;App.tsx은 아래처럼 변경해준다.import React from "react"; import SearchForm from "./components/SearchForm"; import SearchList from "./components/SearchList"; function App() { return ( <div className="App"> <SearchForm /> <SearchList /> </div> ); } export default App;search버튼을 클릭하면fetch함수로 서버(http://localhost:5000/api/data)에 요청을 하게 되고 응답값으로 콘솔창에{ greeting: "Hello World" }가 출력되는 것을 확인할 수 있다.fetch 함수를 App 컴포넌트로 이동
이제 검색키워드를 서버에 보내기 위헤
SearchForm,App컴포넌트를 아래처럼 수정해 준다.SearchForm.tsx
import React, { useState } from "react"; const SearchForm = (props: { getData: any }) => { const { getData } = props; const [keyword, setKeyword] = useState(""); return ( <div className="form"> <input type="text" className="form-text" onChange={(e: any) => { setKeyword(e.target.value); }} onKeyPress={(e: any) => { if (e.charCode === 13) { if (keyword) { getData(keyword); } } }} /> <button type="button" className="form-btn" onClick={() => { if (keyword) { getData(keyword); } }} > search </button> </div> ); }; export default SearchForm;App.tsx
import React from "react"; import SearchForm from "./components/SearchForm"; import SearchList from "./components/SearchList"; function App() { const getData = (keyword: string) => { console.log("검색 키워드: " + keyword); fetch(`api/data?keyword=${keyword}`) .then((res) => { return res.json(); }) .then((data) => { console.log(data); }); }; return ( <div className="App"> <SearchForm getData={getData} /> <SearchList /> </div> ); } export default App;SearchForm에 있던fetch함수를 상위App컴포넌트의getData함수에 넣어놨다. 이 함수를SearchForm에 전달하였고, 검색버튼을 클릭하면getData가 실행되며,input태그의 검색 키워드가 쿼리스트링에 할당되어 서버에 전달되게 된다. 검색폼에서 엔터를 눌러도 요청할 수 있도록onKeyPress이벤트도 추가해 주자. 이제 응답값을 받기 위해 서버 작업을 해보자.서버에서 요청 받기
이제 루트로 가서
index.js를 아래처럼 수정해 준다.// express 모듈 불러오기 const express = require("express"); // express 객체 생성 const app = express(); // 기본 포트를 app 객체에 설정 const port = process.env.PORT || 5000; app.listen(port); // 미들웨어 함수를 특정 경로에 등록 app.use("/api/data", function (req, res) { console.log("검색 키워드: " + req.query.keyword); res.json({ greeting: "Hello World" }); }); console.log(`server running at http ${port}`);요청을 하면 서버 터미널에 검색 키워드가 출력될 것이다. 위 코드를 보면 미들웨어 함수에서 요청값(
req.query.keyword)을 받기 때문이다.Puppeteer 설치
이제 브라우저로 검색하기 위해 루트 디렉토리에 Puppeteer를 설치해주자.
$ npm install puppeteer- Puppeteer는 기본적으로 Chrome 혹은 Chromium 런더링 엔진을 사용하기 때문에 기본적으로 Chromium 브라우저를 내장하고 있다.
- 따로 Chromium 브라우저를 다운받지 않으려면
$ npm install puppeteer-core명령어를 사용하면 되며, Puppeteer는 로컬에 있는 Chrome 혹은 Chromium을 사용하게 될 것이다.
검색해보기
Puppeteer를 설치했으면 이제 브라우저를 실행해 검색을 해보자.
index.js를 아래처럼 수정해준다.// express 모듈 불러오기 const express = require("express"); // express 객체 생성 const app = express(); // 기본 포트를 app 객체에 설정 const port = process.env.PORT || 5000; app.listen(port); // 미들웨어 함수를 특정 경로에 등록 app.use("/api/data", function (req, res) { console.log("검색 키워드: " + req.query.keyword); openBrowser(req.query.keyword); }); console.log(`server running at http ${port}`); // puppeteer 모듈 불러오기 const puppeteer = require("puppeteer"); /** * 브라우저 오픈 함수 * @param {string} keyword 검색 키워드 */ async function openBrowser(keyword) { // 브라우저 실행 및 옵션, 현재 옵션은 headless 모드 사용 여부 const browser = await puppeteer.launch({ headless: false }); // 브라우저 열기 const page = await browser.newPage(); // 포탈로 이동 await page.goto("https://www.google.com/"); // 키워드 입력 await page.type("input[class='gLFyf gsfi']", keyword); // 키워드 검색 await page.type("input[class='gLFyf gsfi']", String.fromCharCode(13)); }puppeteer모듈을 불러온 뒤openBrowser함수를 추가하였으며, 포탈 이동 및 응답값을 받기 위해async함수로 감싸주었다. 브라우저 실행 옵션에서headless모드를true로 설정하면 브라우저가 화면에 노출이 되지 않고 백그라운드에서 작동된다. 지금은 브라우저 작동 순서를 보기 위해 임시로false로 설정해 준다. 위처럼 수정해 준 뒤 클라이언트 화면으로 가서 검색해 보면 아래 순서대로 작동된다.- Chromium 브라우저가 실행되고
- Google 사이트로 이동한 뒤
- Google 검색창에 검색 키워드를 넣고
- 엔터를 눌러 검색을 시작한다.
검색 내용 크롤링하기
이제 검색결과를 크롤링을 해보자.
크롤링할 내용 형태
{ title: "제목", link: "링크", text: "내용", kategorie: "카테고리" }크롤링으로 가져올 정보는 위 형태로 가져올 것이며,
index.js를 아래처럼 코드를 수정한다.// express 모듈 불러오기 const express = require("express"); // express 객체 생성 const app = express(); // 기본 포트를 app 객체에 설정 const port = process.env.PORT || 5000; app.listen(port); // 미들웨어 함수를 특정 경로에 등록 app.use("/api/data", async function (req, res) { console.log("검색 키워드: " + req.query.keyword); const resultList = await openBrowser(req.query.keyword); console.log(resultList); res.json(resultList); }); console.log(`server running at http ${port}`); // puppeteer 모듈 불러오기 const puppeteer = require("puppeteer"); /** * 브라우저 오픈 함수 * @param {string} keyword 검색 키워드 * @return {array} 검색 결과 */ async function openBrowser(keyword) { // 브라우저 실행 및 옵션, 현재 옵션은 headless 모드 사용 여부 const browser = await puppeteer.launch({ headless: true }); // 브라우저 열기 const page = await browser.newPage(); // 포탈로 이동 await page.goto("https://www.google.com/"); // 키워드 입력 await page.type("input[class='gLFyf gsfi']", keyword); // 키워드 검색 await page.type("input[class='gLFyf gsfi']", String.fromCharCode(13)); // 예외 처리 try { // 해당 콘텐츠가 로드될 때까지 대기 await page.waitForSelector("#rso div.g", { timeout: 10000 }); } catch (error) { // 해당 태그가 없을 시 검색결과 없음 반환 console.log("에러 발생: " + error); return [ { title: "검색결과 없음", link: "", text: "", kategorie: "", }, ]; } // 호출된 브라우저 영역 const searchData = await page.evaluate(() => { // 검색된 돔 요소를 배열에 담음 const contentsList = Array.from(document.querySelectorAll("#rso div.g")); let contentsObjList = []; // 검색결과 크롤링 contentsList.forEach((item) => { if (item.className === "g") { const title = item.querySelector("h3"); const link = item.querySelector(".yuRUbf"); const text = item.querySelector(".VwiC3b"); const kategorie = item.querySelector(".iUh30 "); if (title && link && text && kategorie) { contentsObjList.push({ title: title.textContent, // 타이틀 link: link.children[0].href, // 링크 text: text.textContent, // 내용 kategorie: kategorie.textContent, // 카테고리 }); } } }); // 호출된 브라우저 영역 콘솔창에서 확인할 수 있음 console.log(contentsList); // 검색한 엘리먼트 리스트 console.log(contentsObjList); // 검색한 콘텐츠 오브젝트 리스트 return contentsObjList; }); // 브라우저 닫기 browser.close(); // 검색결과 반환 return searchData; }요소 대기
headless모드는 이제true로 설정해준다. 브라우저가 크롤링하는 모습을 직접 확인하고 싶으면false로 그냥 두면 된다. 이제 순서대로 코드를 살펴보자.page.waitForSelector메서드를 추가했으며, 인자로 쿼리 셀렉터와 옵션이 들어간다. 이 메서드는 셀렉터 요소가 로드될 때 까지 대기하며,timeout로 대기 시간을 설정할 수 있다. 대기시간이 끝나도 해당 요소를 로드하지 못하면 에러를 뱉어내며, 이 경우title에 검색결과가 없다는 값을 리턴해 준다.브라우저 영역
page.evaluate메서드는 Puppeteer로 호출한 브라우저에서 실행되는 함수로써 여기다가 크롤링 코드를 작성하면 된다. 구글 검색결과의 각 엘리먼트 셀렉터는#rso div.g이며, 해당 요소들을Array.from메서드를 통해 배열로 담았다. 필요한 정보만 가져오기 위해forEach을 돌려 오브젝트에 내용을 담고 리턴해 준 다음 브라우저는 종료가 된다.검색결과를 응답해주기
이제 이 응답값을 미들웨어 함수에서 받아서 클라이언트의 응답값으로 보내줘야한다. 위 코드의 미들웨어 함수를 보면 콜백함수를
async로 감싸고 결과값을await키워드로 받아 응답값으로 보내주고 있다. 클라이언트의 콘솔창을 보면 크롤링한 리스트를 출력하는걸 확인할 수 있다.연속 검색
지금까지 구현된건 첫 페이지만 크롤링한 것이며, 다음 페이지를 추가로 크롤링을 하려면 아래 순서가 필요하다.
- 검색결과 맨아래 다음버튼이 있는지 찾기
- 다음 버튼 있는 경우 다음 페이지로 이동
- 다음 페이지 내용이 불러올때까지 대기
- 다음 페이지 크롤링
아래처럼 코드를 수정한다.
// express 모듈 불러오기 const express = require("express"); // express 객체 생성 const app = express(); // 기본 포트를 app 객체에 설정 const port = process.env.PORT || 5000; app.listen(port); // 미들웨어 함수를 특정 경로에 등록 app.use("/api/data", async function (req, res) { console.log("검색 키워드: " + req.query.keyword); const resultList = await openBrowser(req.query.keyword); console.log(resultList); res.json(resultList); }); console.log(`server running at http ${port}`); // puppeteer 모듈 불러오기 const puppeteer = require("puppeteer"); /** * 브라우저 오픈 함수 * @param {string} keyword 검색 키워드 * @return {array} 검색 결과 */ async function openBrowser(keyword) { // 모든 검색결과 let searchAllData = []; // 브라우저 실행 및 옵션, 현재 옵션은 headless 모드 사용 여부 const browser = await puppeteer.launch({ headless: true }); // 브라우저 열기 const page = await browser.newPage(); // 포탈로 이동 await page.goto("https://www.google.com/"); // 키워드 입력 await page.type("input[class='gLFyf gsfi']", keyword); // 키워드 검색 await page.type("input[class='gLFyf gsfi']", String.fromCharCode(13)); // 검색하고 싶은 페이지 수 만큼 반복 for (let i = 0; i < 10; i++) { // 처음 검색 if (i === 0) { // 예외 처리 try { // 해당 콘텐츠가 로드될 때까지 대기 await page.waitForSelector("#rso div.g", { timeout: 10000 }); // 크롤링해서 검색 결과들을 담음 searchAllData.push(...(await crawlingData())); } catch (error) { // 해당 태그가 없을 시 검색결과 없음 반환 console.log("에러 발생: " + error); return [ { title: "검색결과 없음", link: "", text: "", kategorie: "", }, ]; } // 처음 이후 검색 } else { // 예외 처리 try { // 다음 버튼이 로드될때까지 대기 await page.waitForSelector("#pnnext", { timeout: 10000 }); // 브라우저를 호출해 다음 버튼을 클릭 await page.evaluate(() => { const nextBtn = document.querySelector("#pnnext"); if (nextBtn) { nextBtn.click(); } }); // 크롤링해서 검색 결과들을 담음 searchAllData.push(...(await crawlingData())); // 다음 버튼이 더이상 없는 경우 지금까지 크롤링한 모든 검색결과 반환 } catch (error) { return searchAllData; } } } /** * 크롤링 함수 * @return {array} 검색 결과 */ async function crawlingData() { // 해당 콘텐츠가 로드될 때까지 대기 await page.waitForSelector("#rso div.g", { timeout: 10000 }); // 호출된 브라우저 영역 const searchData = await page.evaluate(() => { // 검색된 돔 요소를 배열에 담음 const contentsList = Array.from(document.querySelectorAll("#rso div.g")); let contentsObjList = []; // 검색결과 크롤링 contentsList.forEach((item) => { if (item.className === "g") { const title = item.querySelector("h3"); const link = item.querySelector(".yuRUbf"); const text = item.querySelector(".VwiC3b"); const kategorie = item.querySelector(".iUh30 "); if (title && link && text && kategorie) { contentsObjList.push({ title: title.textContent, // 타이틀 link: link.children[0].href, // 링크 text: text.textContent, // 내용 kategorie: kategorie.textContent, // 카테고리 }); } } }); // 호출된 브라우저 영역 콘솔창에서 확인할 수 있음 console.log(contentsList); // 검색한 엘리먼트 리스트 console.log(contentsObjList); // 검색한 콘텐츠 오브젝트 리스트 return contentsObjList; }); // 검색결과 반환 return searchData; } // 브라우저 닫기 browser.close(); // 모든 검색결과 반환 return searchAllData; }이제 추가된 코드들을 살펴보자.
모든 검색결과를 담을 배열 설정
31라인을 보면
searchAllData배열을 추가하였으며, 이 배열안에 각 페이지마다 크롤링한 데이터가 들어간다.브라우저 호출하는 영역을 함수로 묶음
각 페이지마다 크롤링을 반복해줘야되기 때문에 101번째 라인을 보면
crawlingData함수로 따로 묶어줬다. 그리고 리턴값은searchData로 해주며, 기존 코드의 예외 처리(try)안에 있던page.waitForSelector메서드만 함수 상단에 추가해 준다. 대략적인 형태는 아래와 같다./** * 크롤링 함수 * @return {array} 검색 결과 */ async function crawlingData() { // 해당 콘텐츠가 로드될 때까지 대기 await page.waitForSelector("#rso div.g", { timeout: 10000 }); // 호출된 브라우저 영역 const searchData = await page.evaluate(() => { // ... 기존 크롤링 코드 return contentsObjList; } // 검색결과 반환 return searchData; }처음 검색 후 다음 페이지로 이동하여 검색
코드 49번 라인을 보면 반복문이 실행되며, 검색하고 싶은 페이지만큼 반복되도록 되어있다. 위 코드는 10번만 반복하게 했으며, 반복 횟수를 늘리면 더 많은 페이지를 크롤링할 수 있다. 각 예외 처리는 2개 분기로 되어있으며, 이유는 계속 페이지마다 검색을 하는 경우 결과가 여러가지 있기 때문이다.
검색 결과의 경우들
- 처음 검색에 아무 검색결과가 없는 경우
- 처음 검색과는 있는데 다음 버튼이 없어서 검색결과가 첫 페이지만 있는 경우
- 다음 페이지가 존재해 계속 검색을 진행하다 마지막 페이지에 도달해서 다음 버튼이 없는 경우
처음 검색은 51라인부터 시작되며 검색결과가 없다면 59라인에서 에러를 캐치하며 클라이언트에
검색결과 없음을 리턴해 준다. 검색 결과가 있으면 두번째 반복이 73라인에서 실행되며, 다음 버튼이 있는 경우 83라인 함수에서 다음버튼을 클릭하고crawlingData함수가 실행되어 크롤링을 계속 반복하게 된다. 크롤링 결과값은searchAllData배열에 전개연산자를 활용하여 차곡차곡 쌓이도록 해준다. 이렇게 계속 다음 페이지로 이동하고 더이상 다음버튼이 없으면 91라인에서 에러를 캐치하여 지금까지 모은searchAllData를 리턴하게 된다.headless모드를false로 설정하면 브라우저가 각 페이지를 돌면서 크롤링을 하는 모습을 직접 볼 수 있다.결과 확인
서버 터미널이나 클라이언트 콘솔창을 확인하면 각 페이지마다 크롤링한 데이터를 정상적으로 응답해주는것을 확인할 수 있다. 지금까지 클라이언트, 서버 셋팅 및 연속 크롤링하는 것까지 알아봤으며 이제 이 응답값을 클라이언트에 뿌려주는 작업을 해보자.
클라이언트에서 받은 데이터 출력하기
출력될 컴포넌트 추가
데이터가 들어갈 영역을 만들어주자. 우선
/client/src/components디렉토리에SearchItem.tsx파일을 생성하고 아래 코드를 입력해준다.import React from "react"; const SearchItem = (props: { item: any }) => { const { item } = props; return ( <div className="card"> <div className="top"> <div className="kategorie">{item.kategorie}</div> <div className="title">{item.title}</div> </div> <div className="bottom"> <div className="text">{item.text}</div> <a href={item.link} className="link" target="_blank" rel="noreferrer"> 더보기 </a> </div> </div> ); }; export default SearchItem;컴포넌트에 데이터 전달하기
SearchList.tsx파일과App.tsx파일도 각각 아래처럼 수정해준다.SearchList.tsx
import React from "react"; import SearchItem from "./SearchItem"; const SearchList = (props: { searchData: [] }) => { const { searchData } = props; return ( <div className="card-list"> {searchData.map((item: any, idx: number): JSX.Element => { return <SearchItem key={idx} item={item} />; })} </div> ); }; export default SearchList;App.tsx
import React, { useState } from "react"; import SearchForm from "./components/SearchForm"; import SearchList from "./components/SearchList"; import "./App.css"; function App() { const [searchData, setSearchData] = useState<any>([]); const getData = (keyword: string) => { console.log("검색 키워드: " + keyword); fetch(`api/data?keyword=${keyword}`) .then((res) => { return res.json(); }) .then((data) => { setSearchData(data); console.log(data); }); }; return ( <div className="App"> <SearchForm getData={getData} /> <SearchList searchData={searchData} /> </div> ); } export default App;과정을 살펴보자, 서버에서 응답값이 오면
App.tsx의 15번째 라인에서 받고 이것을SearchList컴포넌트에 전달해 준다.SearchList.tsx를 보면 이 응답값을props로 전달받았으며, 이 값은 배열이기 때문에Array.map메서드를 사용하여SearchItem컴포넌트를 리턴하고 있다. 이렇게 리턴받은SearchItem컴포넌트는SearchItem.tsx파일의 코드처럼, 카테고리, 제목, 본문내용, 링크를 출력하고 있는걸 확인할 수 있다.스타일 꾸며주기
CSS 적용이 안되었기 때문에 실제 화면은 이상하게 보일것이다.
App.css파일에 간단히 스타일을 추가해 주자..App { margin: 0 auto; max-width: 500px; padding: 10px; } .App .form { position: relative; margin-bottom: 10px; } .App .form-text { border: 1px solid #e0e0e0; border-radius: 5px; box-sizing: border-box; padding: 0 65px 0 5px; width: 100%; height: 35px; box-shadow: 0 0 5px rgba(0, 0, 0, 0.2); outline: none; } .App .form-btn { position: absolute; top: 0; right: 0; border: none; border-radius: 0 5px 5px 0; width: 60px; height: 35px; background-color: #13424b; color: #fff; } .App .card-list { column-count: 2; column-gap: 10px; margin-top: 10px; padding: 0; } .App .card { display: inline-block; position: relative; margin: 0 0 10px; border: 1px solid #e0e0e0; border-radius: 5px; word-break: break-all; box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); } .App .card .top { border-radius: 5px 5px 0 0; padding: 10px; background-color: #13424b; color: #fff; } .App .card .top .kategorie { font-size: 11px; } .App .card .top .title { margin-top: 5px; font-weight: bold; } .App .card .bottom { padding: 10px; font-size: 13px; } .App .card .bottom .link { display: inline-block; margin-top: 10px; padding: 3px 5px; border-radius: 3px; text-decoration: none; background-color: #e5e5e5; color: #000; }추가 화면 및 비활성 처리
마무리 단계이며, 아래 단계가 남았다.
- 검색하는 동안 출력될 화면 추가
- 검색하는 동안 폼 비활성(disable) 처리
- 검색결과 없는 경우 추가
SearchLoading.tsx파일을 생성하고 아래처럼 입력해 준다. 이 컴포넌트가 검색하는 동안 보여지는 부분이다.import React from "react"; const SearchLoading = (props: { isOnLoading: boolean }) => { const { isOnLoading } = props; return ( <div className={isOnLoading ? "loading on" : "loading"}>loading..</div> ); }; export default SearchLoading;그리고
SearchForm.tsx,SearchList.tsx,App.tsx,App.css파일들을 아래처럼 코드를 수정한다.SearchForm.tsx
import React, { useState } from "react"; const SearchForm = (props: { getData: any; isOnLoading: boolean }) => { const { getData, isOnLoading } = props; const [keyword, setKeyword] = useState(""); return ( <div className={isOnLoading ? "form disable" : "form"}> <input type="text" className="form-text" disabled={isOnLoading ? true : false} onChange={(e: any) => { setKeyword(e.target.value); }} onKeyPress={(e: any) => { if (e.charCode === 13) { if (keyword) { getData(keyword); } } }} /> <button type="button" className="form-btn" disabled={isOnLoading ? true : false} onClick={() => { if (keyword) { getData(keyword); } }} > search </button> </div> ); }; export default SearchForm;SearchList.tsx
import React from "react"; import SearchItem from "./SearchItem"; const SearchList = (props: { searchData: []; isOnLoading: boolean }) => { const { searchData, isOnLoading } = props; return ( <div className={isOnLoading ? "card-list disable" : "card-list"}> {searchData.map( (item: any, idx: number): JSX.Element => { if (item.kategorie && item.kategorie && item.text) { return <SearchItem key={idx} item={item} />; } else { return ( <div key={idx} className="none"> 검색결과 없음 </div> ); } } )} </div> ); }; export default SearchList;App.tsx
import React, { useState } from "react"; import SearchForm from "./components/SearchForm"; import SearchLoading from "./components/SearchLoading"; import SearchList from "./components/SearchList"; import "./App.css"; function App() { const [searchData, setSearchData] = useState<any>([]); const [isOnLoading, setIsOnLoading] = useState(false); const getData = (keyword: string) => { setIsOnLoading(true); console.log("검색 키워드: " + keyword); fetch(`api/data?keyword=${keyword}`) .then((res) => { return res.json(); }) .then((data) => { setSearchData(data); setIsOnLoading(false); console.log(data); }); }; return ( <div className="App"> <SearchForm getData={getData} isOnLoading={isOnLoading} /> <SearchLoading isOnLoading={isOnLoading} /> <SearchList searchData={searchData} isOnLoading={isOnLoading} /> </div> ); } export default App;App.css
.App { margin: 0 auto; max-width: 500px; padding: 10px; } .App .form { position: relative; margin-bottom: 10px; } .App .form-text { border: 1px solid #e0e0e0; border-radius: 5px; box-sizing: border-box; padding: 0 65px 0 5px; width: 100%; height: 35px; box-shadow: 0 0 5px rgba(0, 0, 0, 0.2); outline: none; } .App .form-btn { position: absolute; top: 0; right: 0; border: none; border-radius: 0 5px 5px 0; width: 60px; height: 35px; background-color: #13424b; color: #fff; } .App .form.disable .form-btn { background-color: #b3b3b3; } .App .card-list { column-count: 2; column-gap: 10px; margin-top: 10px; padding: 0; } .App .card { display: inline-block; position: relative; margin: 0 0 10px; border: 1px solid #e0e0e0; border-radius: 5px; word-break: break-all; box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); } .App .card .top { border-radius: 5px 5px 0 0; padding: 10px; background-color: #13424b; color: #fff; } .App .card-list.disable .top { background-color: #b3b3b3; } .App .card .top .kategorie { font-size: 11px; } .App .card .top .title { margin-top: 5px; font-weight: bold; } .App .card .bottom { padding: 10px; font-size: 13px; } .App .card .bottom .link { display: inline-block; margin-top: 10px; padding: 3px 5px; border-radius: 3px; text-decoration: none; background-color: #e5e5e5; color: #000; } .App .card-list.disable .link { color: #fff; } .App .card-list .none { font-size: 13px; } .App .loading { overflow: hidden; text-align: center; height: 0; line-height: 18px; font-size: 0; transition: 0.3s; } .App .loading.on { font-size: 13px; height: 20px; }검색 시작 및 끝을 나타내는 값
컴포넌트에 검색중 여부 전달
코드를 하나씩 살펴보자.
App.tsx파일의 9번째 라인을 보면isOnLoading값이 있는데 요청을 보내면 12번째 라인에서true로 변경, 검색중을 나타내고, 응답을 받으면 20번째 라인에서false로 변경되며 검색이 끝났다는 것을 의미한다. 이 값을SearchForm,SearchLoading,SearchList컴포넌트에 전달해 주었다.컴포넌트에서 검색중 여부 처리
SearchLoading.tsx파일 6번째 라인을 보면isOnLoading가true일 경우on클래스를 추가해 준다. 평상시에 이 엘리먼트는 보이지 않다가on클래스가 추가되면 보여지도록App.css에 설정되어있다.다른 컴포넌트도 마찬가지로
SearchForm.tsx에서 11번째 라인을 보면isOnLoading이true면 폼들은disabled처리가 되며, 7번째 라인에서disable클래스를 주고있다.SearchList.tsx컴포넌트도 검색중이면disable클래스를 주고있으며, 이 클래스로 검색중에 스타일을 변경하도록App.css에 설정되어있다.검색결과 없는 경우
SearchList.tsx파일의 10번째 라인을 보면 전달받은 데이터 값이 모두 있을경우 데이터를 노출하고 없는 경우는 검색결과가 없다는 내용을 출력하고 있다.배포하기
이제 로컬에서 작업한 결과물을 헤로쿠에 배포를 해보자.
깃 초기화
헤로쿠는 깃을 통해 업로드하기 때문에 루트 경로에서 아래 명령어로 깃을 초기화 해준다.
$ git init빌드 설정
정적 파일 생성
client디렉토리에서 아래 명령어를 입력해 배포용 정적 파일을 생성한다.$ npm run build정적 파일 경로 설정
다음에
index.js파일 최하단에 아래 코드를 추가해준다.// ... 기존 코드 // path 모듈 불러오기 const path = require('path'); // 리액트 정적 파일 제공 app.use(express.static(path.join(__dirname, 'client/build'))); // 라우트 설정 app.get('*', (req, res) => { res.sendFile(path.join(__dirname+'/client/build/index.html')); });헤로쿠 빌드 명령어 설정
루트경로의
package.json파일로 가서heroku-postbuild를 아래처럼 추가해준다."scripts": { "start": "nodemon index.js", "dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"", "dev:server": "npm start", "dev:client": "cd client && npm start", "heroku-postbuild": "cd client && npm install && npm run build" }헤로쿠 연동하기
헤로쿠에 대한 간략한 설명은 [이 포스팅]https://recordboy.github.io/2020/11/05/express-react-heroku-init/)을 참고하면 된다. 기존에 회원이 아니면 헤로쿠 홈페이지에서 회원가입을 하고 이곳에서 헤로쿠 CLI를 설치하면 된다.
로그인 및 프로젝트 생성
아래 명령어를 입력하고 아무키나 입력하면 로그인 하라는 브라우저가 뜨고 로그인을 해주자.
$ heroku login아래 명령어로 헤로쿠에 프로젝트를 생성하며 프로젝트 이름은 다른 프로젝트와 중복되지 않게 정한다.
git remote -v명령어로 저장소가 제대로 연결되었는지 확인한다.$ heroku create 프로젝트이름 $ git remote -v헤로쿠 프로젝트 주소와 로컬에서 바라보는 주소가 다를경우
$ git remote set-url heroku 프로젝트주소명령어를 사용하여 동일하게 맞춰주면 된다.빌드팩 추가
한가지 또 추가해줘야 하는 것이 있는데 Puppeteer를 헤로쿠에서 사용하려면 프로젝트에 빌드팩을 추가해줘야 한다. 아래 명령어를 입력해주자.
$ heroku buildpacks:clear $ heroku buildpacks:add --index 1 https://github.com/jontewks/puppeteer-heroku-buildpack $ heroku buildpacks:add --index 1 heroku/nodejs그리고
index.js파일로 가서 34번째 라인의 브라우저 실행 옵션에args값을 아래처럼 추가해 준다.// 브라우저 실행 및 옵션, 현재 옵션은 headless 모드 사용 여부 const browser = await puppeteer.launch({ headless: true, args: [ "--no-sandbox", "--disable-setuid-sandbox", "--window-size=1600,2000", ] });업로드
이제 배포를 위한 모든 준비가 끝났다. 깃 명령어를 입력하여 푸쉬해주자.
$ git add . $ git commit -m '커밋 메세지' $ git push heroku master이제 배포된 페이지를 확인해 보자. url은
https://프로젝트이름.herokuapp.com/로 가면 확인할 수 있다. 정상적으로 배포된 페이지를 화인할 수 있다.지금까지 만들어본건 검색하는 기능만 있는 아주 기본적인 형태이지만 잘만 활용하면 요청, 응답값으로 여러가지 형태로 발전시킬 수 있다. 예를들어 검색 포탈명을 요청해 여러 포탈의 검색결과를 한번에 찾아보거나 각 다음 페이지를 넘기는
index값을 응답을 받아 검색 로딩시간을 알아보는 등 여러가지 활용이 가능하다.마지막으로 주의할 점이 있는데, 헤로쿠 서버의 무료 용량은 500MB로 제한된다. Puppeteer는 자체적으로 Chromium을 내장하고 있는데 이것이 꽤 용량이 나간다.(대략 300MB 조금 넘게) 그래서 프로젝트를 크게 불려 배포를 하면 가끔 용량이 부족하다고 에러가 나오는 경우가 있다. 또 한가지는 Puppeteer를 많이 사용하다보면 구글에서 봇으로 판단하여 ‘로봇이 아닙니다’ 체크를 해야하는 경우도 있었다. 아무튼 Puppeteer는 크롤링 말고 여러가지 강력한 기능이 있기 때문에 잘 활용하면 좋은 도구가 될 수 있을 것이다.
References
-
![[JavaScript] 얕은 복사, 깊은 복사](https://susukkekki.kr/wp-content/uploads/2017/02/web.png)
[JavaScript] 얕은 복사, 깊은 복사
깂은 복사와 얕은 복사에 대해 알아보겠다. 이 글의 초반 내용은 이전 포스팅의 (원시 타입과 참조 타입의 차이)과 맥락이 비슷하며, 위 포스팅은 원시 타입과 참조 타입의 차이점이라면 아래는 참조 타입의 깊은 복사하는 방법에 대해 알아보도록 하겠다.
얕은 복사(shallow copy)
얖은 복사는 참조(주소)값의 복사를 나타낸다.
const obj = { vaule: 1 } const newObj = obj; newObj.vaule = 2; console.log(obj.vaule); // 2 console.log(obj === newObj); // trueobj객체를 새로운newObj객체에 할당하였으며 이를참조 할당이라 부른다. 복사 후newObj객체의value값을 변경하였더니 기존의obj.value값도 같이 변경된 것을 알 수 있다. 두 객체를 비교해도true로 나온다. 이렇게 자바스크립트의 참조 타입은 얕은 복사가 된다고 볼 수 있으며, 이는 데이터가 그대로 생성되는 것이 아닌 해당 데이터의 참조 값(메모리 주소)를 전달하여 결국 한 데이터를 공유하는 것이다.깊은 복사(deep copy)
깊은 복사는 값 자체의 복사를 나타낸다.
let a = 1; let b = a; b = 2; console.log(a); // 1 console.log(b); // 2 console.log(a === b); // false변수
a를 새로운b에 할당하였고b값을 변경하여도 기존의a의 값은 변경되지 않는다. 두 값을 비교하면false가 출력되며 서로의 값은 단독으로 존재하다는 것을 알 수 있다. 이렇게 자바스크립트의 원시 타입은 깊은 복사가 되며, 이는 독립적인 메모리에 값 자체를 할당하여 생성하는 것이라 볼 수 있다.객체의 깊은 복사
객체를 그대로 복사하여 사용할 경우 기존 객체의 원본 데이터가 더럽혀 질 수 있기 때문에 객체의 깊은 복사는 매우 중요하다. 객체를 깊이 복사하는 방법에 대해 몇가지 알아보자.
Object.assign()
Object.assign()메서드를 활용하는 방법이다.문법
Object.assign(생성할 객체, 복사할 객체)메서드의 첫번째 인수로 빈 객체를 넣어주며, 두번째 인수로 할당할 객체를 넣으면 된다.const obj = { a: 1 }; const newObj = Object.assign({}, obj); newObj.a = 2; console.log(obj); // { a: 1 } console.log(obj === newObj); // false새로운
newObj객체를Object.assign()메서드를 사용하여 생성하였으며,newObj.a값을 변경하여도 기존의obj는 변하지 않았다. 서로의 객체를 비교해도false로 뜨며 서로 참조값이 다르다는 것을 알 수 있다.Object.assign()는 2차원 객체는 깊은 복사가이루어지지 않는다
하지만
Object.assign()를 활용한 복사는 완벽한 깊은 복사가 아니다.const obj = { a: 1, b: { c: 2, }, };위처럼
obj객체의b프로퍼티의 값으로{ c: 2 }객체를 가진 2차원 객체일 경우는 경우는 어떨까?const obj = { a: 1, b: { c: 2, }, }; const newObj = Object.assign({}, obj); newObj.b.c = 3; console.log(obj); // { a: 1, b: { c: 3 } } console.log(obj.b.c === newObj.b.c); // true2차원 객체를
newObj에 복사하고,newObj.b.c의 값을 변경하였다. 기존obj객체를 출력해보면newObj.b.c의 값도3으로 변경되었다. 복사된 하위 객체{ c: 2 }도 결국 객체이기 때문에 얕은 복사가 이루어진 것이다. 이는Object.assign()메서드의 한계이며, 전개연산자(Spread Operator) 를 이용한 객체의 복사에도 같은 문제가 있다.전개연산자(Spread Operator)
const obj = { a: 1 }; const newObj = Object.assign({}, obj); newObj.a = 2; console.log(obj); // { a: 1 } console.log(obj === newObj); // false전개연산자를 활용해도 객체의 깊은 복사가 가능하다.
const obj = { a: 1, b: { c: 2, }, }; const newObj = { ...obj }; newObj.b.c = 3; console.log(obj); // { a: 1, b: { c: 3 } } console.log(obj.b.c === newObj.b.c); // true하지만
Object.assign()와 마찬가지로 2차원 객체는 얕은 복사가 되는 것을 확인할 수 있다.JSON 객체 메서드를 이용
객체의 깊은 복사를 위해 JSON 객체의
stringify(),parse()메서드를 사용할 수 있다.문법
JSON.stringify()메서드는 인수로 객체를 받으며 받은 객체는 문자열로 치환되며,JSON.parse()메서드는 문자열을 인수로 받으며, 받은 문자열을 객체로 치환한다.const obj = { a: 1, b: { c: 2, }, }; const newObj = JSON.parse(JSON.stringify(obj)); newObj.b.c = 3; console.log(obj); // { a: 1, b: { c: 2 } } console.log(obj.b.c === newObj.b.c); // falseobj객체를JSON.stringify()메서드를 이용하여 문자열로 변환한 뒤 다시JSON.parse()메서드로 객체로 변환하였다. 문자열로 변환한 뒤 다시 객체로 변환하였기에 2차원 객체에 대한 참조가 사라졌다. 하지만 이 방법도 2가지 문제가 있는데, 다른 방법에 비해 성능이 느린 점과JSON.stringify()메서드는 함수를 만났을 때undefined로 처리한다는 점이다.const obj = { a: 1, b: { c: 2, }, func: function() { return this.a; } }; const newObj = JSON.parse(JSON.stringify(obj)); console.log(newObj.func); // undefined복사된
newObj는func가 없고undefined로 출력되고 있다.커스텀 재귀 함수
이 문제를 원칙적으로 해결하려면 직접 깊은 복사를 구현하는 커스텀 재귀 함수를 사용하는 것이다.
function deepCopy(obj) { if (obj === null || typeof obj !== "object") { return obj; } let copy = {}; for (let key in obj) { copy[key] = deepCopy(obj[key]); } return copy; } const obj = { a: 1, b: { c: 2, }, func: function () { return this.a; }, }; const newObj = deepCopy(obj); newObj.b.c = 3; console.log(obj); // { a: 1, b: { c: 2 }, func: [Function: func] } console.log(obj.b.c === newObj.b.c); // falsedeepCopy함수의 인수로obj객체를 넣었다. 인수값이 객체가 아닌 경우는 그냥 반환하며, 객체인 경우 객체의 값 만큼 루프를 돌며 재귀를 호출하여 복사된 값을 반환한다. 복사된newObj객체를 보면 2차원 객체의 값도 깊은 복사가 이루어 졌으며, 객체의 함수도 제대로 표현되는 것을 확인할 수 있다. 하지만 이미 객체의 깊은 복사를 위한 오픈 소스가 존재하며lodash모듈의cloneDeep()을 이용하면 된다.lodash 모듈의 cloneDeep()
lodash모듈의cloneDeep()메서드를 이용하여 객체의 깊은 복사가 가능하다. 해당 모듈을 설치해 준 뒤 아래 코드를 실행시켜 보자.& npm i lodashconst lodash = require("lodash"); const obj = { a: 1, b: { c: 2, }, func: function () { return this.a; }, }; const newObj = lodash.cloneDeep(obj); newObj.b.c = 3; console.log(obj); // { a: 1, b: { c: 2 }, func: [Function: func] } console.log(obj.b.c === newObj.b.c); // false간단히 객체의 깊은 복사를 구현할 수 있다. 실제로 웹 개발을 하다보면
lodash모듈은 흔히 사용되며, 가장 손쉽게 객체의 깊은 복사를 해결하는 방법이라 할 수 있다.References
자바스크립트 객체 복사하기
Javascript 깊은 복사의 함정
[Java Script] 얕은 복사와 깊은 복사
JavaScript로 Deep Copy 하는 여러 방법
Javascript:Shallow and Deep Copy :: 마이구미 -
![[Network] REST(Representational State Transfer)](https://susukkekki.kr/wp-content/uploads/2017/02/web.png)
[Network] REST(Representational State Transfer)
REST
REST는 웹에서 데이터를 전송하고 처리하는 방법을 정의한 인터페이스를 말하며, 모든 데이터 구조와 처리 방식은 REST에서 URL을 통해 정의된다. 때문에 매우 직관적이고 이해하기 쉬우며 사용자에게 더 쉽게 서비스를 제공할 수 있다.
RESTfull
RESTfull API라고도 하며, HTTP 프로트콜과 REST의 원칙을 사용하여 구현된 웹 서비스이다. 리소스(Resource)는 모든 인터넷 환경에서 사용이 가능한 표준화 된 형식(일반적으로 XML 또는 JSON)으로 표현된다.
REST 중심 규칙
URL는 정보의 자원을 표현해야 한다
URL은 의미를 명확히 전달하기 위해 명사로 구성한다. 예를 들어 요청 URL이
/user면 사용자 정보에 관한 요청이며,/post라면 게시글에 관한 요청하는 것으로 추측이 가능하다.자원에 대한 행위는 HTTP Method로 표현한다
REST에선 HTTP Method를 사용하며 주요 메서드는 아래와 같다.
HTTP Method
GET(자원 정보 조회): 서버의 자원을 가져올 때 사용한다. 요청의 본문에 데이터를 넣지 않으며 쿼리스트링을 사용하여 서버에 데이터를 보낸다. 또한 GET 메서드는 브라우저에서 캐싱(기억)할 수도 있다.
쿼리스트링
URL에 미리 협의된 데이터를 파라미터를 통해 넘기는 것을 말한다.- 정해진 주소 이후에
?를 쓰는것으로 쿼리스트링 시작을 의미한다. - 형태는
parameter=value이며=로 파라미터와 값이 구분되며 파라미터가 여러개의 경우&를 붙여 각 값을 구분한다. - 예시: https://recordboy.github.io/?파라미터=값&파라미터=값
- 정해진 주소 이후에
POST(자원 생성): 서버에 자원을 새로 등록할 때 사용된다. 요청의 본문에 새로 등록할 데이터를 넣어 보낸다.
PUT(자원 업데이트): 서버의 자원을 요청에 있는 자원으로 치환할 때 보낸다. 요청의 본문에 치환할 데이터를 넣어 보낸다.
PATCH(자원 일부 업데이트): PUT이 자원 전체를 업데이트한다면 PATCH는 자원 일부를 수정할 때 사용한다. 요청의 본문에 수정할 데이터를 넣어 보낸다.
DELETE(자원 삭제): 자원을 삭제할 때 사용되며, 요청의 본문에 데이터를 넣지 않는다.
OPTIONS(옵션 설명): 요청을 하기 전에 통신 옵션을 설명하기 위해 사용된다.
REST 구성 요소
구성 요소 내용 표현 방법 Resource 자원 HTTP URI Verb 자원에 대한 행위 HTTP Method Representations 자원에 대한 행위의 내용 HTTP Message Pay Load REST 특징
클라이언트/서버 구조(Client – Server)
자원이 있는 서버와 자원을 요청하는 클라이언트의 구조를 가진다. REST 서버는 클라이언트에게 API만 제공, 클라이언트는 사용자 인증이나 컨텍스트(세션, 로그인 정보)등을 직접 관리하는 구조로 각각의 역할이 확실하게 구분되어 일관적인 인터페이스로 분리되고 작동할 수 있게 한다.
무상태성(Stateless)
HTTP는 무상태 프로토콜 이므로 REST 역시 무상태성을 가진다. 다시 말해 작업을 위한 상태 정보를 따로 저장하고 관리하지 않는다. 세션이나 쿠키를 별도로 저장, 관리하지 않기 때문에 API 서버는 들어오는 요청만 단순히 처리하면 된다. 그래서 서비스의 자유도가 높아지며, 불편한 정보를 관리하지 않음으로써 구현이 단순해진다.
캐시 처리 가능(Cachealble)
REST에서는 웹 표준 HTTP 프로토콜을 그대로 사용하므로, 웹의 기존의 인프라를 그대로 활용 가능하다. 때문에 REST에서도 캐싱 기능을 사용할 수 있다.
계층화(Layered System)
REST 서버는 다중 계층으로 구성될 수 있으며 보안, 로드 밸런싱, 암호화 계층을 추가해 구조를 변경할 수 있다. 또한 Proxy, Gateway와 같은 네트워크 기반의 중간매체를 사용할 수 있다. 하지만 클라이언트는 서버와 직접 통신하는지 중간 서버와 통신하는지 알 수 없다.
자체 표현 구조(Self-descriptiveness)
REST는 JSON 메세지 포멧을 이용하여 직관적으로 이해할 수 있고, 그 요청이 어떤 행위를 하는지 쉽게 알수 있는 자체 표현 구조로 되어있다.
JSON은 하나의 옵션일뿐, 메시지 포맷을 꼭 JSON으로 적용해야할 필요는 없다.
유니폼 인터페이스(Uniform Interface)
URL에 대한 요청을 통일되고 한정적으로 수행하는 이키텍쳐 스타일을 의미하며, HTTP 표준에만 따른다면 모든 플랫폼에서 사용이 가능하다.
REST 장점
쉬운 사용
HTTP 프로토콜 인프라를 그대로 사용하므로 REST API 사용을 위한 별도의 인프라를 구축할 필요가 없다.
클라이언트와 서버의 명확한 역할 분리
클라이언트는 REST API만을 통해 서버와 정보를 주고받는다. 때문에 REST의 무상태성에 따라 사용자의 컨텍스트를 따로 관리할 필요가 없다.
특정 데이터 표현 사용가능
REST는 헤더 부분에 URL 처리 메서드를 명시하고 실제 필요한 데이터는 BODY에 표현할 수 있도록 분리하여 JSON , XML 등 원하는 언어로 사용이 가능하다.
REST 단점
메서드의 한계
REST API는 HTTP 메서드를 이용하여 URI를 표현하기 때문에 쉬운 사용이 가능한 장점이 있지만 반대로 메서드 형태가 제한적이라는 단점이 있다.
표준이 없음
REST는 설계 가이드일 뿐 표준이 아니며 명확한 표준이 없다.
References
REST
Node.js 교과서
REST란
REST API의 이해와 설계-#1 개념 소개
[Server] Restful API란?
REST 아키텍처를 훌륭하게 적용하기 위한 몇 가지 디자인 팁 -
![[JavaScript] 함수형 프로그래밍의 순수 함수](https://susukkekki.kr/wp-content/uploads/2017/02/web.png)
[JavaScript] 함수형 프로그래밍의 순수 함수
함수형 프로그래밍
부수 효과를 없애고 순수 함수를 만들어 모듈화 수준을 높이는 프로그래밍 패러다임
- 부수 효과: 외부 상태를 변경하거나 함수로 들어온 인자 상태를 변경하는 것
- 순수 함수
- 동일한 입력에 대해 항상 동일한 출력을 반환하는 함수
- 외부의 상태를 변경하거나 영향을 받지 않는 함수
순수한 함수
function func(a, b) { return a + b; } console.log(func(2, 2)); // 4위
func함수는 순수하다. 언제나 이 함수를 수백번 실행시켜도 입력값이2,2면 출력값이4로 동일하기 때문이다. 또한 이 함수는 외부의 값에 영향을 주거나 받지 않는다.순수하지 않은 함수
let c = 1; function func(a, b) { return a + b + c; } console.log(func(2, 2)); // 5 c = 2; // c 값이 변경됨 console.log(func(2, 2)); // 6위 함수는 외부 값인인
c에 영향을 받기 때문에 순수함수가 아니다.c가 변하면 동일한 입력에 대해 출력이 다르기 때문이다.let c = 1; function func(a, b) { c += 1; // 외부의 값에 변화를 주며, 이를 부수효과라 함 return a + b; } func(2, 2); // 함수 실행 console.log(c); // c 값이 2로 변화됨위 함수도 함수가 실행되면 외부값인
c를 변경시키기 때문에 순수 함수가 아니며, 이를 부수 효과라 한다.let obj = { a: 1 }; function func(obj) { return obj.b = 1; // 인자로 받은 객체에 b 값을 추가하여 리턴 } func(obj); console.log(obj); // { a: 1, b: 1 }객체의 경우도 살펴보자. 위 함수도 외부
obj객체에b가 추가되었기 때문에 순수함수가 아니다.let obj = { a: 1 }; function func(obj) { return obj; // 인자로 받은 객체를 그대로 리턴 } let obj2 = func(obj);위의 경우는 어떨까? 함수 안에서는 객체를 받고 아무런 변화를 주지 않고 리턴하였으며, 새로운 변수에 리턴된 객체를 할당했다. 위 함수에서는 객체에 아무런 변화를 주지 않았으니 순수 함수라고 할 수 있을까?
let obj = { a: 1 }; function func(obj) { return obj; // 인자로 받은 객체를 그대로 리턴 } let obj2 = func(obj); // 새로운 변수에 리턴된 객체를 할당 obj2.a = 2; // 새로운 객체 obj2의 a 값을 변경 console.log(obj2); // { a: 2 } console.log(obj); // { a: 2 }정답은 아니다.
func함수를 실행하여 새로운 변수에 리턴받은 객체를 할당했으며, 새로운 객체obj2의a값을 변경하였다. 그랬더니 기존의obj객체의 값도 변경이 되었다. 바로 객체의 참조(주소) 값도 같이 복사되어 새롭게 만든obj2가 변화함에 따라 기존의obj객체도 변경되기 때문이다. 이처럼 함수 내에서 직접 값을 변경하지 않았더라도 함수에 들어온 인자값을 그대로 사용하면 순수 함수가 아니다.let obj = { a: 1 }; function func(obj) { // 객체의 값만 참조하여 새로운 객체를 리턴 return { a: obj.a, b: 2 }; } let obj2 = func(obj); obj2.a = 2; console.log(obj2); // { a: 2, b: 2 } console.log(obj); // { a: 1 }위에서는 인자로 받은 객체를 직접 사용하지 않고
obj.a값만 참조해서 새로운 객체를 생성하여 리턴하고 있다. 이럴 경우는 참조(주소)값이 복사가 안되기 때문에obj2객체의 값이 변경되도obj의 값이 변경되지 않는다. 그러므로 위 함수는 순수 함수라 할 수 있다.결론
모든 함수가 순수 할수일 수는 없다. 모든 함수가 순수 함수라면 외부의 어떤 데이터에도 변형을 주지 않기 때문에 프로그램은 구동되지 않을 것이다. 단지, 이런 스타일로 코딩하는것이 함수형 프로그래밍의 패러다임 이며, 이 패러다임의 목적은 외부 상태의 변화를 최소함으로 유지하고, 함수 실행 결과 예측을 용이하게 하여 버그 발생 가능성을 줄이는 것에 목적이 있다.
[번역] JavaScript 함수형 프로그래밍 3단계로 설명하기
순수 함수란 무엇인가요… 별거 없음…
JS 함수형 프로그래밍을 위한 사전 지식 : 순수함수, 일급함수
자바스크립트의 함수형 프로그래밍 1 : 순수 함수란?
순수 함수란? (함수형 프로그래밍의 뿌리, 함수의 부수효과를 없앤다) -
![[JavaScript] try…catch를 이용한 에러 핸들링](https://susukkekki.kr/wp-content/uploads/2017/02/web.png)
[JavaScript] try…catch를 이용한 에러 핸들링
자바스크립트에서 에러가 발생하면 코드는 멈추게 되고, 콘솔에 에러가 출력된다. 하지만
try...catch문법을 사용하면 스크립트가 죽는 것을 방지하고, 에러 상황을 잡아 예외처리를 할 수 있게 한다. 기본적인 형태는 두 블록으로 구성되며 예시 코드는 아래와 같다.기본 형태
try { // 이 구간에서 에러가 발생하면 catch로 이동 } catch (err) { // 에러 핸들링 }- 먼저
try블록의 코드가 실행된다. try블록 안에 에러가 없다면catch블록은 건너 뛴다.try블록 안에서 에러 코드를 만나면try블록의 실행이 중단되고catch블록의 코드가 실행된다.err객체에는 에러에 대한 정보가 있다.
실제 작동 코드 살펴보자.
try { console.log("아직 에러 없음"); a; // 에러 시작 console.log("이곳은 실행 안됨"); } catch (err) { console.log(err); // a is not defined console.log("에러가 나도 이곳의 코드는 실행됨"); }try블록에서 에러가 나면 아래의console.log()는 실행이 안되며 바로catch블록으로 넘어간다.err객체가 콘솔창에 어떤 에러인지 표시해 주며, 에러가 발생해도catch블록의 코드는 계속 실행되는 것을 확인할 수 있다. 이 부분에 에러 예외 처리를 작성해 주면 된다.try...catch는 런타임 에러에만 작동한다try...catch는 실행이 가능한 코드에만 동작하며, 중괄호가 들어가는 등 자바스크립트 엔진이 해석할 수 없는 문법적 오류(SyntaxError)같은 경우는 작동하지 않는다.try { {}{ // SyntaxError console.log("자바스크립트 엔진은 이 코드를 이해할 수 없어 실행 자체가 안됨"); } catch (err) { console.log("여기도 실행 안됨"); }try...catch는 동기적으로 동작한다try...catch는setTimeout와 같이 비동기적으로 실행되는 코드의 에러는 잡아낼 수 없다.try { setTimeout(() => { a; // 에러가 발생하지만 catch가 잡아낼 수 없음 }, 1000); } catch (err) { console.log("try 블록의 에러를 잡아낼 수 없음"); }1초뒤
setTimeout의 익명함수가 실행되고 에러가 발생하지만 이미 자바스크립트 엔진은try...catch블록을 떠났기 때문에 오류를 잡아낼 수 없다. 비동기로 실행되는 코드의 에러를 잡으려면 반드시 해당 함수 안에서try...catch구문을 사용해야 한다.setTimeout(() => { try { a; // 에러가 발생하지만 catch가 잡아낼 수 있음 } catch (err) { console.log("try 블록의 에러를 잡아냄"); } }, 1000);에러 객체
에러가 발생하면 자바스크립트는 에러 내용이 담긴 객체를 생성하고
catch블록의 인수로 전달한다.try { a; // 에러 시작 } catch (err) { console.log(err); // ReferenceError: a is not defined console.log(err.name); // ReferenceError console.log(err.message); // a is not defined }name프로퍼티는 에러의 이름을 나타내고message는 에러에 대한 상세 내용을 가지고 있다.직접 에러를 생성해 던지기
throw 연산자
throw연산자는 예외를 던질 수 있으며,catch블록에 전달된다.throw연산자는 함수의 실행을 중단한다는 표현과 같다.try { throw "예외 처리를 던짐"; console.log("여긴 실행 안됨"); } catch (err) { console.log(err); // 예외 처리를 던짐 }위 코드에서
throw는 예외를 던지고 있으며,throw아래의 로직은 실행이 안된다.throw연산자자와 에러 객체 생성자를 이용하여 예외 처리를 해보자.에러 객체 생성자
const error = new Error("에러 발생"); const syntaxError = new SyntaxError("문법 에러 발생"); console.log(error); // Error: 에러 발생 console.log(syntaxError); // SyntaxError: 문법 에러 발생자바스크립트는
Error,SyntaxError,ReferenceError,TypeError등 표준 애러 객체 생성자를 지원하며, 이 생성자들을 이용해 에러 객체를 만들 수 있다.생성한 에러 던지기
const person = { name: "foo", age: 20, }; try { if (!person.gender) { throw new SyntaxError("성별이 없음"); } console.log("이곳은 실행이 안됨"); // person.gender 값이 없기 때문에 실행이 안됨 } catch (err) { console.log(err); // Error: 성별이 없음 }위 코드의
try블록에서는 성별 값이 있는지 체크하고 있는데, 체크 대상인person객체에는 이름과 나이만 있고 성별이 없다. 때문에 성별이 없을 때 직접 생성한 에러를 던지고 있고,catch블록에서 에러를 받아 출력하고 있다.참고로 위 에러는 직접 생성한 에러이기 때문에 실제
SyntaxError에러는 아니다.에러 다시 던지기
try...catch는 애초에try블록에서 발생한 모든 에러를 잡는 목적으로 만들어졌다. 에러의 종류와 상관 없이 모든 에러를 잡는것은 디버깅에 어려움을 주기 때문에 예상치 못한 에러를 다시 던져서 에러의 종류에 따라 대응을 해줘야 한다.const person = { name: "foo", age: 20, }; const getError = () => { try { if (!person.gender) { throw new SyntaxError("성별이 없음"); // throw new ReferenceError("성별이 없음"); } } catch (err) { if (err instanceof SyntaxError) { console.log("이 에러는 " + err); // SyntaxError일 경우 } else { throw err; // SyntaxError가 아닐 경우 밖으로 다시 던짐 } } }; try { getError(); } catch (err) { if (err instanceof ReferenceError) { console.log("이 에러는 " + err); // ReferenceError일 경우 } }getError()가 실행되면서SyntaxError를 잡아내고 있으며, 만약SyntaxError에러가 아닐 경우는 다시 에러를 함수 밖으로 던지며, 함수 외부에서 에러를 다시 잡고 있다. 함수 밖에서는ReferenceError일 경우를 잡아내고 있다.SyntaxError일 경우는 함수 내부에서 에러를 잡고ReferenceError일 경우는 함수 외부에서 잡는다고 보면 된다.finally
finally절은 에러의 유무와 상관없이 마지막으로 사용되는 블록이며, 마지막 제어가 필요할 때 사용하면 된다.try { throw new Error("에러 발생"); } catch (err) { console.log(err); // 에러 발생 } finally { console.log("항상 실행"); // 항상 실행 }References
‘try..catch’와 에러 핸들링
에러 처리를 어떻게 하면 좋을까? – 1
예외 ( throw,[try/catch/finally]) - 먼저
-
![[JavaScript] try…catch를 이용한 에러 핸들링](https://susukkekki.kr/wp-content/uploads/2017/02/web.png)
[JavaScript] try…catch를 이용한 에러 핸들링
자바스크립트에서 에러가 발생하면 코드는 멈추게 되고, 콘솔에 에러가 출력된다. 하지만
try...catch문법을 사용하면 스크립트가 죽는 것을 방지하고, 에러 상황을 잡아 예외처리를 할 수 있게 한다. 기본적인 형태는 두 블록으로 구성되며 예시 코드는 아래와 같다.기본 형태
try { // 이 구간에서 에러가 발생하면 catch로 이동 } catch (err) { // 에러 핸들링 }- 먼저
try블록의 코드가 실행된다. try블록 안에 에러가 없다면catch블록은 건너 뛴다.try블록 안에서 에러 코드를 만나면try블록의 실행이 중단되고catch블록의 코드가 실행된다.err객체에는 에러에 대한 정보가 있다.
실제 작동 코드 살펴보자.
try { console.log("아직 에러 없음"); a; // 에러 시작 console.log("이곳은 실행 안됨"); } catch (err) { console.log(err); // a is not defined console.log("에러가 나도 이곳의 코드는 실행됨"); }try블록에서 에러가 나면 아래의console.log()는 실행이 안되며 바로catch블록으로 넘어간다.err객체가 콘솔창에 어떤 에러인지 표시해 주며, 에러가 발생해도catch블록의 코드는 계속 실행되는 것을 확인할 수 있다. 이 부분에 에러 예외 처리를 작성해 주면 된다.try...catch는 런타임 에러에만 작동한다try...catch는 실행이 가능한 코드에만 동작하며, 중괄호가 들어가는 등 자바스크립트 엔진이 해석할 수 없는 문법적 오류(SyntaxError)같은 경우는 작동하지 않는다.try { {}{ // SyntaxError console.log("자바스크립트 엔진은 이 코드를 이해할 수 없어 실행 자체가 안됨"); } catch (err) { console.log("여기도 실행 안됨"); }try...catch는 동기적으로 동작한다try...catch는setTimeout와 같이 비동기적으로 실행되는 코드의 에러는 잡아낼 수 없다.try { setTimeout(() => { a; // 에러가 발생하지만 catch가 잡아낼 수 없음 }, 1000); } catch (err) { console.log("try 블록의 에러를 잡아낼 수 없음"); }1초뒤
setTimeout의 익명함수가 실행되고 에러가 발생하지만 이미 자바스크립트 엔진은try...catch블록을 떠났기 때문에 오류를 잡아낼 수 없다. 비동기로 실행되는 코드의 에러를 잡으려면 반드시 해당 함수 안에서try...catch구문을 사용해야 한다.setTimeout(() => { try { a; // 에러가 발생하지만 catch가 잡아낼 수 있음 } catch (err) { console.log("try 블록의 에러를 잡아냄"); } }, 1000);에러 객체
에러가 발생하면 자바스크립트는 에러 내용이 담긴 객체를 생성하고
catch블록의 인수로 전달한다.try { a; // 에러 시작 } catch (err) { console.log(err); // ReferenceError: a is not defined console.log(err.name); // ReferenceError console.log(err.message); // a is not defined }name프로퍼티는 에러의 이름을 나타내고message는 에러에 대한 상세 내용을 가지고 있다.직접 에러를 생성해 던지기
throw 연산자
throw연산자는 예외를 던질 수 있으며,catch블록에 전달된다.throw연산자는 함수의 실행을 중단한다는 표현과 같다.try { throw "예외 처리를 던짐"; console.log("여긴 실행 안됨"); } catch (err) { console.log(err); // 예외 처리를 던짐 }위 코드에서
throw는 예외를 던지고 있으며,throw아래의 로직은 실행이 안된다.throw연산자자와 에러 객체 생성자를 이용하여 예외 처리를 해보자.에러 객체 생성자
const error = new Error("에러 발생"); const syntaxError = new SyntaxError("문법 에러 발생"); console.log(error); // Error: 에러 발생 console.log(syntaxError); // SyntaxError: 문법 에러 발생자바스크립트는
Error,SyntaxError,ReferenceError,TypeError등 표준 애러 객체 생성자를 지원하며, 이 생성자들을 이용해 에러 객체를 만들 수 있다.생성한 에러 던지기
const person = { name: "foo", age: 20, }; try { if (!person.gender) { throw new SyntaxError("성별이 없음"); } console.log("이곳은 실행이 안됨"); // person.gender 값이 없기 때문에 실행이 안됨 } catch (err) { console.log(err); // Error: 성별이 없음 }위 코드의
try블록에서는 성별 값이 있는지 체크하고 있는데, 체크 대상인person객체에는 이름과 나이만 있고 성별이 없다. 때문에 성별이 없을 때 직접 생성한 에러를 던지고 있고,catch블록에서 에러를 받아 출력하고 있다.참고로 위 에러는 직접 생성한 에러이기 때문에 실제
SyntaxError에러는 아니다.에러 다시 던지기
try...catch는 애초에try블록에서 발생한 모든 에러를 잡는 목적으로 만들어졌다. 에러의 종류와 상관 없이 모든 에러를 잡는것은 디버깅에 어려움을 주기 때문에 예상치 못한 에러를 다시 던져서 에러의 종류에 따라 대응을 해줘야 한다.const person = { name: "foo", age: 20, }; const getError = () => { try { if (!person.gender) { throw new SyntaxError("성별이 없음"); // throw new ReferenceError("성별이 없음"); } } catch (err) { if (err instanceof SyntaxError) { console.log("이 에러는 " + err); // SyntaxError일 경우 } else { throw err; // SyntaxError가 아닐 경우 밖으로 다시 던짐 } } }; try { getError(); } catch (err) { if (err instanceof ReferenceError) { console.log("이 에러는 " + err); // ReferenceError일 경우 } }getError()가 실행되면서SyntaxError를 잡아내고 있으며, 만약SyntaxError에러가 아닐 경우는 다시 에러를 함수 밖으로 던지며, 함수 외부에서 에러를 다시 잡고 있다. 함수 밖에서는ReferenceError일 경우를 잡아내고 있다.SyntaxError일 경우는 함수 내부에서 에러를 잡고ReferenceError일 경우는 함수 외부에서 잡는다고 보면 된다.finally
finally절은 에러의 유무와 상관없이 마지막으로 사용되는 블록이며, 마지막 제어가 필요할 때 사용하면 된다.try { throw new Error("에러 발생"); } catch (err) { console.log(err); // 에러 발생 } finally { console.log("항상 실행"); // 항상 실행 }References
‘try..catch’와 에러 핸들링
에러 처리를 어떻게 하면 좋을까? – 1
예외 ( throw,[try/catch/finally]) - 먼저
-
![[Express] Express + React 연동 및 Heroku에 배포하기](https://susukkekki.kr/wp-content/uploads/2017/02/web.png)
[Express] Express + React 연동 및 Heroku에 배포하기
헤로쿠(Heroku)란?
헤로쿠(Heroku)는 서버 호스팅을 지원하는 클라우드 플랫폼이며, 무료로 서비스를 이용할 수 있는 장점이 있다. 단 무료 버전의 경우 최대 5개의 어플리케이션만 올릴 수 있으며, 30분동안 요청이 없는 경우 사이트는 잠이 든다. 잠이 든 상태에서 다시 요청이 들어오면 깨어나지만, 10초 ~ 30초 가량의 시간이 걸린다는 단점이 있다. 간단한 토이 프로젝트나 포트폴리오 용도로 적합하며, 파일 업로드는 Git을 이용하여 업로드할 수 있다.
이번 포스팅에서는 하나의 디렉토리에 클라이언트와 백앤드로 구성된 프로젝트를 세팅하여 헤로쿠에 배포하는 것까지 진행해 볼 것이다. 클라이언트는 리액트(React)로 구성하고 서버는 익스프레스(Express)로 구성한다.
보통 CRA(Create-React-App)로 리액트 프로젝트를 생성하면 자동으로 서버가 생성되기 때문에 로컬에서 바로 확인이 가능하다. 하지만 익스프레스로 구축한 서버에 리액트를 연동할 경우 두개의 서버(리액트 + 익스프레스)가 존재해 버린다. 로컬에서 작업할 때는 두개의 서버를 돌려 작업할것이며, 실제로 헤로쿠에 배포할 때는 익스프레스 서버에 리액트 빌드 파일을 배포하여 사용할 것이다.
리액트를 빌드하면
build/디렉토리에는 웹팩과 바벨 등을 통해 빌드된 번들 등이 담기지만 CRA에서 제공하는 서버는 포함되지 않는다.디렉토리 구조
프로젝트 기본 구조는 아래와 같다.
my-app/ ├── clinet // 클라이언트(리액트) 영역 │ ├── build // 배포 전용 파일 │ ├── node_modules │ ├── public // 정적 파일 리소스 │ ├── src // 개발 전용 소스 │ ├── .gitignore │ ├── package-lock.json │ ├── package.json │ ├── README.md │ └── tsconfig.json │ ├── node_modules ├── .gitignore ├── index.js // 백앤드(익스프레스) 영역 ├── package-lock.json ├── package.json └── README.md프로젝트 초기화 및 익스프레스 설치
프로젝트 초기화
프로젝트 디렉토리 생성 및 npm 초기화한다.
$ mkdir my-app $ cd my-app $ npm init -y익스프레스와 필요 모듈 설치
익스프레스 서버 생성 및 필요 모듈 추가한다.
$ npm install express nodemon concurrentlyexpress: 데이터와 통신할 서버로 사용할 것이다. nodemon: node.js를 이용하는 파일들은 수정을 해도 반영이 바로 안되고 서버를 재시작해줘야 반영이 되기 때문에 번거롭다. 노드몬은 코드가 수정될 경우 자동으로 서버를 재시작 해주기 때문에 편리하게 사용할 수 있다. concurrently: 리액트와 익스프레스를 동시 실행해주는 역할을 한다.
index.js파일을 생성하여 아래 내용 입력한다.// express 모듈 불러오기 const express = require('express'); // express 객체 생성 const app = express(); // 기본 포트를 app 객체에 설정 const port = process.env.PORT || 5000; app.listen(port); // 미들웨어 함수를 특정 경로에 등록 app.use('/api/data', function(req, res) { res.json({ greeting: 'Hello World' }); }); console.log(`server running at http ${port}`);노드몬으로 서버 구동해보자.
$ nodemon serverhttp://localhost:5000/api/data경로에 들어가 보면
{"greeting":"Hello World"}를 확인해 볼 수 있다.헤로쿠 실행 스크립트 추가
헤로쿠 앱을 시작하려면 아래 명령어가 필요하다.
package.json파일의script에 하단 명령어를 추가해 준다."scripts": { "start": "node index.js" }헤로쿠에 배포해보기
계정 생성 및 CLI 설치
헤로쿠 홈페이지로 들어가 회원가입을 하고 이곳에서 헤로쿠 CLI를 설치한다. 설치가 완료되면 버전을 확인하여 헤로쿠 CLI가 제대로 설치되어 있는지 확인한다.
$ heroku -vGit 초기화
헤로쿠는 Git을 이용하여 업로드한다. 깃을 초기화 하고, 업로드 할 때
node_modules파일들을 무시하도록.gitignore파일을 추가한다. 다음에 첫번째 커밋을 해주자.$ echo node_modules > .gitignore $ git add . $ git commit -m 'init commit'헤로쿠 프로젝트 생성
아래 명령어를 사용하여 내가 만든 계정에 로그인을 해주자. 명령어 입력 후 아무키나 입력하면 새 브라우저 창이 열리며 내 헤로쿠 계정에 로그인할 수 있다.
$ heroku login만약 새 브라우저 창에서
IP address mismatch라는 오류가 뜰 경우 아래 명령어를 이용하여 터미널에서 직접 로그인해준다.$ heroku login -i아래 명령어를 사용하여 내 헤로쿠 계정에 프로젝트를 생성한다. 프로젝트 이름은 url로 사용되기 때문에 다른 헤로쿠 사용자와 중복되면 안되며,
heroku create명령어만 사용할 경우 임의의 이름으로 설정된다. 이후 로컬과 헤로쿠 저장소가 제대로 연결이 되었는지 확인해 본다.$ heroku create 프로젝트이름 $ git remote -v헤로쿠에 배포해보자.
$ git push heroku master시간이 약간 소요되며
https://프로젝트이름.herokuapp.com/api/data으로 들어가면{"greeting":"Hello World"}를 확인해 볼 수 있다.리액트 설치
익스프레스를 설치하였으니 이제 클라이언트 영역인 리액트를 생성하겠다. 이 포스팅에서는
TypeScript를 사용하겠다.client이란 폴더명으로 CRA를 실행한다.$ npx create-react-app client --template typescript조금 기다리면 리액트 앱이 생성될 것이다.
/client/src/경로의App.tsx파일을 아래처럼 수정해준다.import React from 'react'; function App() { return ( <div className="App"> <button type="button" onClick={() => { fetch('http://localhost:5000/api/data') .then((res) => { return res.json(); }) .then((data) => { console.log(data); }); }}>get data</button> </div> ); } export default App;버튼을 누르면
fetch함수를 이용하여http://localhost:5000/api/data에서 데이터를 가져오겠다는 코드다. 수정하고client디렉토리로 가서 리액트를 실행한다.$ npm start그럼 브라우저가 열리면서 http://localhost:3000페이지가 나타난다.
get data버튼을 누르면 브라우저의 콘솔창에http://localhost:5000/api/data에서 가져온 JSON 데이터를 출력해야 되는데 아래와 같은 오류가 난다.Access to fetch at 'http://localhost:5000/api/data' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.CORS(Cross-Origin Resource Sharing) 오류로써, 클라이언트와 서버의 포트가 다른 상태에서 클라이언트 측에서 서버 측으로 무언가를 요청했을 때 브라우저가 보안상의 이유로 요청을 차단하는 문제다. 여기서 설정한 리액트
(http://localhost:3000)와 익스프레스(http://localhost:5000)는 각 다른 포트를 사용하고 있다. 이럴 경우 프록시 설정을 해줘야 한다.프록시(Proxy) 설정
프록시란 사전적으로 대리, 대리인이라는 의미를 가지고 있으며, 프로토콜에 대한 대리 응답이라는 개념으로 보면 된다. 유저가 요청을 하는 경우 IP 주소가 전달되는데, 이를 프록시 서버가 임의로 IP 주소를 변경할 수 있다. 즉, 유저의 실제 IP를 알 수 없도록 하는 것이 프록시 서버의 역할이다.
http-proxy-middleware모듈을 설치하여 프록시를 설정할 수 있다.client디렉토리에서 아래 명령어로 설치해 준다.$ npm install http-proxy-middleware다음에
/client/src/디렉토리로 가서setupProxy.js파일을 생성하고 아래 코드를 입력해준다.const { createProxyMiddleware } = require("http-proxy-middleware"); module.exports = function (app) { app.use( createProxyMiddleware("/api/data", { target: "http://localhost:5000", changeOrigin: true, }) ); };http-proxy-middleware모듈은 앱이 실행될 때src디렉토리에서setupProxy.js파일을 찾은 뒤 이 파일의 설정을 참고하여 프록시를 설정해 준다./api/data라는 경로로 요청이 들어올 경우localhost:5000서버를 이용하도록 설정했다. 다시App.tsx파일을 아래처럼 수정해준다.import React from 'react'; function App() { return ( <div className="App"> <button type="button" onClick={() => { fetch('/api/data') .then((res) => { return res.json(); }) .then((data) => { console.log(data); }); }}>get data</button> </div> ); } export default App;리액트를 재시작 하여
get data버튼을 클릭하면 콘솔창에{"greeting":"Hello World"}데이터가 정상적으로 출력되는 것을 확인할 수 있다.서버 동시 시작
익스프레스와 리액트, 프록시 설정까지 마쳤다. 이제 이 두개의 서버를 동시에 시작해야하는데, 처음에 설치했던
concurrently모듈을 이용하면 된다. 루트 디렉토리의package.json파일의script에 아래 부분을 추가한다."scripts": { "dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"", "dev:server": "npm start", "dev:client": "cd client && npm start" }기존의 실행되있던 서버를 모두 종료 후 아래 명령어를 실행해보면 두개의 서버가 동시에 실행된다.
$ npm run dev빌드 빛 배포하기
client디렉토리로 가서 빌드를 해보자$ npm run build빌드가 완료되면
build디렉토리가 생성되며 안에는 배포용 파일들이 들어있다. 이 정적 파일들이 헤로쿠에서 클라이언트 영역으로 사용되며, 서버는 익스프레스만 구동된다. 따라서 정적 파일에 접근할 수 있도록 Route 설정이 필요하다.index.js파일을 아래 코드처럼 수정한다.// express 모듈 불러오기 const express = require('express'); // express 객체 생성 const app = express(); // path 모듈 불러오기 const path = require('path'); // 미들웨어 함수를 특정 경로에 등록 app.use('/api/data', function(req, res) { res.json({ greeting: 'Hello World' }); }); // 기본 포트를 app 객체에 설정 const port = process.env.PORT || 5000; app.listen(port); // 리액트 정적 파일 제공 app.use(express.static(path.join(__dirname, 'client/build'))); // 라우트 설정 app.get('*', (req, res) => { res.sendFile(path.join(__dirname+'/client/build/index.html')); }); console.log(`server running at http ${port}`);코드를 수정했으면 다시 루트 디렉토리의
package.json파일의script에 아래 부분을 추가한다."scripts": { "heroku-postbuild": "cd client && npm install && npm run build" }이제 헤로쿠에 푸쉬해주자.
$ git add . $ git commit -m 'build' $ git push heroku master배포가 완료되고 해당 주소로 들어가 배포가 잘 되었는지 확인해 본다.
References
Deploy React and Express to Heroku) 배포
Express 서버와 React: Proxy 활용과 빌드 및 헤로쿠(Heroku) 배포
[React.js] 프록시(Proxy) 설정을 통해 CORS 이슈를 해결해보자!