728x90
반응형

지금까지의 이미지 & 컨테이너에서는 한 종류의 데이터만 다뤄봤지만 이번엔 볼륨이라는 개념과 함께 다른 종류의 데이터들도 다뤄보는 방법을 알아보겠다.

 

1. 아래 파일을 다운로드 한다.

data-volumes-01-starting-setup.zip
0.01MB

 

npm package들을 다운받고 서버를 실행해보자.

$ npm install
$ node server.js

 

localhost 80번 포트로 웹을 열어보면

 

이런 식으로 화면이 구성 된 것을 볼 수 있다. 서버 파일을 잠시 보면

 

app.post('/create', async (req, res) => {
  const title = req.body.title;
  const content = req.body.text;

  const adjTitle = title.toLowerCase();

  const tempFilePath = path.join(__dirname, 'temp', adjTitle + '.txt');
  const finalFilePath = path.join(__dirname, 'feedback', adjTitle + '.txt');

  await fs.writeFile(tempFilePath, content);
  exists(finalFilePath, async (exists) => {
    if (exists) {
      res.redirect('/exists');
    } else {
      await fs.rename(tempFilePath, finalFilePath);
      res.redirect('/');
    }
  });
});

내가보낸 피드백이 파일 형식으로 저장이 되는 것 또한 볼 수 있다.

 

이제부턴 이 앱을 도커화를 해보자. 먼저 Dockerfile을 만들어준다.

 

/Dockerfile

FROM node:14

WORKDIR /app

COPY package.json .

RUN npm install

COPY . .

EXPOSE 80

CMD ["node","server.js"]

 

터미널에서 만든 Dockerfile로 이미지를 빌드한다.

$ docker build -t feedback-node .

 

빌드된 이미지로 run 명령어를 실행한다.

$ docker run -p 3000:80 -d --name feedback-app --rm feedback-node

 

이제는 localhost 3000번 포트에서 사이트를 볼 수 있다.

여기서 피드백을 작성하고 저장을 하면 localhost:3000/feedback/title.txt ( 피드백 제목 )

경로에서 내용을 확인할 수도 있다. ( 서버파일 참조 )

 

하지만 우리는 이미 켜둔 VSCode 프로젝트 안에서는 feedback 폴더 밑에 파일이 생기지 않은 것을 알 수 있다.

이는 당연하겠지만 이미지는 컨테이너를 만드는데에 쓰임이 끝났고, 파일은 격리된 컨테이너에만 생성됐기 때문이다.

 

2. 볼륨이란?

볼륨은 도커에 내장된 기능이며 위에 경우처럼 컨테이너가 삭제될 경우 데이터를 보존할 수 있도록 도와준다.

볼륨은 컨테이너나 이미지에 있는게 아니라 호스트 컴퓨터에 장착된 하드 드라이브에 존재하여 사용가능하거나 컨테이너로 매핑되는 것을 의미한다.

 

3. 컨테이너에 볼륨 적용하기

Dockerfile 파일 안에 VOLUME 을 추가해줄 수 있다.

/Dockerfile

FROM node:14

WORKDIR /app

COPY package.json .

RUN npm install

COPY . .

EXPOSE 80

# 저장하려는 볼륨
VOLUME ["/app/feedback"]

CMD ["node","server.js"]

 

이제 다시 이미지를 빌드하고 실행해보자.

$ docker build -t feedback-node:volumes .
$ docker run -d -p 3000:80 --rm --name feedback-app feedback-node:volumes

 

이러고 서버를 실행해보면 

피드백을 작성하고 저장버튼을 누르면 로딩이 되면서 더이상 진행이 안되는 것을 확인할 수 있다. 

로그를 확인해보자.

 

$ docker logs feedback-app

 

/create 라우터를 보면 fs.rename이라는 메소드가 있는데 이 부분을 바꿔준다.

app.post('/create', async (req, res) => {
  const title = req.body.title;
  const content = req.body.text;

  const adjTitle = title.toLowerCase();

  const tempFilePath = path.join(__dirname, 'temp', adjTitle + '.txt');
  const finalFilePath = path.join(__dirname, 'feedback', adjTitle + '.txt');

  await fs.writeFile(tempFilePath, content);
  exists(finalFilePath, async (exists) => {
    if (exists) {
      res.redirect('/exists');
    } else {
      // await fs.rename(tempFilePath, finalFilePath);
      await fs.copyFile(tempFilePath, finalFilePath);
      await fs.unlink(tempFilePath);
      res.redirect('/');
    }
  });
});

 

이제 이미지를 삭제한 뒤 다시 만들고 실행해준다.

$ docker stop feedback-app
$ docker rmi feedback-node:volumes
$ docker rmi feedback-node:volumes
$ docker run -d -p 3000:80 --rm --name feedback-app feedback-node:volumes

 

다시 실행해보면 정상적으로 파일이 읽히는 것을 볼 수 있다.

 

이번엔 컨테이너를 종료하고 파일이 잘 남아있는지 확인해보자.

$ docker stop feedback-app

 

이러고 다시 실행해서 확인해보면 test.txt 가 읽혀지지 않는 것을 볼 수 있다.

 

4. 볼륨의 종류

볼륨의 종류에는 명명된 볼륨, 익명의 볼륨이 있다.

볼륨을 확인하는 명령은 다음과 같다.

$ docker volume ls

 

