[작성자:] 수수께끼

  • [JavaScript] try…catch를 이용한 에러 핸들링

    [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...catchsetTimeout와 같이 비동기적으로 실행되는 코드의 에러는 잡아낼 수 없다.

    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에 배포하기

    [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 concurrently
    

    express: 데이터와 통신할 서버로 사용할 것이다. 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 server
    

    http://localhost:5000/api/data경로에 들어가 보면 {"greeting":"Hello World"}를 확인해 볼 수 있다.

    헤로쿠 실행 스크립트 추가

    헤로쿠 앱을 시작하려면 아래 명령어가 필요하다. package.json파일의 script에 하단 명령어를 추가해 준다.

    "scripts": {
      "start": "node index.js"
    }
    

    헤로쿠에 배포해보기

    계정 생성 및 CLI 설치

    헤로쿠 홈페이지로 들어가 회원가입을 하고 이곳에서 헤로쿠 CLI를 설치한다. 설치가 완료되면 버전을 확인하여 헤로쿠 CLI가 제대로 설치되어 있는지 확인한다.

    $ heroku -v
    

    Git 초기화

    헤로쿠는 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 이슈를 해결해보자!

  • [JavaScript] Fetch API

    [JavaScript] Fetch API

    자바스크립트의 fetch 함수는 비동기 통신 API로써 서버에 네트워크 요청을 보내 새로운 정보를 받아올 수 있다. ES6부터 지원하며, 가독성이 매우 뛰어난 장점이 있다. 이곳을 클릭하면 {"message": "hello world"}라는 JSON 데이터 화면이 나온다. fetch API를 이용해 이 JSON을 가져와 보자.

    기본 형태

    fetch API의 기본 형태는 아래와 같다.

    fetch(url, [options])
      .then((res) => res.json())
      .then((res) => {
        // data를 응답 받은 후 로직
      });
    
    • url에는 접근하고자 경로를 넣으면 된다.
    • options에는 methodheader등을 지정하여 요청할 수 있다.
    • options에 아무값도 넘기지 않으면 요청은 GET 메서드로 진행된다.

    화살표 함수를 함수 선언식으로 변경하면 아래와 같다.

    fetch(url, [options])
      .then(function(res) {
        return res.json();
      })
      .then(function(res) {
        // data를 응답 받은 후 로직
      });
    

    위에서 언급한 주소를 입력하여 JSON 데이터를 잘 가져오는지 확인해 본다.

    요청 하기

    fetch('https://recordboy.github.io/ui/dummy/data.json')
      .then(function(res) {
    
        // Response Object
        console.log(res);
    
        // 응답값 JSON 형태로 얻기
        return res.json();
      })
      .then(function(res) {
    
        // 리턴 받은 JSON
        console.log(res);
      });
    
    • 응답값은 첫번째 then에 지정된 함수의 res에 담겨지며, 이 값은 http 응답값을 가지고 있는 Response Object이다.
    • 첫번째 then의 응답값을 JSON 형태로 얻기 위해 Response Objectjson() 함수를 호출하여 값을 리턴한다.
    • 두번째 then에서 응답 받은 JSON을 확인할 수 있다.

    References

    fetch
    fetch() 함수 사용법
    Javascript에서의 비동기 통신

  • [Node.js] http 서버 만들기

    [Node.js] http 서버 만들기

    http 서버 만들기

    Node.js 환경에서 간단한 http 서버를 만들어 보자.

    Node.js는 Chrome V8 JavaScript 엔진으로 빌드된 JavaScript 런타임(특정 언어로 만든 프로그램들을 실행할 수 있는 환경)이다. 기존의 자바스크립트는 웹 브라우저에서만 실행할 수 있었지만, Node.js를 사용하면 자바스크립트를 서버 환경에서도 사용할 수 았다.

    노드 환경에서 서버를 만들려면 노드 기본 모듈인 http가 필요하다. 이 모듈은 말그대로 HTTP의 각종 기능을 가지고 있다. 모듈을 불러오고 아래 코드를 입력한다.

    HTTP 모듈 생성

    const http = require("http");
    
    http
      .createServer((req, res) => {
        res.writeHead(200, {
          "Content-Type": "text/html; charset=utf-8",
        });
        res.write("<div>hello world</div>");
        res.end("<div>end</div>");
      })
    
      .listen(8080, () => {
        console.log("8080포트에서 서버 대기중");
      });
    

    아래 명령어를 시용하여 서버를 구동시켜 보자.

    $ node server
    

    8080포트에서 서버 대기중이란 문구가 터미널에 출력되면 http서버가 정상 작동하는 것이다. http://localhost:8080 주소로 들어가보면 hello world를 확인할 수 있다.

    • http 모듈의 createServer메서드로 서버 객체를 생성하였고 요청이 들어올 때 마다 매번 이 콜백함수가 실행된다. 콜백의 인수로 res(request)는 요청에 관한 정보들을, res(respone)은 응답에 관한 정보들을 담고있다.


    • res.writeHead 메서드는 응답에 대한 정보를 기록하는 메서드이며, 첫번째 인수로 요청이 성공했다는 상태 코드 200을 넣어주고, 두번째 인수로 응답에 대한 정보인 형식(HTML)과 문자셋(utf-8)를 넣어준다. 이 정보가 기록되는 부분을 헤더(header) 라고 한다.


      상태 코드

      res.writeHead 메서드의 첫번째 인수로 넣어주는 200이나 500과 같은 숫자는 응답 상태를 나타내는 값으로서 브라우저는 상태 코드를 보고 요청이 성공/실패했는지 판단한다.


      • 2XX: 성공을 알리는 상태코드며, 대표적으로 아래가 있다.

        • 200(성공)

        • 201(작성됨)



      • 3XX: 리다이렉션(다른 페이지로 아동)을 알리는 상태코드로 넘겨받은 URL을 브라우저가 열려고 하면 다른 URL로 갈 때 이 코드가 사용된다. 대표적으로 아래가 있다.

        • 301(영구 이동)

        • 302(임시 이동)

        • 302(수정되지 않음, 요청의 응답으로 캐시를 사용했음)



      • 4XX: 요청 오류를 알리는 코드로서 요청 자체에 오류가 있을 때 표시된다. 대표적으로 아래가 있다.

        • 400(잘못된 요청)

        • 401(권한 없음)

        • 403(금지됨)

        • 404(찾을 수 없음)



      • 5XX: 서버 오류를 나타내며, 요청은 제대로 왔지만 서버에 오류가 있을 때 발생한다. 이 오류는 res.writeHead에서 직접 보내는 경우는 거의 없고, 예기치 못한 에러가 발생 시 서버가 알아서 5XX대 코드를 보낸다. 대표적으로 아래가 있다.

        • 500(내부 서버 오류)

        • 502(불량 게이트웨이)

        • 503(서비스를 사용할 수 없음)




    • res.write 메서드는 클라이언트로 보낼 데이터이며, 인수로 지정한 값이 바디 부분의 컨텐츠로 작성된다. 여러번 호출해서 데이터를 여러개 보낼 수 있다.


    • res.end 메서드는 출력을 완료하는 메서드이며, 인수로 값을 지정하면 해당 인수의 값을 작성한 뒤 내용을 완료한다. 이 메서드로 인해 응답 처리는 종료되고, 그 요청의 처리가 종료된다. res.writeHead, res.write, res.end 이 3개의 메서드로 클라이언트에 대한 응답 내용을 사용할 수 있다.


    • createServer 메서드 뒤에 listen 메서드에 클라이언트에게 공개할 포트번호를 인수로 넣고 두번째 인자로 포트연결 완료 후 살행될 콜백 함수를 넣었다.


    HTML 불러오기

    res.writeres.end에 HTML 코드를 일일히 작성하는 것은 비효율적이므로 미리 HTML 파일을 만들고 fs 모듈로 파일을 읽어 전송할 수 있다. 간단한 HTML 코드를 작성하고 index.html 파일로 루트경로에 저장한다,

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8" />
        <title>title</title>
    </head>
    <body>
        <div>contents</div>
    </body>
    </html>
    
    const http = require("http");
    
    // fs 모듈 로드
    const fs = require("fs").promises;
    
    http
      .createServer(async (req, res) => {
    
        // 통신 성공 시
        try {
            
          // fs 모듈로 index.html 로드
          const data = await fs.readFile("./index.html");
          res.writeHead(200, {
            "Content-Type": "text/html; charset=utf-8",
          });
    
          // html 응답
          res.end(data);
    
        // 에러 발생 시
        } catch (err) {
          console.error(err);
          res.writeHead(500, {
            "Content-Type": "text/plain; charset=utf-8",
          });
    
          // 에러 메세지 응답
          res.end(err.message);
        }
      })
    
      .listen(8080, () => {
        console.log("8080포트에서 서버 대기중");
      });
    

    요청이 들어오면 fs 모듈로 HTML 파일을 읽어 클라이언트의 응답 값으로 보낼 수 있다.

    라우팅

    라우팅이란 특정한 URL에 대해 특정한 화면으로 연결하는 역할이라 정의할 수 있다. 주소가 http://localhost:8080/index.htmlindex.html 화면을 보여주고 http://localhost:8080/about.htmlabout.html 화면을 보여주는 역할이라 보면 된다. 간단히 라우팅을 구현해 보겠다. index.html 파일을 아래와 같이 수정해 주고, about.html 파일도 새롭게 추가해 준다.

    index.html

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8" />
        <title>title</title>
    </head>
    <body>
        <nav>
          <a href="/">home</a>
          <a href="/about">about</a>
        </nav>
        <div>메인 페이지</div>
    </body>
    </html>
    

    about.html

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8" />
        <title>title</title>
    </head>
    <body>
        <nav>
          <a href="/">home</a>
          <a href="/about">about</a>
        </nav>
        <div>소개 페이지</div>
    </body>
    </html>
    

    server.js 파일은 아래와 같이 수정해 준다.

    const http = require("http");
    
    // fs 모듈 로드
    const fs = require("fs").promises;
    
    http
      .createServer(async (req, res) => {
    
        // 통신 성공 시
        try {
          if (req.url === "/") {
    
            // index.html 로드
            const data = await fs.readFile("./index.html");
            res.writeHead(200, {
              "Content-Type": "text/html; charset=utf-8",
            });
            res.end(data);
          } else if (req.url === "/about") {
    
            // about.html 로드
            const data = await fs.readFile("./about.html");
            res.writeHead(200, {
              "Content-Type": "text/html; charset=utf-8",
            });
            res.end(data);
          }
    
        // 에러 발생 시
        } catch (err) {
          console.error(err);
          res.writeHead(500, {
            "Content-Type": "text/plain; charset=utf-8",
          });
    
          // 에러 메세지 응답
          res.end(err.message);
        }
        
      })
    
      .listen(8080, () => {
        console.log("8080포트에서 서버 대기중");
      });
    

    특정 링크를 클릭하면 지정한 화면으로 이동하는 것을 확인할 수 있다.

    References

    Node.js 교과서
    [Node.js코드랩] 2.기본 모듈 http
    기본 스크립트와 http 객체

  • [ESLint] ESLint, Prettier 적용하기

    [ESLint] ESLint, Prettier 적용하기

    자바스크립트 개발을 하다 보면 문법 오류나 코드 정리로 인해 시간을 많이 소비한다. ESLintPrettier는 이러한 상황을 해결해 주는 도구들이며, VSCode, WebStorm, Atom 등 여러 에디터와 연동해 사용이 가능하다. 이번 포스팅에서는 두가지 도구를 간단히 살펴보고 리액트 프로젝트에 적용하는 방법을 알아보겠다. 에디터는 VSCode를 기준으로 하겠다.

    ESLint

    ESLint는 ES + Lint의 합성어로 ES는 EcmaScript를 의미하고 Lint는 보푸라기라는 뜻인데, 프로그래밍에서는 에러가 있는 코드에 표시를 달아 놓는 것을 의미한다. 즉 ESLint는 JavaScript의 스타일 가이드를 따르지 않거나 문제가 있는 안티 패턴들을 찾아주고 일관된 코드 스타일로 작성하도록 도와준다. 코딩 컨벤션 및 안티 패턴을 자동 검출 하므로 옮바른 코딩 습관을 위해 필히 사용할 것을 권장한다.

    ESLint는 스타일 가이드를 편리하게 적용하기 위해 사용되기도 하는데, 많은 개발자가 사용중인 Airbnb Style Guide, Google Style Guide가 대표적인 예이다.

    ESLint가 어떻게 오류를 잡아주는지 예제를 통해 간단히 알아보자.

    $ mkdir test
    $ cd test
    $ touch test.js
    

    프로젝트를 생성할 디렉토리로 이동하여 test 폴더를 만들고 test.js 파일을 생성하였다.

    let foo = text;;
    

    문자열에 따옴표도 없고, 세미콜론도 두개고, 변수에 값이 할당되어도 사용이 안되는 엉망인 코드를 작성하였다. 그냥 js 파일은 문법적 오류를 따로 잡아주지 않을 것이다.

    ESLint 설치

    npm 프로젝트를 하나 생성하고 ESLint를 설치하도록 하겠다.

    $ npm init -y
    $ npm i -D eslint
    

    축약법 간단 설명

    1. npm init -y 명령어에 -y--yes의 축약법으로 npm 프로젝트를 초기 세팅할 때 아무 질문 없이 기본값으로 프로젝트가 세팅된다. 비슷한 명령어로 --force(-f)가 있다.
    2. npm i -D eslint 명령어의 -D--save-dev의 축약법이며, 비슷한 옵션으로 --save가 있다. 차이는 아래를 참고한다.
    3. --save-dev는 설치한 패키지 정보를 ./package.json 파일의 devDependencies 항목에 저장하며, npm install을 할 때 해당 패키지가 같이 설치된다. 설치할 때 --production 옵션을 붙이면 해당 패키지를 제외하고 npm이 설치된다.
    4. --save는 설치한 패키지 정보가 ./package.jsondependencies에 추가되며, npm install을 할 때 해당 패키지는 항상 설치가 된다.
    5. 아무 옵션을 넣지 않으면 순수하게 ./node_modules에 패키지만 설치한다.

    설치가 끝나면 ESLint 실행 파일이 ./node_modules 디렉토리 안에 생성될 것이다. ESLint 설정 파일을 생성하기 위해 아래 명령어를 실행한다.

    $ node_modules/.bin/eslint --init
    

    실행하면 몇가지 질문들이 나온다. 본인의 프로젝트 상황에 맞게 답변을 하면 된다.

    ? How would you like to use ESLint?
        ❯ To check syntax and find problems
    ? What type of modules does your project use?
        ❯ None of these
    ? Which framework does your project use?
        ❯ None of these
    ? Does your project use TypeScript?
        ❯ No
    ? Where does your code run?
        ❯ browser
    ? What format do you want your config file to be in?
        ❯ JavaScript
    

    답변을 전부 마치면 루트 경로에 .eslintrc.js 파일이 생성되며, 여기에서 ESLint를 설정할 수 있다.

    module.exports = {
        "env": {
            "browser": true,
            "es2020": true
        },
        "extends": "eslint:recommended",
        "parserOptions": {
            "ecmaVersion": 11,
            "sourceType": "module"
        },
        "rules": {
        }
    };
    
    • 환경(env): 프로젝트의 사용 환경을 설정한다.
    • 확장(extends): 다른 ESLint 설정을 확장해서 사용할때 설정한다. 위 파일에서는 ESLint가 추천하는 규칙을 적용하라는 설정이며, 실제 프로젝트에서는 위에서 언급한 airbnbprettier 등을 확장해서 사용한다.
    • 파서 옵션(parserOptions): ESLint 사용을 위해 지원하려는 Javascript 언어 옵션을 설정할 수 있다.
    • 규칙(rules): 프로젝트에서 자체적으로 덮어쓰고 싶은 규칙을 정의할 때 사용한다.

    설정 규칙 및 리스트는 상당히 방대하며, ESLint 공식 레퍼런스에서 참고 가능하다.

    ESLint 검사

    이제 위에서 엉망으로 작성한 test.js 파일을 아래 명령어를 사용하여 검사해 보자.

    $ node_modules/.bin/eslint test.js
    

    터미널에서 오류를 세가지를 아래처럼 알려준다.

    C:\code\test\test.js
      1:5   error  'foo' is assigned a value but never used  no-unused-vars
      1:11  error  'text' is not defined                     no-undef
      1:16  error  Unnecessary semicolon                     no-extra-semi
    
    ✖ 3 problems (3 errors, 0 warnings)
      1 error and 0 warnings potentially fixable with the `--fix` option.
    
    1. foo에 값이 할당되었지만 사용되지 않음
    2. text가 정의되지 않았음
    3. 불필요한 세미콜론이 있음

    위에서 3번째 항목에는 오른쪽에 no-extra-semi라고 나와있다. 이는 ESLint가 의도된 것이 아닌 실수라고 판단한 것이며, 자동으로 코드를 수정할 수 있다. 검사 명령어에 --fix를 붙이면 no-extra-semi항목을 자동으로 고쳐준다.

    $ node_modules/.bin/eslint test.js --fix
    

    명령어를 실행하면 세미콜론이 두개에서 한개로 바뀐것을 확인할 수 있다.

    실제 프로젝트에서는 검사할 파일이 많은데 위와 같이 일일히 터미널에서 ESLint를 실행하는것은 비효율적이다. 그래서 일반적으로 ESLint를 사용할때는 package.json 파일에 따로 설정해준다. 전체 파일을 상대로 ESLint를 실행되도록 설정해 보자.

      "scripts": {
        "lint": "eslint ."
      },
    

    이제 터미널에 아래 명령어를 실행하면 전체 파일을 상대로 ESLint가 실행될 것이다.

    $ npm run lint
    

    Prettier

    Prettier는 기존의 코드에 적용되어있던 스타일들을 전부 무시하고, 정해진 규칙에 따라 자동으로 코드 스타일을 정리해 주는 코드 포멧터이다.

    코드 포멧터(Code Formatter)란 개발자가 작성한 코드를 정해진 코딩 스타일을 따르도록 변환해주는 도구를 말한다.

    ESLint와 다른점이라면 ESLint는 문법 에러를 잡아내고, 특정 문법 요소를 쓰도록 만드는 등 코드 퀄리티와 관련된 것을 고치기 위해 사용되지만 Prettier는 코드 한 줄의 최대 길이나, 탭의 길이는 몇으로 할 것인지, 따옴표는 홀따옴표(‘)나 쌍따옴표(“)중 무엇을 사용 할 것인지 등등 코드 퀄리티보단 코딩 스타일을 일괄적으로 통일하는 도구에 가깝다.

    다시 테스트를 위해 test.js에 엉망인 코드를 작성하였다.

    let func=function 
    ( )
        {
      let 
    foo  
    ='text'
    return     foo}
    

    Prettier 설치

    Prettier을 아래 명령어로 설치해 준다.

    $ npm i prettier -D -E
    

    위 명령어중 -E--save-exac의 축약법이다. 위 ESLint 모듈을 설치할때와 다르게 Prettier에서는 이 옵션을 붙이는 것을 권장하는데, 버전이 달라지면서 생길 스타일 변화를 막기 위해서라고 한다.

    Prettier 실행

    $ npx prettier test.js
    

    위 명령어를 사용하면 엉망인 코드가 아래처럼 올바른 코드로 포멧팅되어 터미널창에 출력이 된다.

    let func = function () {
      let foo = "text";
      return foo;
    };
    

    코드 자체를 수정하고 싶다면 명령어에 --write 옵션을 추가하면 된다.

    $ npx prettier --write test.js
    

    에디터에 아래처럼 코드가 수정된다.

    let func = function () {
      let foo = "text";
      return foo;
    };
    

    이제 리액트 프로젝트에 ESLint와 Prettier를 적용하는 방법을 알아보겠다.

    리액트에 ESLint와 Prettier 적용하기

    이 포스팅에서는 CRA(Create React App)을 이용하여 생성한 리액트 프로젝트를 기준으로 한다. 아래 명령어를 사용하여 리액트 프로젝트를 생성한다.

    리액트 프로젝트 생성

    자바스크립트 사용 경우

    $ create-react-app 프로젝트이름 --use-npm
    

    타입스크립트 사용 경우

    $ create-react-app 프로젝트이름 --use-npm --template typescript
    

    CRA로 생성된 프로젝트는 안에 ESLint가 따로 탑재되어 있기 때문에 위처럼 따로 설치할 필요가 없다. 만약 CRA로 생성한 프로젝트에 수동으로 ESLint를 설치한다면, 프로젝트는 실행이 안되고, 터미널에 아래 경고가 출력될 것이다.

    There might be a problem with the project dependency tree.
    It is likely not a bug in Create React App, but something you need to fix locally.
    
    The react-scripts package provided by Create React App requires a dependency:
    
      "eslint": "^6.6.0"
    
    Don't try to install it manually: your package manager does it automatically.
    However, a different version of eslint was detected higher up in the tree:
    
      /Users/kongseongjoo/Documents/app/node_modules/eslint (version: 7.5.0) 
    
    Manually installing incompatible versions is known to cause hard-to-debug issues.
    
    If you would prefer to ignore this check, add SKIP_PREFLIGHT_CHECK=true to an .env file in your project.
    That will permanently disable this message but you might encounter other issues.
    .
    .
    .
    

    이는 기본 탑재되는 node_module과 수동설치한 node_module의 버전 호환성 문제이며 터미널 경고 아래에 해결 방법이 나온다. CRA로 생성된 프로젝트는 ESLint를 따로 설치를 하지 않도록 한다.

    Prettier 설치

    $ npm i prettier -D -E
    

    필요한 추가 모듈

    Prettier와 ESLint를 같이 사용하려면 아래 모듈을 추가로 설치해야 한다.

    • eslint-config-prettier
      ESLint의 formatting 관련 설정 중 Prettier와 충돌하는 부분을 비활성화 한다.
    • eslint-plugin-prettier
      Prettier를 ESLint 플러그인으로 추가한다. 즉, Prettier에서 인식하는 코드상의 포맷 오류를 ESLint 오류로 출력해준다.

    아래 명령어를 입력하여 위에서 언급한 모듈을 설치한다.

    $ npm i eslint-plugin-prettier eslint-config-prettier -D
    

    그리고 프로젝트의 루트 경로에 .eslintrc.json파일을 만들고 아래 내용을 추가한다.

    {
      "plugins": ["prettier"],
      "extends": ["eslint:recommended", "plugin:prettier/recommended"],
      "rules": {
        "prettier/prettier": "error"
      }
    }
    

    ESLint, Prettier 익스텐션 설치

    Node 모듈을 설치했으니, 이제 VSCode와 같이 사용할 때 필요한 익스텐션을 설치하고 설정을 바꿔주자. VSCode의 Extensions: Marketplace로 들어가서 ESLint와 Prettier를 검색하여 설치한다.

    VSCode 설정

    VSCode에서 파일을 저장할 때마다 자동으로 코드가 수정되도록 설정해보자. VSCode 설정(윈도우, 리눅스에서는 Ctrl + , 맥에서는 Cmd + ,)으로 들어간다. 설정으로 들어가면 Search settings 입력창 아래에 User와 Workspace 항목이 있다. User는 VSCode 자체 설정으로 모든 프로젝트에 적용이 되고, Workspace는 현재 프로젝트에서만 설정이 적용되며, .vscode/settings.json에 설정 항목이 저장된다. ESLint와 Prettier의 경우 프로젝트별로 설정이 다른경우가 많이 때문에 작업공간마다 설정파일을 따로 관리하는 것을 선호한다. 설정은 json파일에 직접 입력이 가능하며, 우측 상단에 종이 모양의 Open Setting(JSON)아이콘을 클릭하면 settings.json파일이 열린다. 아래처럼 설정한다.

    {
      // Set the default
      "editor.formatOnSave": false,
      // Enable per-language
      "[javascript]": {
        "editor.formatOnSave": true
      },
      "editor.codeActionsOnSave": {
        // For ESLint
        "source.fixAll.eslint": true
      }
    }
    

    ESLint 설정

    ESLint 규칙은 상당히 방대하며, 모든것을 다 바꾸기 어렵기 때문에 여러가지 규칙을 정해준 모음이 있는데 위에서 언급한 Airbnb Style Guide나 Google Style Guide가 있다. 여기선 Airbnb를 적용해 보겠다.

    Airbnb Style Guide 적용

    아래 명령어를 사용하여 Airbnb를 설치해준다.

    $ npm i -D eslint-config-airbnb
    

    eslint-config-airbnb 말고도 eslint-config-airbnb-base가 있는데, 차이는 base에는 리액트 관련 규칙이 들어있지 않다는 점이다.

    그리고 .eslintrc.json파일의 "extends"속성에 "airbnb"를 추가 해준다.

    {
      "plugins": ["prettier"],
      "extends": ["eslint:recommended", "plugin:prettier/recommended", "airbnb"],
      "rules": {
        "prettier/prettier": "error"
      }
    }
    

    App.js파일을 열어보면 빨간줄이 엄청 그어져 있을 것이다. Airbnb의 규칙이 상당히 까다롭기 때문이다. 꼭 Airbnb 규칙을 따를 필요는 없다. ESLint 문서에서 본인 스타일에 맞는 스타일을 찾거나 수정하여 사용하면 된다.

    Prettier 설정

    ESLint 설정 파일과 마찬가지로 루트 경로에 .prettierrc.json을 만들어 준다. Prettier의 옵션 문서에서 필요한 옵션을 골라 설정해 주면 된다. 아래는 간단한 예시이다.

    {
      "trailingComma": "es5",
      "tabWidth": 2,
      "semi": true,
      "singleQuote": true
    }
    

    References

    [자바스크립트] ESLint로 소스 코드의 문제 찾기
    [자바스크립트] Prettier로 코딩 스타일 통일하기
    VS Code에서 ESlint와 Prettier 함께 사용하기
    ESLint 설정 살펴보기
    27. 리액트 개발 할 때 사용하면 편리한 도구들 – Prettier, ESLint, Snippet
    ESLint 조금 더 잘 활용하기
    Airbnb JavaScript Style Guide
    프론트엔드 개발환경의 이해: 린트
    ESLint(TSLint)와 Prettier 함께 사용하기
    리액트 프로젝트에 ESLint 와 Prettier 끼얹기
    ESLint

  • [JavaScript] 재귀 함수(Recursive Function)

    [JavaScript] 재귀 함수(Recursive Function)

    정의

    재귀(Recursive)를 정의한다면 한 함수가 자기 자신을 호출하는 순간이다. 재귀함수를 이해하기 전에는 팩토리얼 이라는 개념이 필요하다.

    팩토리얼

    팩토리얼이란 자기 자신의 수에 1 작은 수를 곱하고 또 1 작은 수를 곱하고 해서 1 작은 수가 1이 될때까지 곱하는 것이다. 팩토리얼의 기호는 !이며 아래의 예제는 5!의 팩토리얼을 나타내고 있다.

    5 * 4 * 3 * 2 * 1 = 120
    

    재귀 함수

    이제 팩토리얼의 개념을 알았으니 재귀 함수가 무엇인지 확인해 본다.

    function factorial(x) {
      if (x < 0) return;
      if (x === 0) return 1;
      return x * factorial(x - 1);
    }
    console.log(factorial(3)); // 6
    

    결과 값이 나오는 과정을 순서대로 살펴보겠다.

    1. factorial 함수에 인자값으로 3을 담아 함수를 실행한다.
    2. 파라미터로 3을 받아 처음 조건문을 거친다. 30보다 크기 때문에 다음 조건문으로 이동한다. 30이 아니기 때문에 아래 구문으로 넘어간다.
    3. 3factorial 함수에 2를 넣은 결과 값을 곱하라는 구문이다. 다시 factorial 함수에 인자로 2를 넣어 실행한다.
    4. 20보다 크고 0이 아니니 다시 아래 구문으로 내려간다. 2factorial 함수에 1를 넣은 결과 값을 곱하라는 구문이다. 다시 factorial 함수에 인자로 1를 넣어 실행한다.
    5. 1역시 0보다 크고 0이 아니니 다시 아래 구문으로 내려간다. 1factorial 함수에 0를 넣은 결과 값을 곱하라는 구문이다. 다시 factorial 함수에 인자로 0를 넣어 실행한다. 위와는 다르게 0은 두번째 조건문에서 걸려 1을 리턴한다. 결국은 3 * 2 * 1라는 값을 리턴하게 되는 것이다. 이렇게 함수가 자기 자신을 호출하는 순간을 재귀 라고 한다.

    References

    재귀 함수
    자바스크립트 개발자라면 알아야 할 33가지 개념 #23 자바스크립트 : 자바스크립트 재귀(Recursion) 이해하기

  • [UI] 탭메뉴(tab menu)

    [UI] 탭메뉴(tab menu)

    설명

    vanillaJS로 제작된 기본 탭 메뉴

    HTML

    <div id="tab">
        <div class="btn">
            <button type="button">01</button>
            <button type="button">02</button>
            <button type="button">03</button>
            <button type="button">04</button>
        </div>
        <div class="cnt">
            <div>content01 content01 content01 content01 content01 content01 content01 content01 content01 content01</div>
            <div>content02 content02 content02 content02 content02 content02 content02 content02 content02 content02</div>
            <div>content03 content03 content03 content03 content03 content03 content03 content03 content03 content03</div>
            <div>content04 content04 content04 content04 content04 content04 content04 content04 content04 content04</div>
        </div>
    </div>
    

    CSS

    #tab {
        border: 1px solid #ccc;
        width: 300px;
    }
    
    #tab .btn:after {
        content: '';
        display: block;
        clear: both;
    }
    
    #tab .btn button {
        float: left;
        border: 0;
        width: 25%;
        height: 30px;
        cursor: pointer;
        outline: none;
        background-color: #ccc;
    }
    
    #tab .btn button:hover {
        background-color: #fff;
    }
    
    #tab .btn button.on {
        background-color: #fff;
    }
    
    #tab .cnt div {
        display: none;
    }
    
    #tab .cnt div.on {
        display: block;
    }
    

    JavaScript

    window.addEventListener('load', function(){
    
        var tab = document.getElementById('tab'),
            btn = tab.getElementsByClassName('btn')[0],
            cnt = tab.getElementsByClassName('cnt')[0],
            index = 0;
    
        btn.children[0].classList.add('on');
        cnt.children[0].classList.add('on');
    
        for(var i = 0;i < btn.children.length;i++){
            (function(target){
                btn.children[target].addEventListener('click', function(){
                    tabOn(target);
                });
            })(i);
        };
    
        function tabOn(target){
            for(var i = 0;i < btn.children.length;i++){
                btn.children[i].classList.remove('on');
                cnt.children[i].classList.remove('on');
            };
            btn.children[target].classList.add('on');
            cnt.children[target].classList.add('on');
        }
    
    });
    
  • [UI] 슬라이드 배너(Slide Banner)

    [UI] 슬라이드 배너(Slide Banner)

    설명

    vanillaJS로 만들어본 기본 슬라이드 배너

    조건

    • 라이브러리 없이 자바스크립트만 활용
    • 다음, 이전 버튼으로 슬라이드 조작 가능
    • 인디게이터는 슬라이드 갯수에 맞게 자동으로 생성
    • 자동재생, 일시정지 버튼으로 웹 접근성 준수

    HTML

    <div id="slide">
      <ul class="cnt">
        <li>1</li>
        <li>2</li>
        <li>3</li>
        <li>4</li>
      </ul>
      <div class="btn">
        <button type="button" class="prev">prev</button>
        <button type="button" class="next">next</button>
      </div>
      <div class="auto">
        <button type="button" class="stop">stop</button>
        <button type="button" class="play">play</button>
      </div>
    </div>
    

    CSS

    #slide {
      position: relative;
      overflow: hidden;
      width: 300px;
      height: 300px;
    }
    
    #slide .cnt > li {
      position: absolute;
      top: 0;
      left: 300px;
      width: 300px;
      height: 300px;
      text-align: center;
      font-size: 30px;
      line-height: 300px;
      color: #fff;
    }
    
    #slide .cnt > li:nth-child(1) {
      background-color: red;
    }
    
    #slide .cnt > li:nth-child(2) {
      background-color: orange;
    }
    
    #slide .cnt > li:nth-child(3) {
      background-color: green;
    }
    
    #slide .cnt > li:nth-child(4) {
      background-color: blue;
    }
    
    #slide .btn > button {
      position: absolute;
      top: 50%;
      transform: translateY(-50%);
      border: 0;
      padding: 5px;
      background-color: #fff;
    }
    
    #slide .btn .prev {
      left: 5px;
    }
    
    #slide .btn .next {
      right: 5px;
    }
    
    #slide .auto > button {
      display: none;
      position: absolute;
      bottom: 5px;
      right: 5px;
      border: 0;
      padding: 5px;
      background-color: #fff;
    }
    
    #slide .indi {
      position: absolute;
      bottom: 10px;
      left: 50%;
      transform: translateX(-50%);
    }
    
    #slide .indi:after {
      content: "";
      display: block;
      clear: both;
    }
    
    #slide .indi > li {
      float: left;
      margin-left: 5px;
      border-radius: 50%;
      width: 12px;
      height: 12px;
      cursor: pointer;
      opacity: 0.5;
      background-color: #fff;
    }
    
    #slide .indi > li.on {
      opacity: 1;
    }
    
    #slide .indi > li:first-child {
      margin-left: 0;
    }
    

    JavaScript

    window.addEventListener("load", function () {
      var MOVEING_PX = 4,
        AUTO_TIME = 2000,
        slide = document.getElementById("slide"),
        indi = document.createElement("ul"),
        slideCnt = slide.getElementsByClassName("cnt"),
        slideCntItem = slideCnt[0].getElementsByTagName("li"),
        prevBtn = slide.getElementsByClassName("prev"),
        nextBtn = slide.getElementsByClassName("next"),
        playBtn = slide.getElementsByClassName("play"),
        stopBtn = slide.getElementsByClassName("stop"),
        playSet = null,
        before = 0,
        after = 0,
        moveIng = false;
    
      // init
      slideCntItem[0].style.left = 0;
      playBtn[0].style.display = "block";
      var indi = document.createElement("ul");
      for (var i = 0; i < slideCntItem.length; i++) {
        indi.innerHTML += "<li></li>";
      }
      indi.classList.add("indi");
      indi.children[0].classList.add("on");
      slide.append(indi);
    
      for (var j = 0; j < indi.children.length; j++) {
        indiClick(j);
      }
    
      // initEvnet
      nextBtn[0].addEventListener("click", function (e) {
        if (!moveIng) {
          after++;
          if (after >= slideCntItem.length) {
            after = 0;
          }
          move(after, before, "next");
          before = after;
        }
      });
    
      prevBtn[0].addEventListener("click", function (e) {
        if (!moveIng) {
          after--;
          if (after < 0) {
            after = slideCntItem.length - 1;
          }
          move(after, before);
          before = after;
        }
      });
    
      playBtn[0].addEventListener("click", function () {
        playBtn[0].style.display = "none";
        stopBtn[0].style.display = "block";
        playSet = setInterval(function () {
          if (!moveIng) {
            after++;
            if (after >= slideCntItem.length) {
              after = 0;
            }
            move(after, before, "next");
            before = after;
          }
        }, AUTO_TIME);
      });
    
      stopBtn[0].addEventListener("click", function () {
        playBtn[0].style.display = "block";
        stopBtn[0].style.display = "none";
        clearInterval(playSet);
      });
    
      function indiClick(target) {
        indi.children[target].addEventListener("click", function () {
          if (!moveIng) {
            after = target;
            if (after > before) {
              move(after, before, "next");
            } else if (after < before) {
              move(after, before);
            }
            before = after;
          }
        });
      }
    
      function move(after, before, type) {
        var nextX = type === "next" ? slide.offsetWidth : slide.offsetWidth * -1,
          prevX = 0,
          set = null;
        set = setInterval(function () {
          moveIng = true;
          if (type === "next") {
            nextX -= MOVEING_PX;
            slideCntItem[after].style.left = nextX + "px";
            if (nextX <= 0) {
              clearInterval(set);
              nextX = slide.offsetWidth;
              moveIng = false;
            }
            prevX -= MOVEING_PX;
          } else {
            nextX += MOVEING_PX;
            slideCntItem[after].style.left = nextX + "px";
            if (nextX >= 0) {
              clearInterval(set);
              nextX = slide.offsetWidth * -1;
              moveIng = false;
            }
            prevX += MOVEING_PX;
          }
          slideCntItem[before].style.left = prevX + "px";
        });
        indi.children[before].classList.remove("on");
        indi.children[after].classList.add("on");
      }
    });
    
  • [JavaScript] var, let, const 차이

    [JavaScript] var, let, const 차이

    정의

    var는 변수를 선언하는 키워드이며, ES6부터 letconst가 추가되었다. var는 함수 스코프를 가지고 letconst블록 스코프를 가진다. 여기서 스코프란 코드가 실행되는 유효범휘이다.

    블록 스코프는 중괄호{}로 감싸진 범위를 말한다. 아래는 조건문(if), 반복문(for), 함수(function)가 블록 스코프를 가지고 있는 모습이다.

    if (true) {
        // 블록 스코프
    }
    
    for (var i = 0; i < 10; i++) {
        // 블록 스코프
    }
    
    function func() {
        // 블록 스코프
    }
    

    var

    기존의 var는 위 세개중 함수에서만 스코프를 가진다.

    if (true) {
        var a = 1;
    }
    console.log(a);
    // 1
    
    for (var i = 0; i < 1; i++) {
        var b = 2;
    }
    console.log(b);
    // 2
    
    function func() {
        var c = 3;
    }
    console.log(c);
    // Uncaught ReferenceError: foo is not defined at window.onload
    

    var는 중복 선언이 가능하다.

    var a = 1;
        a = 2;
    console.log(a);
    // 2
    

    let, const

    ES6부터 추가된 let, const는 조건문과 반복문에도 스코프를 가진다.

    if (true) {
        let a = 1;
        const b = 2;
        console.log(a); // 1
        console.log(B); // 2
    }
    console.log(a);
    // Uncaught ReferenceError: a is not defined at window.onload
    console.log(b);
    // Uncaught ReferenceError: a is not defined at window.onload
    
    for (var i = 0; i < 1; i++) {
        let c = 3;
        const d = 4;
        console.log(c); // 3
        console.log(d); // 4
    }
    console.log(c); // Uncaught ReferenceError: a is not defined at window.onload
    console.log(d); // Uncaught ReferenceError: a is not defined at window.onload
    
    function func() {
        let e = 5;
        const f = 6;
        console.log(e); // 5
        console.log(f); // 6
    }
    func()
    console.log(e);
    // Uncaught ReferenceError: foo is not defined at window.onload
    console.log(f);
    // Uncaught ReferenceError: foo is not defined at window.onload
    

    letconst의 차이는 const 가 좀 더 엄격하다. let은 중복선언이 되지만 const는 중복선언이 안되기 때문에 변수가 선언될 때 값을 할당하여야 한다. const는 DB환경정보, API응답값 등 변하지 않을 값을 담을 때 사용한다.

    let a;
    a = 1;
    console.log(a);
    // 1
    
    const b;
    b = 2;
    // Uncaught TypeError: Assignment to constant variable.
    

    결론

    • var은 함수 스코프에서만 유효
    • let, const는 블록 스코프에서 유효
    • const는 선언과 동시에 할당이 일어나야하고, 재할당이 불가

    References

    자바스크립트 변수와 스코프(유효범위)

  • [UI] 전체동의 체크박스(agree checkbox)

    [UI] 전체동의 체크박스(agree checkbox)

    설명

    vanillaJS로 만들어진 전체동의 체크박스, 보통 이용약관 등에 많이 사용된다.

    조건

    • 전체동의 클릭 시 모든 체크박스 체크
    • 전체동의 클릭 해제 시 모든 체크박스 체크 해제
    • 전체동의 체크박스 외 체크박스가 모두 체크 될 시 전체동의 체크박스 체크
    • 전체동의 체크박스 외 체크박스가 모두 체크 해제 될 시 전체동의 체크박스 체크 해제

    HTML

    <div id="checkWrap">
        <label for="cheeck01"><input type="checkbox" id="cheeck01" />전체 동의</label>
        <label for="cheeck02"><input type="checkbox" id="cheeck02" />동의</label>
        <label for="cheeck03"><input type="checkbox" id="cheeck03" />동의</label>
        <label for="cheeck04"><input type="checkbox" id="cheeck04" />동의</label>
    </div>
    

    CSS

    #checkWrap {
        width: 120px;
    }
    
    #checkWrap label {
        display: block;
        margin: 5px;
        font-size: 14px;
    }
    
    #checkWrap label input[type="checkbox"] {
        margin-right: 5px;
        width: 20px;
        height: 20px;
        vertical-align: middle;
    }
    
    #checkWrap label:first-child input[type="checkbox"] {
        width: 30px;
        height: 30px;
    }
    

    JavaScript

    window.onload = function(){
        var checkWrap = document.getElementById('checkWrap'),
            check = checkWrap.getElementsByTagName('input');
        checkWrap.addEventListener('click', function(e){
            var target = e.target,
                checkNum = 0;
            if(target === check[0]){
                if(target.checked){
                    for(var i = 1;i < check.length;i++){
                        check[i].checked = true;
                    };
                }else{
                    for(var i = 1;i < check.length;i++){
                        check[i].checked = false;
                    };
                };
            }else{
                for(var i = 1;i < check.length;i++){
                    if(check[i].checked){
                        checkNum++;
                    };
                };
                if(checkNum >= check.length - 1){
                    check[0].checked = true;
                }else{
                    check[0].checked = false;
                };
            };
        });
    };