Dockerfile에서 VOLUME 레이어를 삭제한 뒤 이미지를 재구축하고 다시 실행해보자. 

이 때 run 명령어에 볼륨을 지칭하는 부분을 넣어준다.

$ docker stop feedback-app
$ docker rmi feedback-node:volumes
$ docker rmi feedback-node:volumes
$ docker run -d -p 3000:80 -rm --name feedback-app -v feedback:/app/feedback feedback-node:volumes

 

이러면 컨테이너를 삭제하더라도 볼륨의 데이터가 정상적으로 남아있는 것을 확인할 수 있다.

 

4. 바인드 마운트

우리가 개발한 환경을 이미지화해서 컨테이너를 만들고, 다시 이미지를 수정할 경우 계속 언급해왔듯 변경 사항이 바로 반영이 되지않으므로 이미지를 재구축하고 다시 실행해야 했다. 개발하는 동안에 코드를 수정할 때마다 이 과정을 반복하면 매우 불편하고 시간 소요도 클 것이다. 이것을 바인드 마운트가 도와준다.

 

호스트 컴퓨터의 절대경로로 현재 app이 지정하는 파일 경로를 붙여서 다음과같이 run 을 실행한다. ( 한 줄로 구성된 명령어입니다. )

$ docker run -d -p 3000:80 -rm --name feedback-app -v feedback:/app/feedback
-v "/Users/Desktop/workspace/docker:/app" feedback-node:volumes

 

5. docker volume 명령어들

 

728x90
반응형
728x90
반응형

전 포스트에서는 이미지에 대한 기본 지식과 빌드하는 방법을 살펴보았다.

이번 포스트에서는 이미지와 컨테이너를 관리하는 방법에 대해서 알아보겠다.

 

1. docker start vs docker run

docker run 명령어로 컨테이너를 실행할 때는 터미널이 더이상 명령어를 입력할 수 없는 것을 볼 수 있었다. 

docker start를 사용하면 백그라운드에서 컨테이너가 실행되기 때문에 별개로 터미널에서 다른 작업을 할 수 있다.

 

docker run 명령어로 컨테이너를 실행하면 터미널에서 실시간으로 컨테이너의 console.log 출력 결과를 확인할 수 있다.

이를 attached 모드라고 한다. 또한 컨테이너가 새로 생성되면서 실행까지 된다.

 

1. 만약 이미 실행중인 컨테이너에 연결하고 싶으면 docker  attach 를 사용하면 된다.

2. 컨테이너의 이미 지나간 로그 기록을 보고 싶으면 docker logs 라는 명령어도 존재한다. -f 옵션을 주면 attatch 까지 가능하다.

3. docker start 를 attatch 모드로 실행하고 싶으면 docker start -a 옵션을 주면 된다.

 

 

2. 컨테이너 삭제하기

컨테이너를 중지한 뒤 삭제를 안하고 새로 만들고 하다보면 언젠가 계속 쌓이게 될 것이다. 때때로 이 목록을 정리해야 될 필요가 있다.

docker rm '컨테이너이름' 명령어로 컨테이너를 삭제할 수 있는데 이는 컨테이너가 실행중이면 오류가 발생한다.

때문에 실행중이면 컨테이너를 종료하고 제거를 해야한다.

 

3. 이미지 삭제하기

이미지를 삭제할 때에는 docker rmi '이미지ID' 명령어를 사용한다. 다만 한 가지 유의사항이 있다면 해당 이미지를 가지고 있는 컨테이너가 한 개라도 존재할 경우엔 이미지 삭제가 불가능하다. 때문에 컨테이너를 먼저 삭제한 뒤 이미지를 삭제해야한다.

 

다만 docker image prune 이라는 명령어도 존재하는데, 이는 사용되지 않는 모든 이미지를 제거할 수 있게 해준다.

 

4. 중지된 컨테이너 자동으로 제거하기 

이미지로 docker run 을 실행하여 만들어진 컨테이너를 중지될 때 자동으로 제거하게끔 만드는 명령어가 있다.

docker run -p 3000:80 -d --rm '이미지id'

 

5. 컨테이너와 이미지에 이름 지정 & 태그 지정하기

 컨테이너의 자동 생성된 이름을 사용하기에는 불편함이 있을 수 있다. ( 까먹거나 너무 길 경우에 )

docker run -p 3000:80 -d --rm --name teepo '이미지id'

 

이미지에는 이름과 태그를 설정할 수 있는데,

name : tag 이런식으로 설정된다. ( tag 는 부 주제로 버전 등을 나타낼 수 있다)

docker build -t teepo:latest -> latest는 최신 버전임을 나타냄, 숫자도 가능

 

docker images 명령어로 이미지들을 확인할 수 있다.

 

 

728x90
반응형
728x90
반응형

1. 이미지 vs 컨테이너

 컨테이너는 애플리케이션, 웹사이트, 노드 서버, 애플리케이션을 실행하는 전체 환경 등 무엇이든 포함하는 유닛 패키지다.

이미지는 쉽게 말해 컨테이너의 블루프린트가 되는 것인데, 실제로 코드와 코드를 실행하는데 필요한 도구를 포함한다.

예를 들어 NodeJS 환경이 깔린 이미지가 있으면 도장처럼 찍어서 여러개의 nodeJS 환경이 구성된 컨테이너를 만들어낼 수 있다.

 

2. 외부 (사전 빌드된) 이미지의 사용 및 실행

 도커 허브 사이트에 가보면 매우 일반적인 ( 우분투나 node 같은 ) 이미지를 받을 수 있다.  아래 명령어를 입력해보자.

$ docker run node

 그 다음 아래 명령어를 입력하면 컨테이너가 생성되어 실행되는 것을 확인할 수 있다.

 

$ docker ps -a

 

도커 프로그램을 켜봐도 알 수 있다.

 

보통은 이렇게 일반적인 이미지로 컨테이너를 만들고 자신의 환경에 맞게 코딩한 다음 나만의 이미지로 만들어서 사용한다.

 

3. 나만의 node 이미지 만들기

1. 먼저 파일을 다운로드 받고 VSCode 에서 폴더를 연다.

nodejs-app-starting-setup.zip
0.00MB

 

2. 설치된 npm 패키지들을 받기 위해 아래 명령어를 입력한다.

$npm install

 

 

3. Docker 확장 프로그램을 설치하고 다운로드 받았던 프로젝트 안에 Dockerfile 을 만들어준다.

 

 

/Dockerfile

# 위에서 도커 허브 node 이미지를 기반으로 로컬로 다운로드 및 캐싱 되었기 때문에 이미지를 가져올 수 있다.
FROM node 

# 만약 컨테이너 안의 이미지의 경로가 /app 이런식으로 되어있다면 작업할 div 경로를 설정할 수도 있다.
# 설정해주면 COPY 의 두번째 경로를 ./ 이것으로 했을 때 자동으로 /app 경로가 된다.
WORKDIR /app

# 어떤 파일이 이미지에 들어가야 하는지 
# 첫 번째 .은 이 프로젝트의 모든 폴더 및 파일들 (Dockerfile을 제외한)
# 두 번째 .은 파일을 저장할 컨테이너 내부 경로 (ex /app)
COPY . /app

# 이미지를 받으면 npm install을 자동으로 해줌
RUN npm install

# 도커에게 우리가 서버를 실행할 포트를 말해준다.
EXPOSE 80

# 이미지가 생성될 때 실행되지 않고 컨테이너가 실행될 때 수행하는 명령어
CMD ["node", "server.js"]

 

4. 터미널에서 아래 명령어를 입력해보자

$docker build .

위에 생성된 image ID 를 복사해서 docker run을 해보면,

이렇게 컨테이너가 실행된 것을 확인할 수 있다.

 

이제 이미지로 만들었고 컨테이너 실행도 했겠다 80번 포트에 열린 사이트를 확인해 보자.

??? 화면이 뜨질 않는다 이 이유는 아래에 나온다.

 

 

5. docker ps 명령어를 입력하면 현재 실행중인 프로세스를 확인할 수 있다.

컨테이너 실행도 잘 되었다. 하지만 우리는 문서상으로만 포트 80번을 expose 한다고 명시하였고, 실제로 컨테이너안의 포트를 로컬 포트에 할당시켜주질 않았다. 이는 run 명령어를 실행할 때 쓰면 된다. 일단 컨테이너를 중지시키고 포트를 할당해준 뒤 다시 시작해보자.

 

$ docker stop '위에서 확인한 컨테이너의 이름'

 

 

포트를 할당해주고 다시 실행한다. (위에서 만들었던 컨테이너의 id 를 씁니다.)

$ docker run -p 3000:80 sha256:5ed67ec470b78df4b721d82a9ae3924059e432e58d710782d2f73a6efd6dbb75

 

그 다음 웹을 켜보면

로컬 3000번 포트에 컨테이너의 80번 포트가 할당되어 잘 나타나는 것을 볼 수 있다.

 

이미지는 읽기 전용이기 때문에 이 방법으로는 만약 변경사항이 생겨 적용하고 싶으면 이미지를 다시 빌드해야 한다.

 

 

또한 Dockerfile 이미지 레이어에는 캐시를 사용하는데, 빌드했었던 이미지를 다시 빌드할 때 변경사항이 없으면 매우 빠르게 빌드된다. 

이것을 이용해서 다음과 같이 npm package에 대한 변경사항이 없을 때 npm install 을 빠르게 넘어갈 수도 있다

 

/Dockerfile

# 위에서 도커 허브 node 이미지를 기반으로 로컬로 다운로드 및 캐싱 되었기 때문에 이미지를 가져올 수 있다.
FROM node 

# 만약 컨테이너 안의 이미지의 경로가 /app 이런식으로 되어있다면 작업할 div 경로를 설정할 수도 있다.
# 설정해주면 COPY 의 두번째 경로를 ./ 이것으로 했을 때 자동으로 /app 경로가 된다.
WORKDIR /app

# package.json 파일을 복사한다. 만약 다시 빌드할 때 변경사항이 없을 경우 npm install까지 그냥 넘어간다.
COPY package.json /app

# 이미지를 받으면 npm install을 자동으로 해줌
RUN npm install

# 어떤 파일이 이미지에 들어가야 하는지 
# 첫 번째 .은 이 프로젝트의 모든 폴더 및 파일들 (Dockerfile을 제외한)
# 두 번째 .은 파일을 저장할 컨테이너 내부 경로 (ex /app)
COPY . /app

# 도케에게 우리가 서버를 실행할 포트를 말해준다.
EXPOSE 80

# 이미지가 생성될 때 실행되지 않고 컨테이너가 실행될 때 수행하는 명령어
CMD ["node", "server.js"]

 

 

728x90
반응형
728x90
반응형

1. Docker 란?

 Docker란 컨테이너를 생성하고 관리하기 위한 도구이다. 여기서 컨테이너란 표준화된 소프트웨어 유닛을 말한다.

기본적으로 해당 코드를 실행하는데 필요한 종속성과 도구가 포함된 패키지라고 보면 된다. 컨테이너라는 곳에 우리가 개발을 한 환경 자체를 넣어버리고 그 환경을 다른 곳에서 불러오면 문제없이 동일하게 사용할 수 있다.

 

2. 컨테이너가 왜 필요한가?

소프트웨어에서 왜 독립적인 표준화된 애플리케이션 패키지를 원하는지에 대한 이유를 생각해보면 컨테이너의 중요성을 좀 더 정확하게 깨닫게 된다. 도커의 주요 사용 사례 중 하나는 우리가 종종 다른 개발 제품 생산 환경을 가지게 될 때 환경 별로 관리하기 위한 사례인데,

 

예를 들어 Node js 버전이 14.3인 환경에서 

다음과 같은 코드를 짰을 때 에러가 뜨는 것을 확인할 수 있고 이에 따라 우리는 14.13 보다 높은 버전이 필요한 것을 알게 된다.

이와 같이 버전 문제로 인해 문법에 에러가 뜨는데 우리가 사용할 수 있는 환경이 로컬밖에 없다면 이 코드는 아예 사용할 수 없을 것이다.

그렇다고 코드 자체를 버리기엔 애매한 상황이 있을 때 우리가 로컬 환경 자체를 바꾼다고 해서(Node js 버전 업그레이드 등)는 상당한 소요가 필요할 것이다. (이 코드 뿐만 아니라 다른 곳에서 에러가 뜰 수도 있기 때문에)

이 때 애플리케이션이 자체적으로 필요한 Node 버전을 제공하는 컨테이너를 사용한다면 테스트를 해보기에 훨씬 편리할 수 있다.

 

물론 프로젝트가 몇 개 안되고 모든 프로젝트가 하나의 버전을 똑같이 사용한다면 별로 필요가 없겠지만, 큰 회사에서 복잡한 프로젝트들이 여러개 있을 경우 각각의 프로젝트에 적합한 버전이 있을 수도 있을 것이고, 그 복잡한 프로젝트를 똑같은 버전으로 만드는 것 자체가 큰 일이 될 수도 있다고 생각되었다. 그럴 때 컨테이너를 여러개 만들고 버전별로 관리하면 상당히 유용할 것이다.

 

3. 가상 머신과 도커 컨테이너의 차이점?

물론 위의 상황이 있을 때 가상머신으로도 버전 별로 프로젝트를 관리하는 것은 가능하다. 하지만 가상 머신을 사용할 때 가장 큰 문제점은 오버헤드이다. 컴퓨터 안에 컴퓨터를 집어넣는 행동을 여러번 반복하다보면 메모리, cpu, 내 하드 드라이브의 공간을 낭비하게 될 것이고 컴퓨터는 계속 무거워지고 나중엔 오버헤드가 발생할 것이다. 실제로 학생 때 가상 머신을 사용해봤는데 가상 환경을 한 개만 만들었어도 컴퓨터 성능이 별로 좋지 않으면 내 컴퓨터 자체가 느려지는 것을 볼 수 있었다. 이에 비해 컨테이너는 자체에 OS를 가지고 있고, 가상 머신보단 훨씬 가벼운 느낌이 있다.

 

4. Windows 에서 도커 설치하기

https://typo.tistory.com/entry/Docker-in-Windows?category=896128 

 

Docker | Windows |Docker in Windows

먼저 Windows 에서 도커를 사용하기 위해선 WSL2(Windows Subsystem for Linux 2)가 필요하다. 마이크로소프트 스토어에서 Windows Terminal을 다운받는다. 그다음 관리자 권한으로 실행한다. 터미널에 DISM 명..

typo.tistory.com

 

 

5. MacOS 에서 도커 설치하기

https://www.docker.com/products/docker-desktop/

 

Docker Desktop - Docker

MOST COMMON

www.docker.com

 

해당 사이트에서 다운로드 받으면 된다.

 

 

728x90
반응형
728x90
반응형

이번 포스트는 winston으로 만든 모듈을 라우터에 미들웨어로 사용하는 법을 익히겠다.

 

1. ./middleware/index.js 파일을 만든다

 

2. index.js 파일에서 looging 모듈을 export 해준다.

exports.logging = (req, res, next) => {

    try {
        logger.info('');
    }
    catch (Err) {
		logger.error(Err);
    }
}

 

여기서 어떤 내용을 로그에 담아 모아둘지 생각해야한다. 나같은 경우는

 

Error

  • error 로그는 각 라우터에서 확인

Info

  • 어떤 라우터에 접근했는지 
  • 토큰을 verify 하여 토큰에 대한 정보 ( 로그인 데이터 )
  • 메소드 별로 body 나 params, 또는 query (Request Data)

이렇게 할 생각이다. 코드로 구현해보면

 

./middleware/index.js

const { logger } = require('../config/winston');

const jwt = require("jsonwebtoken");
const secretObj = require("../config/jwt");

exports.logging = (req, res, next) => {
    var logStr = {}
    try {
        // 접속 경로
        logStr.url = req.originalUrl;
        console.log("cookies : ", req.cookies)

        const verify = jwt.verify(
            '쿠키 안의 토큰 위치',
            secretObj.secret,
            { expiresIn: '86400000' }
        )

        // 쿠키 안의 토큰 (로그인 정보)
        logStr.loginData = verify.isLogined // 토큰의 정보


        // 메소드
        logStr.method = req.method;

        switch (req.method) {
            case 'GET':
                logStr.query = req.query;
                break;
            case 'POST':
                logStr.body = req.body;
                break;
            case 'PATCH':
                logStr.body = req.body;
                break;
            case 'DELETE':
                logStr.query = req.query;
                break;
        }

        logger.info(JSON.stringify(logStr))

        next();
    }
    catch (Err) {
        logger.error(Err)
        res.send({ success: false });
    }
}

 

 

3. 로깅이 필요한 라우터 파일에 logger 를 import 해주고 미들웨어로 심어준다.

const { logging } = require('../middleware/index');

...

router.post("/list", logging, function (req, res, next) {

...

})

 

4. 파일을 열어 확인해보면 정상적으로 log Data 가 찍힌 것을 볼 수 있다.

 


원하는 색깔을 넣고 싶을 경우 다음과 같이 하면 된다.

const winston = require('winston');
const winstonDaily = require('winston-daily-rotate-file');

const logDir = 'logs';  // logs 디렉토리 하위에 로그 파일 저장
const { combine, timestamp, printf, colorize } = winston.format;

// Define log format
const logFormat = printf(info => {
    return `${info.timestamp} ${info.level}: ${info.message}`;
});

const colors = {
    error: 'red',
    warn: 'yellow',
    info: 'green',
    http: 'magenta',
    debug: 'blue'
}

winston.addColors(colors)


/*
 * Log Level
 * error: 0, warn: 1, info: 2, http: 3, verbose: 4, debug: 5, silly: 6
 */
const logger = winston.createLogger({
    format: combine(
        colorize({ all: true }),
        timestamp({
            format: 'YYYY-MM-DD HH:mm:ss',
        }),
        logFormat,
    ),
    transports: [
        // info 레벨 로그를 저장할 파일 설정
        new winstonDaily({
            level: 'info',
            datePattern: 'YYYY-MM-DD',
            dirname: logDir,
            filename: `%DATE%.log`,
            maxFiles: 30,  // 30일치 로그 파일 저장
            zippedArchive: true,
        }),
        // error 레벨 로그를 저장할 파일 설정
        new winstonDaily({
            level: 'error',
            datePattern: 'YYYY-MM-DD',
            dirname: logDir + '/error',  // error.log 파일은 /logs/error 하위에 저장 
            filename: `%DATE%.error.log`,
            maxFiles: 30,
            zippedArchive: true,
        }),
    ],
});

// Production 환경이 아닌 경우(dev 등) 
if (process.env.NODE_ENV !== 'production') {
    logger.add(new winston.transports.Console({
        format: winston.format.combine(
            winston.format.colorize(),  // 색깔 넣어서 출력
            winston.format.simple(),  // `${info.level}: ${info.message} JSON.stringify({ ...rest })` 포맷으로 출력
        )
    }));
}

module.exports = { logger };
728x90
반응형
728x90
반응형

프로젝트를 진행하다보면 로깅의 필요성을 절실히 깨닫게 된다. 

예를 들어 어떤 정보가 오고 갔는지, 에러가 떠서 프로그램이 멈췄을 경우 어떤 에러가 발생했었는지 등 활용 방안은 여러가지가 있다.

 

바로 시작해보자.

 

1. 필요한 npm 패키지를 설치한다.

$ npm install winston winston-daily-rotate-file --save

 

2. winston logger config 파일 작성

./config/winston.js

import winston from 'winston';
import winstonDaily from 'winston-daily-rotate-file';

const logDir = 'logs';  // logs 디렉토리 하위에 로그 파일 저장
const { combine, timestamp, printf } = winston.format;

// Define log format
const logFormat = printf(info => {
  return `${info.timestamp} ${info.level}: ${info.message}`;
});

/*
 * Log Level
 * error: 0, warn: 1, info: 2, http: 3, verbose: 4, debug: 5, silly: 6
 */
const logger = winston.createLogger({
  format: combine(
    timestamp({
      format: 'YYYY-MM-DD HH:mm:ss',
    }),
    logFormat,
  ),
  transports: [
    // info 레벨 로그를 저장할 파일 설정
    new winstonDaily({
      level: 'info',
      datePattern: 'YYYY-MM-DD',
      dirname: logDir,
      filename: `%DATE%.log`,
      maxFiles: 30,  // 30일치 로그 파일 저장
      zippedArchive: true, 
    }),
    // error 레벨 로그를 저장할 파일 설정
    new winstonDaily({
      level: 'error',
      datePattern: 'YYYY-MM-DD',
      dirname: logDir + '/error',  // error.log 파일은 /logs/error 하위에 저장 
      filename: `%DATE%.error.log`,
      maxFiles: 30,
      zippedArchive: true,
    }),
  ],
});

// Production 환경이 아닌 경우(dev 등) 
if (process.env.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.combine(
      winston.format.colorize(),  // 색깔 넣어서 출력
      winston.format.simple(),  // `${info.level}: ${info.message} JSON.stringify({ ...rest })` 포맷으로 출력
    )
  }));
}

export { logger };
// module.exports = { logger };

 

로그 레벨에 따라서 저장하는 파일 위치와 파일 명을 설정할 수도 있다.

error: 0, warn: 1, info: 2, http: 3, verbose: 4, debug: 5, silly: 6

 

3. log관리가 필요한 라우터에 import 하고 logger를 사용한다.

import express from 'express';
import { logger } from './config/winston';

const app = express().Application;

app.listen(3000, () => {
  logger.info('Server listening on port 3000');
});

app.get('/', (req, res) => {
  logger.info('GET /');
  res.sendStatus(200);
});

app.get('/error', (req, res) => {
  logger.error('Error message');
  res.sendStatus(500);
});

 

4. 파일이 제대로 생기는지 확인해본다.

728x90
반응형
728x90
반응형

아래와 같은 방법을 쓰면 사용할 수 있다.

class App extends React.Component {

  
  render() {
  let codes = "<b>Will This Work?</b>";
    return (
      <div dangerouslySetInnerHTML={ {__html: codes} }>
      </div>
    );
  }
};
728x90
반응형
728x90
반응형

kcp 를 쓰고 m_redirect_url을 지정해주면 모바일 웹앱 환경에서는 빌링키를 받을 때 콜백 함수가 실행이 되질 않는다..

하지만 이 방법을 쓰면 어떤 환경이든 다 가능하게끔 만들었다!

검색을 해보니 내가 redirect url 로 지정한 주소에 ?imp_uid="" 이런식으로 파라미터로 넘겨준다고 한다. 바로 방법이 떠올랐다.

  1. Next.js 에서 쓸 수있는 SSR 로 해당 파라미터를 읽는다.
  2. useEffect로 파라미터가 넘겨졌을때의 로직을 구성해준다.( 파라미터가 없을 경우엔 실행이 안되게끔)
  3. 파라미터로 넘겨졌을때의 라우터는 이전에 써왔던 것들과 동일하다.

1. 페이지들의 라우팅을 관리해주는 파일의 getServerSideProps 에서 파라미터를 읽는 부분을 추가해주자.

export const getServerSideProps: GetServerSideProps = async (context) => {
  const imp_uid = context.query.imp_uid ? context.query.imp_uid : "";
  const merchant_uid = context.query.merchant_uid ? context.query.merchant_uid : "";
  
  ...

이런식으로 imp_ud와 merchant_uid를 받고 구독을 신청하는 페이지까지 전달해준다. ( 컴포넌트끼리의 props 전달은 블로그에 있는 다른 글을 참조 바랍니다. )

 

abc.com/view/category?imp_uid="1234" 이런식으로 넘어오면 props에 "1234"라는 데이터가 들어올 것이고 아니면 "" 빈 스트링으로 올 것이다. 이걸 활용해보자.

 

2. useEffect 로 파라미터를 감지해서 빈 스트링이 아닐 때 이전에 콜백함수에 담겨있던 로직을 실행해준다.

interface LoginData {
    loginData: any
}

const Subscribe: NextPage<LoginData> = (props: any) => {

    useEffect(() => {
        if (props.imp_uid !== "" && props.merchant_uid !== "") {
            var merchant_uid_date = moment().format('YYYY-MM-DD_HH:mm:ss');
            axios.post('/api/subscribe/payments', {
                muser_id: loginData.userId, // 로그인 아이디
                radioItem: radioItem, // 구독 개월 수 라디오 버튼
                merchant_uid: "subscribe_" + merchant_uid_date,
                customer_uid: loginData.userCompanyCode + "_" + loginData.userId,
                name: "기본정기결제",
                amount: 500
            }).then(({ data }) => {
                console.log("data : ", data)
                if (data.success) {
                    // 성공할 경우
                    alert('구독신청이 완료되었습니다. 다시 로그인해주세요.')
                    router.push('/');
                }
                else {
                    // 실패할 경우
                    alert('내용을 다시확인해주세요.')
                }
            })
        }
    }, [props])
    
    ...

 

이제 모바일로 접속했을 때 빌링키를 받고 redirect 되고, 파라미터를 받아 useEffect 로 결제 신청을 하는 것까지 완료되었다.

아임포트 웹훅으로 정기결제 예약까지 완성되었다.

아임포트의 관리자 콘솔에서 정기결제예약 스케줄을 확인할 수 있다. 

 


참고로 m_redirect_url에 먼저 내가 원하는 파라미터를 넣어도 좋다. 페이지가 리렌더링 되기 때문에 기존의 상태값들은 초깃값으로 넘어가기 때문에 원하는 값도 같이 전달하고 싶으면 아래와 같은 방법으로 해주면 좋다.

m_redirect_url: 'abc.com/view/subscribe?subscribe_item=' + subscribeItem

 

728x90
반응형
728x90
반응형

1. 버튼과 버튼 클릭 이벤트 함수를 만들어준다.

    const unSubscribeClickHandler = (e: any) => {
        if (window.confirm('구독을 취소하시겠습니까?')) {
            axios.post('/api/subscribe/unsubscribe', {
                muser_id: loginData.userId
            }).then(({ data }) => {
                if (data.success) {
                    alert('구독취소가 완료되었습니다. 다시 로그인해주세요.')
                    router.push('/')
                }
                else {
                    alert('구독취소 오류입니다. 관리자에게 문의 바랍니다.')
                }
            })
        }
        else {

        }
    }

 

2. 백엔드에 구독을 취소하는 라우터를 만들어준다. 

router.post("/unsubscribe", async function (req, res, next) {
    try {
        Subscribe.findOne({
            where: {
                subscribe_muser_id: req.body.muser_id,
                subscribe_type: "기본정기결제예약"
            },
            order: [['subscribe_createdAt', 'DESC']],
        }).then(async (result) => {

            if (result) {
                // 인증 토큰 발급 받기
                const getToken = await axios({
                    url: "https://api.iamport.kr/users/getToken",
                    method: "post", // POST method
                    headers: { "Content-Type": "application/json" }, // "Content-Type": "application/json"
                    data: {
                        imp_key: process.env.IMP_API_KEY, // REST API 키
                        imp_secret: process.env.IMP_API_SECRET_KEY // REST API Secret
                    }
                });
                const { access_token } = getToken.data.response; // 인증 토큰

                // 정기결제 예약 취소
                const paymentResult = await axios({
                    url: 'https://api.iamport.kr/subscribe/payments/unschedule',
                    method: "post",
                    headers: { "Authorization": access_token }, // 인증 토큰을 Authorization header에 추가
                    data: {
                        customer_uid: result.subscribe_customer_uid,
                        merchant_uid: result.subscribe_merchant_uid, // 새로 생성한 결제(재결제)용 주문 번호
                    }
                });

                MUserInfo.update({
                    muser_subscribe_type: ""
                }, {
                    where: {
                        muser_id: result.subscribe_muser_id
                    }
                })

                res.clearCookie('')
                res.send({ success: true })
            }
            else {
                res.send({ success: false })
            }
        })
    }
    catch (Err) {
        console.log("Err : ", Err)
        res.send({ success: false })
    }
})

module.exports = router;
728x90
반응형
728x90
반응형

아임포트 웹훅-

https://docs.iamport.kr/tech/webhook

 

[가이드] Webhook

아임포트 Webhook 이 문서는 아임포트 webhook을 사용하여 아임포트 서버에 저장된 결제 정보를 가맹점 서버에 동기화하고 네트워크 불안정성을 보완하는 방법을 설명합니다. Webhook이란? Webhook(웹훅

docs.iamport.kr


아임포트에서 제공하는 웹훅으로 결제가 완료된 후 다음 결제를 예약할 수 있다.

데이터베이스에 저장된 구독 남은 개월 수를 측정해서 카운팅한 후

남은 개월 수가 있을 경우 결제완료 후 다음 달로 결제 예약을 거는 시스템을 구현해보도록 하자.

 

1. 아임포트 관리자 콘솔에서 웹훅에 대한 설정을 해준다.

웹훅 발송 공통 URL을 내가 백엔드에서 요청받을 라우터로 지정해주면 결제가 완료될 때 원하는 행동을 할 수 있다.

 

2. 백엔드를 만들고 위에 이미지의 호출 테스트를 눌러본다.

router.post("/repayments", async function (req, res, next) {
    const body = req.body
    console.log("body : ", body)
    try {
        res.send({ success: true })
    }
    catch (Err) {
        console.log("err : ", Err)
        res.send({ success: false })
    }
});

 

- 호출테스트 눌렀을 때 정상적인 경우

 

3. 라우터에 로직을 만든다. 

결제가 잘되거나 예약결제가 시도되었을 땐 paid, 예약결제가 실패했을 땐 failed 결과값이 온다.

 

  1. 구독 테이블을 조회해서 남은 개월 수를 측정한다.
  2. 남은 개월 수가 0보다 클 경우 토큰을 발급받고 imp_uid 또는 merchant_uid로 결제 정보를 조회하고 잘 되었는지 확인한다.
  3.  예약결제가 정상적으로 이루어진 상태면 카운팅을 해주고 merchant_uid를 새로운 예약결제에 맞게 수정하며 다음 달에 예약을 걸고 쿠키를 수정한다.( 로그인 정보에 담아줘서 나중에 화면에 달리보이게 하기 위함)
  4. 만약 잔액 부족 등으로 결제가 안 된 상태면 로그인정보를 수정해서 구독 시 사용 가능한 서비스를 불가능하게 하고 다음날을 예약일로 요청한다. (구독에 관한 데이터는 남아있음).
  5. 남은 개월 수가 0일 경우 예약을 걸지 않고 구독해지를 해준다 로그인 정보를 바꾸고 해당 컬럼을 삭제한다.
router.post("/repayments", async function (req, res, next) {
    const body = req.body;
    const { imp_uid, merchant_uid } = req.body;
    try {
        // 1. 구독 테이블을 조회해서 남은 개월 수를 측정한다.
        Subscribe.findOne({
            where: {
                subscribe_merchant_uid: merchant_uid
            }
        }).then(async (result) => {
            if (result) {
                // 2. 남은 개월 수가 0보다 클 경우 토큰을 발급받고 imp_uid 또는 merchant_uid로 결제 정보를 조회하고 잘 되었는지 확인한다.
                if (result.subscribe_count > 0) {
                    // 액세스 토큰(access token) 발급 받기
                    const getToken = await axios({
                        url: "https://api.iamport.kr/users/getToken",
                        method: "post", // POST method
                        headers: { "Content-Type": "application/json" }, // "Content-Type": "application/json"
                        data: {
                            imp_key: process.env.IMP_API_KEY, // REST API 키
                            imp_secret: process.env.IMP_API_SECRET_KEY // REST API Secret
                        }
                    });
                    const { access_token } = getToken.data.response; // 인증 토큰

                    // imp_uid 또는 merchant_uid로 아임포트 서버에서 결제 정보 조회
                    const paymentResult = await axios({
                        url: 'https://api.iamport.kr/payments/' + imp_uid,
                        method: "get",
                        headers: { "Authorization": access_token }, // 인증 토큰을 Authorization header에 추가
                        data: {
                            imp_uid: imp_uid
                        }
                    });

                    console.log("repayments result : ", paymentResult)

                    var merchant_uid_date = moment().format('YYYY-MM-DD_HH:mm:ss');

                    if (paymentResult.data.response.status === 'paid') {
                        // 3. 예약결제가 정상적으로 이루어진 상태면 카운팅을 해주고 merchant_uid를 새로운 예약결제에 맞게 수정하며 다음 달에 예약을 걸고 로그인정보를 수정한다.( 로그인 정보에 담아줘서 나중에 화면에 달리보이게 하기 위함) 
                        // 카운팅, merchant_uid 수정

                        Subscribe.update(
                            {
                                subscribe_count: result.subscribe_count - 1,
                                subscribe_merchant_uid: "subscribe_" + merchant_uid_date,
                                subscribe_type: "기본정기결제예약"
                            }, {
                            where: {
                                subscribe_merchant_uid: merchant_uid
                            }
                        }).then(async (updateResult) => {
                            // 로그인 정보를 수정해줌 ( 잔액부족 등으로 결제 실패 후 다시 결제 했을 때 서비스 이용 가능하게 함)
                            MUserInfo.update({
                                muser_subscribe_type: result.subscribe_type
                            }, {
                                where: {
                                    muser_id: result.subscribe_muser_id
                                }
                            })

                            // 새로운 결제 예약
                            var today = new Date();
                            var schedule_at_time = new Date(today.getFullYear(), today.getMonth() + 1, today.getDate());
                            await axios({
                                url: "https://api.iamport.kr/subscribe/payments/schedule", // 예: https://api.iamport.kr/subscribe/payments/schedule
                                method: "post",
                                headers: { "Authorization": access_token }, // 인증 토큰 Authorization header에 추가
                                data: {
                                    customer_uid: result.subscribe_customer_uid, // 카드(빌링키)와 1:1로 대응하는 값
                                    schedules: [
                                        {
                                            merchant_uid: "subscribe_" + merchant_uid_date, // 주문 번호
                                            schedule_at: Math.floor(schedule_at_time.getTime() / 1000), // 결제 시도 시각 in Unix Time Stamp. 예: 다음 달 1일
                                            amount: result.subscribe_amount,
                                            name: "기본정기결제예약",
                                        }
                                    ]
                                }
                            });
                        })
                    }
                    else {
                        // 4. 만약 잔액 부족 등으로 결제가 안 된 상태면 로그인정보를 수정해서 구독 시 사용 가능한 서비스를 불가능하게 하고 다음날을 예약일로 요청한다. (구독에 관한 데이터는 남아있음).
                        Subscribe.update(
                            {
                                subscribe_merchant_uid: "subscribe_" + merchant_uid_date,
                                subscribe_type: "기본정기결제예약"
                            }, {
                            where: {
                                subscribe_merchant_uid: merchant_uid
                            }
                        }).then((updateResult) => {
                            // 로그인 정보를 수정해줌 ( 구독 서비스를 이용하지 못하게 함. )
                            MUserInfo.update({
                                muser_subscribe_type: ""
                            }, {
                                where: {
                                    muser_id: result.subscribe_muser_id
                                }
                            })

                            // 새로운 결제 예약
                            var today = new Date();
                            var schedule_at_time = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
                            axios({
                                url: "https://api.iamport.kr/subscribe/payments/schedule", // 예: https://api.iamport.kr/subscribe/payments/schedule
                                method: "post",
                                headers: { "Authorization": access_token }, // 인증 토큰 Authorization header에 추가
                                data: {
                                    customer_uid: result.subscribe_customer_uid, // 카드(빌링키)와 1:1로 대응하는 값
                                    schedules: [
                                        {
                                            merchant_uid: "subscribe_" + merchant_uid_date, // 주문 번호
                                            schedule_at: Math.floor(schedule_at_time.getTime() / 1000), // 결제 시도 시각 in Unix Time Stamp. 예: 다음 달 1일
                                            amount: result.subscribe_amount,
                                            name: "기본정기결제예약",
                                        }
                                    ]
                                }
                            });
                        })
                    }
                }
                // 5. 남은 개월 수가 0일 경우 예약을 걸지 않고 구독해지를 해준다 로그인 정보를 바꾸고 해당 컬럼을 삭제한다.
                else {
                    MUserInfo.update({
                        muser_subscribe_type: ""
                    }, {
                        where: {
                            muser_id: result.subscribe_muser_id
                        }
                    })
                }
            }
            else {
            }
        })

        res.send({ success: true })
    }
    catch (Err) {
        console.log("err : ", Err)
        res.send({ success: false })
    }
});
 
728x90
반응형

+ Recent posts