728x90
반응형

1. jest 모듈을 설치한다.

$ npm i jest @types/jest ts-jest

 

2. jest 설정파일을 만든다.

/jest.config.ts

module.exports = {
  "testMatch": [
    "**/__tests__/**/*.+(ts|tsx|js)",
    "**/?(*.)+(spec|test).+(ts|tsx|js)"
  ],
  "transform": {
    "^.+\\.(ts|tsx)$": "ts-jest"
  },
}

 

3. pakage.json 에 테스트 script를 수정해준다.

  "scripts": {
    "test": "jest",
    "start": "node dist/app.js",
    "build": "tsc -p .",
    "dev": "nodemon --watch \"src/**/*.ts\" --exec \"ts-node\" src/app.ts"
  },

 

4. test 파일을 만들고 아래처럼 테스트 파일을 만들어준다.

 

/tests/routers/one.test.ts

// dotenv
import dotenv from 'dotenv'
dotenv.config();

// modules
import { By, until, WebDriver } from 'selenium-webdriver';

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

// node-json-db
import { db } from '../../config/nodejsondb';

import { 
    getDriverHandler,

    alertCloseAccept,
    alertCloseDismiss,
    promptCloseHandler,

    addCookie,
    getOneCookie,
    getAllCookie,
    deleteOneCookie,
    deleteAllCookie,

    fileRegister,
    
    findElementById,
    findElementByName,
    findElementByXpath,
    findElementByCss,

    JqChangeValueByID,
    JqRemoveAttribute,

    naviGet,
    naviBack,
    naviForward,
    naviRefresh,

    popupClose

 } from '../../modules';

 describe('GET /one', () => {
    // 웹드라이버 설정
    let driver : WebDriver;

    beforeAll(async () => {
        driver = await getDriverHandler();
    })

    afterAll(async () => {
        await driver.quit()
    })

    test('url 잘 뜨는지 확인', async () => {
        try {
            // 브라우저에 접속
            await driver.get('https://typo.tistory.com/');

            // 현재 주소 가져오기
            const text = await driver.getCurrentUrl();

            // test
            expect(text).toEqual('https://typo.tistory.com/')
        }
        catch(Err) {
            console.log(Err)
            logger.debug(Err)
            throw Error;
        }
    })

 })

 

 

/tests/middlewares/http.test.ts

import express, { Request, Response, NextFunction } from 'express';
import {   
    loggerInfo,
    loggerError,
    loggerHttp,
    loggerDebug,
 } from '../../config/winston'
import { httpLoggingMiddleware } from '../../middlewares';

describe('MiddleWare http', () => {
    let mockRequest : Partial<Request>;
    let mockResponse : Partial<Response>;
    let nextFunction: NextFunction = jest.fn();

    beforeEach(()=> {
        mockRequest = {};
        mockResponse = {
            json: jest.fn()
        };
    });

    test('http 로깅 미들웨어 테스트', async () => {
        try {
            await httpLoggingMiddleware(mockRequest as Request, mockResponse as Response, nextFunction);
            
            // nextFunction 까지 잘되는지 확인
            expect(nextFunction).toBeCalledTimes(1);
        }
        catch (Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

})

reqest, response, next 를 mock으로 만들어 테스트해준다.

 

/tests/middlewares/timeInterval.ts

import express, { Request, Response, NextFunction } from 'express';
import {   
    loggerInfo,
    loggerError,
    loggerHttp,
    loggerDebug,
 } from '../../config/winston'

import { timeIntervalMiddleware } from '../../middlewares';

describe('MiddleWare timeInterval', () => {
    let mockRequest : Partial<Request>;
    let mockResponse : Partial<Response>;
    let nextFunction: NextFunction = jest.fn();

    beforeEach(()=> {
        mockRequest = { originalUrl : '/one' };
        mockResponse = {
            json: jest.fn()
        };
    });

    test('timeInterval 로깅 미들웨어 테스트', async () => {

        try {
            await timeIntervalMiddleware(mockRequest as Request, mockResponse as Response, nextFunction);

            // nextFunction 까지 잘되는지 확인
            expect(nextFunction).toBeCalledTimes(1);
        }
        catch (Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

})

 

/tests/modules/db/expireHandler.ts

import express, { Request, Response, NextFunction } from 'express';

// node-json-db
import { db } from '../../../config/nodejsondb';

// logger
import { loggerDebug } from '../../../config/winston';
import { expireHandler } from '../../../modules';

describe('Module expireHandler', () => {
    let dbPath : string;

    beforeEach(()=> {
        dbPath = '/one'
    });

    test('30분 지난 데이터 삭제 및 확인', async () => {
        try {
            let nowTime = new Date(); // 현재 시간

            // 30분 지난 데이터 삭제
            await expireHandler(dbPath);

            // 데이터 베이스 안 데이터들
            const datalist = await db.getData(dbPath);

            let index = 0; // 기준점이 되는 index

            // 데이터 매핑 및 시간 차이 계산해서 30분 지난 데이터는 삭제
            await datalist.map(async (item : any, index2: number) => {
                let beforeTime =  new Date(item.date); // 데이터들 저장된 시간
                let diff = (nowTime.getTime() - beforeTime.getTime()) / (1000*60); // 데이터들 시간 차이
                // 저장된 지 30분이 지나면 index 체크
                if(diff > 30) {
                    index = index2;
                }
            })

            // ----- 30분 지난 데이터 없는지 확인 -----
            expect(index).toEqual(0);

        }
        catch(Err) {
            console.log("Err : ",Err)
            loggerDebug.info(JSON.stringify(Err))
        }
    })
})

 

/tests/modules/selenium/alertHandler.test.ts

// dotenv
import dotenv from 'dotenv'
dotenv.config();

// modules
import { By, until, WebDriver } from 'selenium-webdriver';

// logger
import {   
    loggerDebug,
} from '../../../config/winston';

import { 
    getDriverHandler,
    findElementById,

} from '../../../modules';

 // window type 선언
declare const window: typeof globalThis;

 describe('Module alertHandler', () => {
    // 웹드라이버 설정
    let driver : WebDriver;

    beforeEach(async () => {
        driver = await getDriverHandler();
        await driver.get('https://testpages.herokuapp.com/styled/alerts/alert-test.html')
    })

    afterEach(async () => {
        await driver.quit()
    })

    test('alert 테스트 ( alert 텍스트가 잘 나오는지 )', async () => {
        try {
            await (await findElementById(driver,'alertexamples')).click();

            // alert 창 뜰 때까지 기다림
            await driver.wait(until.alertIsPresent());

            // alert 로 드라이버 이동
            let alert = await driver.switchTo().alert();

            // alertText
            let alertText = await alert.getText();

            // 확인버튼 클릭
            await alert.accept();

            // 다시 원래 컨텐츠로 드라이버 이동
            await driver.switchTo().defaultContent();

            expect(alertText).toEqual('I am an alert box!');
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

    test('prompt 테스트', async () => {
        try {
            await (await findElementById(driver,'promptexample')).click();

            // alert 창 뜰 때까지 기다림
            await driver.wait(until.alertIsPresent());

            // alert 로 드라이버 이동
            let alert = await driver.switchTo().alert();

            // alert text 사용하는 곳
            let alertText = await alert.getText();

            // 확인버튼 클릭
            await alert.accept();

            // 다시 원래 컨텐츠로 드라이버 이동
            await driver.switchTo().defaultContent();

            expect(alertText).toEqual('I prompt you');

        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })
})

 

/tests/modules/selenium/cookieHandler.test.ts

// dotenv
import dotenv from 'dotenv'
dotenv.config();

// modules
import { By, until, WebDriver } from 'selenium-webdriver';

// logger
import {   
    loggerDebug,
} from '../../../config/winston';

import { 
    getDriverHandler,

    addCookie,
    getOneCookie,
    getAllCookie,
    deleteOneCookie,

    findElementById,
    findElementByName,

 } from '../../../modules';
  

 describe('Module cookieHandler', () => {
    // 웹드라이버 설정
    let driver : WebDriver;

    beforeEach(async () => {
        driver = await getDriverHandler();
        await driver.get('https://testpages.herokuapp.com/styled/cookies/adminlogin.html');

        // 로그인 및 쿠키생성
        await (await findElementByName(driver,'username')).sendKeys('Admin');
        await (await findElementByName(driver,'password')).sendKeys('AdminPass');
        await (await findElementById(driver,'login')).click();
    },15000)

    afterEach(async () => {
        await driver.quit()
    })

    test('쿠키 추가하기', async () => {
        try {
            // 쿠키추가
            await addCookie(driver,{name : "name", value : "teepo"});

            // 쿠키 가져오기
            const cookies = (await driver.manage().getCookie('name')).value;

            // 쿠키 확인
            expect(cookies).toEqual('teepo');
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

    test('쿠키 하나 가져오기', async () => {
        try {
            // 쿠키 가져오기
            const result = await getOneCookie(driver,'loggedin');

            // 쿠키 확인
            expect(result.value).toEqual('Admin');
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

    test('쿠키 전부 가져오기', async () => {
        try {
            // 쿠키 가져오기
            const result = await getAllCookie(driver);

            // 쿠키 확인
            expect(result[0].value).toEqual('Admin');
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

    test('쿠키 하나 지우기', async () => {
        try {
            // 쿠키 삭제하기
            await deleteOneCookie(driver,'loggedin');

            // 쿠키 확인
            const result = await getAllCookie(driver);

            // 쿠키가 없어졌는지 확인
            expect(result.length).toEqual(0);
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

    test('쿠키 전부 지우기', async () => {
        try {
            // 쿠키 삭제하기
            await deleteOneCookie(driver,'loggedin');

            // 쿠키 확인
            const result = await getAllCookie(driver);

            // 쿠키가 없어졌는지 확인
            expect(result.length).toEqual(0);
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }

    })

})

 

/tests/modules/selenium/driverHandler.test.ts

// dotenv
import dotenv from 'dotenv'
dotenv.config();

// modules
import { By, until, WebDriver } from 'selenium-webdriver';

// logger
import {   
    loggerDebug,
 } from '../../../config/winston';

import { 
    getDriverHandler,
 } from '../../../modules';
  

 describe('Module driverHandler', () => {
    // 웹드라이버 설정
    let driver : WebDriver;

    beforeEach(async () => {
        
    })

    afterEach(async () => {
        await driver.quit()
    })

    test('webdriver가 잘 반환되는지 테스트', async () => {
        try {
            driver = await getDriverHandler();
            expect(driver).toBeInstanceOf(WebDriver);
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

})

 

/tests/modules/selenium/fileHandler.test.ts

// dotenv
import dotenv from 'dotenv'
dotenv.config();

// modules
import { By, until, WebDriver } from 'selenium-webdriver';

// logger
import {   
    loggerDebug,
} from '../../../config/winston';

import { 
    fileRegister,

    findElementById,
    findElementByName,

    getDriverHandler,

 } from '../../../modules';
  

 describe('Module fileHandler', () => {
    // 웹드라이버 설정
    let driver : WebDriver;

    beforeEach(async () => {
        driver = await getDriverHandler();
        await driver.get('https://testpages.herokuapp.com/styled/file-upload-test.html')
    })

    afterEach(async () => {
        await driver.quit()
    })

     test('fileRegister', async () => {
         try {
             await fileRegister(driver,By.id('fileinput'),['파일 위치'])
             await (await findElementByName(driver,'fileinput')).click()
             var text = await (await findElementById(driver,'uploadedfilename')).getText()
             expect(text).toEqual('image.jpg')
         }
         catch(Err) {
             loggerDebug.info(JSON.stringify(Err))
         }
     })

})

 

/tests/modules/selenium/findHandler.test.ts

// dotenv
import dotenv from 'dotenv'
dotenv.config();

// modules
import { By, until, WebDriver } from 'selenium-webdriver';

// logger
import {   
    loggerDebug,
 } from '../../../config/winston';

import { 
    getDriverHandler,

    findElementById,
    findElementsById,
    findElementByName,
    findElementByXpath,

    findElementByClass,
    findElementsByName,
    findElementsByXpath,
    findElementsByClass

 } from '../../../modules';
  

 describe('Module findElementHandler', () => {
    // 웹드라이버 설정
    let driver : WebDriver;

    beforeAll(async () => {
        driver = await getDriverHandler();
        driver.get('https://testpages.herokuapp.com/styled/find-by-playground-test.html')
    })

    afterAll(async () => {
        await driver.quit()
    })

    test('findElementById', async () => {
        try {
            let text = await (await findElementById(driver,'p1')).getText();
            expect(text).toEqual('This is a paragraph text')
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    });

    test('findElementByName', async () => {
        try {
            let text = await (await findElementByName(driver,'pName1')).getText();
            expect(text).toEqual('This is a paragraph text')
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    });

    test('findElementByXpath', async () => {
        try {
            let text = await (await findElementByXpath(driver,'//*[@id="p1"]')).getText();
            expect(text).toEqual('This is a paragraph text')
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    });

    test('findElementByClass', async () => {
        try {
            let text = await (await findElementByClass(driver,'explanation')).getText();
            expect(text).toEqual('This is a set of nested elements. There are various ways to locate each of the elements. Challenge yourself to find as many ways of locating elements as possible.')
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    });

})


describe('Module findElementsHandler', () => {
    // 웹드라이버 설정
    let driver : WebDriver;

    beforeAll(async () => {
        driver = await getDriverHandler();
        await driver.get('https://testpages.herokuapp.com/styled/find-by-playground-test.html')
    })

    afterAll(async () => {
        await driver.close()
    })

    test('findElementsById', async () => {
        try {
            let text = await (await findElementsById(driver,'p1'))[0].getText();
            expect(text).toEqual('This is a paragraph text')
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    });

    test('findElementsByName', async () => {
        try {
            let text = await (await findElementsByName(driver,'pName1'))[0].getText();
            expect(text).toEqual('This is a paragraph text')
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    });

    test('findElementsByXpath', async () => {
        try {
            let text = await (await findElementsByXpath(driver,'//*[@id="p1"]'))[0].getText();
            expect(text).toEqual('This is a paragraph text')
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    });

    test('findElementsByClass', async () => {
        try {
            let text = await (await findElementsByClass(driver,'explanation'))[0].getText();
            expect(text).toEqual('This is a set of nested elements. There are various ways to locate each of the elements. Challenge yourself to find as many ways of locating elements as possible.');
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
        });
})

 

/tests/modules/selenium/jqueryHandler.test.ts

// dotenv
import dotenv from 'dotenv'
dotenv.config();

// modules
import { By, until, WebDriver } from 'selenium-webdriver';

// logger
import {   
    loggerDebug,
 } from '../../../config/winston';

// node-json-db
// import { db } from '../../config/nodejsondb';

import { 
    getDriverHandler,

    findElementById,

    JqChangeValueByID,
    JqSetAttribute,
    JqRemoveAttribute,
 } from '../../../modules';
  

 describe('Module jqueryHandler', () => {
    // 웹드라이버 설정
    let driver : WebDriver;

    beforeEach(async () => {
        driver = await getDriverHandler();
        await driver.get('https://testpages.herokuapp.com/styled/html5-form-test.html')
    })

    afterEach(async () => {
        await driver.quit()
    })

    test('JqChangeValueByID', async () => {
        try {
            let value = "teepo";
            await (await findElementById(driver,'email-field')).clear()
            await JqChangeValueByID(driver,'email-field',value);
            let text = await (await findElementById(driver,'email-field')).getAttribute('value');
            expect(text).toEqual(value)
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

    test('JqSetAttribute', async () => {
        try {
            await JqSetAttribute(driver,'email-field',['disabled','true']);
            let bool = await (await findElementById(driver,'email-field')).getAttribute('disabled');
            expect(bool).toBe("true")
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

    test('JqRemoveAttribute', async () => {
        try {
            await JqSetAttribute(driver,'email-field',['disabled','true']);
            await JqRemoveAttribute(driver,'email-field','disabled');
            let bool = await (await findElementById(driver,'email-field')).getAttribute('disabled');
            expect(bool).toBeNull()
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

})

 

/tests/modules/selenium/navigationHandler.test.ts

// dotenv
import dotenv from 'dotenv'
dotenv.config();

// modules
import { By, until, WebDriver } from 'selenium-webdriver';

// logger
import {   
    loggerDebug,
 } from '../../../config/winston';

// node-json-db
// import { db } from '../../config/nodejsondb';

import { 
    getDriverHandler,

    findElementById,

    naviGet,
    naviBack,
    naviForward,
    naviRefresh,

 } from '../../../modules';
  

 describe('Module navigation', () => {
    // 웹드라이버 설정
    let driver : WebDriver;
    let url = 'https://testpages.herokuapp.com/styled/index.html';

    beforeEach(async () => {
        driver = await getDriverHandler();
        await naviGet(driver,url)
    })

    afterEach(async () => {
        await driver.quit()
    })

    test('naviGet', async () => {
        try {
            let currentUrl = await driver.getCurrentUrl();
            expect(currentUrl).toEqual(url);
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

    test('naviBack', async () => {
        try {
            await (await findElementById(driver,'basicpagetest')).click();
            await naviBack(driver);
            let currentUrl = await driver.getCurrentUrl();
            expect(currentUrl).toEqual(url);
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

    test('naviForward', async () => {
        try {
            await (await findElementById(driver,'basicpagetest')).click();
            await naviBack(driver);
            await naviForward(driver);
            let currentUrl = await driver.getCurrentUrl();
            expect(currentUrl).toEqual('https://testpages.herokuapp.com/styled/basic-web-page-test.html');
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }

    })

    test('naviRefresh', async () => {
        try {
            await naviRefresh(driver);
            let currentUrl = await driver.getCurrentUrl();
            expect(currentUrl).toEqual(url);
        }
        catch(Err) {
            loggerDebug.info(JSON.stringify(Err))
        }

    })

})

 

/tests/modules/selenium/popupHandler.test.ts

// dotenv
import dotenv from 'dotenv'
dotenv.config();

// modules
import { By, until, WebDriver } from 'selenium-webdriver';

// logger
import {   
    loggerDebug,
 } from '../../../config/winston';

import { 
    getDriverHandler,

    findElementById,

    naviGet,

    popupClose

 } from '../../../modules';
  

 describe('Module popupHandler', () => {
    // 웹드라이버 설정
    let driver : WebDriver;

    beforeEach(async () => {
        driver = await getDriverHandler();
        await naviGet(driver,'https://testpages.herokuapp.com/styled/alerts/alert-test.html');
    })

    afterEach(async () => {
        await driver.quit()
    })

    test('popupClose', async () => {
        try {
            await (await findElementById(driver,'promptexample')).click();
            popupClose(driver);
            let windowNmber = await (await driver.getAllWindowHandles()).length;
            expect(windowNmber).toEqual(1);
        }
        catch (Err) {
            loggerDebug.info(JSON.stringify(Err))
        }
    })

})

 

5. 빌드하고 테스트해본다.

$ npm run build
$ npm run test

728x90
반응형
728x90
반응형

재사용성이 많은 함수들은 모듈로 만들어두면 매우 편하다. 예를 들면 아래처럼 driver 를 선언하는 함수는 각 라우터마다 동일하게 필요하다.

        // 웹드라이버 설정
        let driver = await new webdriver.Builder()
        .forBrowser('chrome')
        .setChromeOptions(new chrome.Options().windowSize({width: 1920, height: 1080}))
        .setChromeService(new chrome.ServiceBuilder(process.env.CHROMEDRIVER_PATH))
        .build();

 

또한 driver 메소드들을 우리 입맛에 맞게끔 만들어 두면 개발할 때도 훨씬 수월할 것이다.

 

참고로 selenium javacscript의 메소드에 관한 내용들은 아래 사이트에서 자세히 확인할 수 있다.

 

Index

 

www.selenium.dev

 

 

The Selenium Browser Automation Project

Selenium automates browsers. That's it!

www.selenium.dev

1. modules 밑에 모듈을 내보내는 파일을 만든다.

 

/mdules/selenium/driverHandler.ts

import webdriver, { WebDriver } from 'selenium-webdriver';
import chrome from 'selenium-webdriver/chrome';
import firefox from 'selenium-webdriver/firefox'

// dotenv
import dotenv from 'dotenv'
dotenv.config();

// new driver 반환 함수
export const getDriverHandler = async () : Promise<WebDriver> => {
    const driver =  await new webdriver.Builder()
        .forBrowser('chrome')
        .setChromeOptions(new chrome.Options().windowSize({width: 1920, height: 1080}))
        .setChromeService(new chrome.ServiceBuilder(process.env.CHROMEDRIVER_PATH))
        .build();

    return driver;
}

 

/modules/index.ts

export * from "./db/expireHandler"

export * from "./selenium/driverHandler"

 

2. 라우터에서 모듈을 가져와 쓴다.

...

// modules
import { 
    getDriverHandler,
 } from '../modules';
 
 ...
 
// 웹드라이버 설정
let driver = await getDriverHandler();

 

이런식으로 여러개의 모듈을 만들 수 있다.

 

/modules/selenium/alertHandler.ts

import { until, WebDriver } from "selenium-webdriver";

// alert 확인 누르기
export const alertCloseAccept = async (driver : WebDriver ) => {

    // alert 창 뜰 때까지 기다림
    await driver.wait(until.alertIsPresent());

    // alert 로 드라이버 이동
    let alert = await driver.switchTo().alert();

    // alert text 사용하는 곳
    let alertText = await alert.getText();

    // 확인버튼 클릭
    await alert.accept();

    // 다시 원래 컨텐츠로 드라이버 이동
    await driver.switchTo().defaultContent();
}

// alert 닫기 누르기
export const alertCloseDismiss = async (driver : WebDriver ) => {

    // alert 창 뜰 때까지 기다림
    await driver.wait(until.alertIsPresent());

    // alert 로 드라이버 이동
    let alert = await driver.switchTo().alert();

    // alert text 사용하는 곳
    let alertText = await alert.getText();

    // 확인버튼 클릭
    await alert.dismiss();

    // 다시 원래 컨텐츠로 드라이버 이동
    await driver.switchTo().defaultContent();
}

// prompt 핸들러
export const promptCloseHandler = async (driver : WebDriver, text : string) => {

    // alert 창 뜰 때까지 기다림
    await driver.wait(until.alertIsPresent());

    // alert 로 드라이버 이동
    let alert = await driver.switchTo().alert();
    
    // prompt text 전송
    await alert.sendKeys(text)
    
    // 확인버튼 클릭
    await alert.accept();
    
    // 다시 원래 컨텐츠로 드라이버 이동
    await driver.switchTo().defaultContent(); 
}

 

/modules/selenium/navigationHandler.ts

import { WebDriver } from "selenium-webdriver";

export const naviGet = async ( driver : WebDriver, url : string) => {
    await driver.get(url);
}

export const naviBack = async (driver : WebDriver) => {
    await driver.navigate().back();
}

export const naviForward = async (driver : WebDriver) => {
    await driver.navigate().forward();
}

export const naviRefresh = async (driver : WebDriver) => {
    await driver.navigate().refresh();
}

 

/modules/selenium/cookieHandler.ts

import {  IWebDriverCookie, WebDriver } from "selenium-webdriver";

// 쿠키 추가하기
export const addCookie = async (driver : WebDriver, cookie : IWebDriverCookie ) => {
    await driver.manage().addCookie(cookie)
}

// 쿠키 하나 가져오기
export const getOneCookie = async (driver : WebDriver, key : string) : Promise<IWebDriverCookie> => {
    return await driver.manage().getCookie(key);
}

// 쿠키 전부 가져오기 
export const getAllCookie = async (driver : WebDriver) : Promise<IWebDriverCookie[]> => {
    return await driver.manage().getCookies();
}

// 쿠키 하나 지우기
export const deleteOneCookie = async (driver : WebDriver,  key : string) => {
    return await driver.manage().deleteCookie(key);
}

// 쿠키 전부 지우기
export const deleteAllCookie = async (driver : WebDriver) => {
    return await driver.manage().deleteAllCookies();
}

 

/modules/selenium/popupHandler.ts

import { WebDriver } from "selenium-webdriver";

export const popupClose = async (driver : WebDriver ) => {

    // window handles 0 빼고 모든 윈도우 닫음
    (await driver.getAllWindowHandles()).map(async (item : string, index: number)=> {
        if(index > 0) {
            await driver.switchTo().window((await driver.getAllWindowHandles())[index]);
            driver.close();
        }
    })

    // 메인 윈도우로 이동
    await driver.switchTo().window((await driver.getAllWindowHandles())[0]);

}

 

/modules/selenium/jqueryHandler.ts

import { By, WebDriver } from "selenium-webdriver";

// 해당 document의 value값을 바꿔준다.
export const JqChangeValueByID = async (driver : WebDriver, id : string, value: string ) => {
    await driver.executeScript(`document.getElementById('${id}').value='${value}';`)
}

export const JqSetAttribute = async (driver : WebDriver, id : string, attribute: string[] ) => {
    await driver.executeScript(`document.getElementById('${id}').setAttribute('${attribute[0]}','${attribute[1]}')`)
}

export const JqRemoveAttribute = async (driver : WebDriver, id : string, attribute: string ) => {
    await driver.executeScript(`document.getElementById('${id}').removeAttribute("${attribute}")`)
}

 

/modules/selenium/fileHandler.ts

import { Locator, until, WebDriver } from "selenium-webdriver";

// 경로에 맞는 파일 등록 ( 인증서 등 )
export const fileRegister = async (driver : WebDriver, func : Locator, path: string[] ) => { 
    let pathString : string = "";
    
    // 받은 배열을 줄바꿈을 포함한 문자열로 바꿔준다.
    path.map((item : string, index: number) => {
        if(index < path.length - 1) {
            pathString = pathString + item + " \n "
        }
        else {
            pathString = pathString + item
        }
    })
    
    const el = await driver.wait(until.elementLocated(func), 2000);
    return await driver.wait(until.elementIsVisible(el), 2000).sendKeys(pathString);

 

/modules/selenium/findHandler.ts

import { By, until, WebDriver } from "selenium-webdriver";

export const findElementById = async (driver: WebDriver, id: string, timeout = 2000) => {
    return await driver.wait(until.elementLocated(By.id(id)), timeout);
};
  
export const findElementByName = async (driver: WebDriver, name: string, timeout = 2000) => {
  return await driver.wait(until.elementLocated(By.name(name)), timeout);
};

export const findElementByCss = async (driver: WebDriver, css: string, timeout = 2000) => {
  return await driver.wait(until.elementLocated(By.css(css)), timeout);
};
  
export const findElementByXpath = async (driver: WebDriver, xpath: string, timeout = 2000) => {
  return await driver.wait(until.elementLocated(By.xpath(xpath)), timeout);
};

export const findElementByClass = async (driver: WebDriver, classname: string, timeout = 2000) => {
  return await driver.wait(until.elementLocated(By.className(classname)), timeout);
};

export const findElementByTagName = async (driver: WebDriver, tag: string, timeout = 2000) => {
  return await driver.wait(until.elementLocated(By.tagName(tag)), timeout);
};

export const findElementsById = async (driver: WebDriver, id: string) => {
  return  await driver.findElements(By.id(id));
};

export const findElementsByName = async (driver: WebDriver, name: string) => {
  return  await driver.findElements(By.name(name));
};

export const findElementsByCss = async (driver: WebDriver, css: string) => {
  return  await driver.findElements(By.css(css));
};

export const findElementsByXpath = async (driver: WebDriver, xpath: string) => {
  return  await driver.findElements(By.xpath(xpath));
};

export const findElementsByClass= async (driver: WebDriver, classname: string) => {
  return  await driver.findElements(By.className(classname));
};

export const findElementsByTagName= async (driver: WebDriver, tagName: string) => {
  return  await driver.findElements(By.tagName(tagName));
};

 

/modules/selenium/sleepHandler.ts

export const sleep = (ms : number) => {
    return new Promise((resolve) => setTimeout(resolve, ms));
}

 

 

/modules/index.ts

export * from "./db/expireHandler"

export * from "./selenium/alertHandler"
export * from "./selenium/cookieHandler"
export * from "./selenium/driverHandler"
export * from "./selenium/fileHandler"
export * from "./selenium/jqueryHandler"
export * from "./selenium/navigationHandler"
export * from "./selenium/popupHandler"
export * from "./selenium/findHandler"
export * from "./selenium/sleepHandler"
728x90
반응형
728x90
반응형

1. node-json-db 와 shortid 를 설치해준다. 

$ npm i node-json-db shortid @types/shortid --save

 

2. config 폴더 아래 db 설정을 해준다.

/config/nodejsondb.ts

import { JsonDB } from 'node-json-db';
import { Config } from 'node-json-db/dist/lib/JsonDBConfig'

const db = new JsonDB(new Config("myDataBase", true, false, '/'));

db.push('/one/lastdate', "");
db.push('/one/data',[]);

export {db}

 

3. 라우터에 결과값을 저장하는 메소드를 사용해준다.

추가로 검색한 데이터는 로그로 남겨둔다.

/routers/one.ts

import express, { Request, Response, NextFunction } from 'express';
var router = express.Router();

// middlewares
import { httpLoggingMiddleware } from '../middlewares';

// logger
import {
    loggerHttp,
    loggerDebug,
    loggerError,
    loggerInfo 
    } from '../config/winston';

// selenium
import webdriver from 'selenium-webdriver';
import chrome from 'selenium-webdriver/chrome';

// node-json-db
import { db } from '../config/nodejsondb';

router.get('/',httpLoggingMiddleware, async function (req: Request, res: Response, next: NextFunction) {
    // 웹드라이버 설정
    let driver = await new webdriver.Builder()
    .forBrowser('chrome')
    .setChromeOptions(new chrome.Options().windowSize({width: 1920, height: 1080}))
    .setChromeService(new chrome.ServiceBuilder(process.env.CHROMEDRIVER_PATH))
    .build()
    try {

        // 브라우저에 접속
        await driver.get('https://typo.tistory.com/')

        // 현재 주소 가져오기
        const text = await driver.getCurrentUrl();

        // 저장 데이터
        const savedata = {
            id : shortid.generate(),
            date : new Date,
            result : text
        }

        // 데이터베이스에 저장 
        db.push('/one[]', savedata);

        // log 저장
        loggerInfo.info(JSON.stringify(savedata))

        res.send({success: true, result : text});
    }
    catch(Err) {
        loggerError.info(JSON.stringify(Err))
        console.log(Err)
        driver.close();
        res.send({success: false, result : Err})
    }
});

export default router;

 

여기서 중요한 점은 db에서 접근하는 경로 "/one" 와 라우터의 경로인 "/one"( '/'제외 )의 문자열를 일치하게 만들었다는 점이다.

이 방법으로 우린 미들웨어에서 라우터의 경로에 맞게 db에 손쉽게 접근할 수 있다. ( request.originalUrl 사용 )

아직은 이해가 안 될 수 있지만 차근차근 알게 될 것이다.

 

보통 크롤링 api 서버를 열어두고 제한을 안두면 무분별하게 라우터에 접속해서 userAgent 또는 접속 IP가 막힐 수도 있다.

때문에 userAgent를 바꿔주거나 IP를 우회하는 방법도 있지만 살짝 불법 느낌이 있어서 여기서는 전에 접속했던 시간을 비교해서 5분 간격으로 크롤링이 가능하게끔 만들어보겠다.

 

방법은 미들웨어에서 url path와 db path를 비교해서 전에 크롤링했던 데이터의 시간을 보고, 5분이 넘었는지 안넘었는지 확인 후

만약 5분이 안지난 상태면 res.send 메소드를 사용해 실패 메세지를 전송하도록 하겠다.

 

4. timeInterval 미들웨어를 만들어준다.

/middlewares/timeInterval.ts

import express, { Request, Response, NextFunction } from 'express';

const {   
    loggerInfo,
    loggerError,
    loggerHttp,
    loggerDebug,
 } = require('../config/winston');

// node-json-db
import { db } from '../config/nodejsondb';

// module
import { expireHandler } from '../modules';

export const timeIntervalMiddleware = async (req: Request, res: Response, next: NextFunction) => {
    try {
        // url
        let url = req.originalUrl;

        // 30분이 지난 데이터는 지워줌
        expireHandler(url);

        // 전체 데이터베이스 조회
        const data = await db.getData('/');

        // 처음 크롤링 할 때는 key값 ( 데이터베이스 주소 )이 없는 상태이므로 패스
        // 라우터 경로에서 '/'를 뺀 값이랑 데이터베이스의 키 값이랑 같다.
        if(!Object.keys(data).includes(url.split('/')[1])  || !(await db.getData(url + "[-1]"))) {
            next();
        }
        // 데이터가 존재할 경우 시간을 현 시간과 비교함
        else {
        	expireHandler(url);
        
            // 현재 시간
            let nowTime = new Date()
            let beforeTime = new Date(await (await db.getData(url + "[-1]")).date); // 제일 최신 데이터의 조회 시간
            let diff = (nowTime.getTime() - beforeTime.getTime()) / (1000*60); // 분 으로 계산한다.
            // 전 시간과 비교하여 차이가 5분 미만일 때
            if (diff < 5) {
                res.send({success: false, result: "5분 미만입니다."})
            }
            else {
                next();
            }
        }
        // next();
    }
    catch (Err) {
        console.log(Err)
        loggerError.info(Err)
        res.send({ success: false, result: "timeInterverError" });
    }
}

 

/middlewares/index.ts

export * from "./winston"
export * from "./timeInterval"

 

/routers/one.ts

...

// middlewares
import { httpLoggingMiddleware, timeIntervalMiddleware } from '../middlewares';

...

router.get('/', timeIntervalMiddleware ,httpLoggingMiddleware, async function (req: Request, res: Response, next: NextFunction) {

...

 

5. 서버를 실행하고 브라우저를 켜보자.

 

크롤링이 시작하기도 전에 실패 메세지가 도착했다.

 

6. 일정 시간이 지난 데이터는 지워주는 모듈을 만든다.

node-json-db 에는 expire을 설정해주는 메소드가 없는거 같다. 때문에 일정 시간이 지난 데이터를 지워주는 모듈을 만들어봤다.

모듈화에 대해선 다음 포스트에서 제대로 다루겠다.

 

/modules/db/expireHandler.ts

// node-json-db
import { db } from '../../config/nodejsondb';

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

export const expireHandler = async (dbPath: string) => {
    try {
        let nowTime = new Date(); // 현재 시간

        // 데이터 베이스 안 데이터들
        const datalist = await db.getData(dbPath);

        let index = 0; // 기준점이 되는 index

        // 데이터 매핑 및 시간 차이 계산해서 30분 지난 데이터는 삭제
        await datalist.map(async (item : any, index2: number) => {
            let beforeTime =  new Date(item.date); // 데이터들 저장된 시간
            let diff = (nowTime.getTime() - beforeTime.getTime()) / (1000*60); // 데이터들 시간 차이
            // 저장된 지 30분이 지나면 index 체크
            if(diff > 30) {
                index = index + 1;
            }
        })

         // 설정한 시간을 기준점으로 잡고  그 이전에 만든 데이터를 다 삭제한다. (앞에서부터 shift)
        for (let i = 0; i < index; i ++) {
            await db.delete(dbPath + '/data[0]');
        }
    }
    catch(Err) {
        loggerError.info(JSON.stringify(Err))
    }
}

 

/modules/index.ts

export * from "./db/expireHandler"

 

 

미들웨어에 적용시킨다.

/middlewares/timeInterval.ts

...

// module
import { expireHandler } from '../modules';

export const timeIntervalMiddleware = async (req: Request, res: Response, next: NextFunction) => {
    try {
        // 30분이 지난 데이터는 지워줌
        expireHandler(req.originalUrl)
        
...
728x90
반응형
728x90
반응형

 

1. 제 어느정도 세팅했으니 selenium 을 사용해보자. 먼저 설치한다.

$ npm install --save @types/selenium-webdriver selenium-webdriver

 

2. 크롬드라이버의 위치를 .env 파일에 추가해준다. ( 환경 변수 설정 )

/.env

CHROMEDRIVER_PATH="C:\\Users\\Administrator\\Downloads\\chromedriver_win32\\chromedriver.exe"

 

3. one.ts 라우터 파일에 다음과 같이 현재 주소를 가져오는 로직을 구성해준다.

/routers/one.ts

import express, { Request, Response, NextFunction } from 'express';
var router = express.Router();

// middlewares
import { httpLoggingMiddleware } from '../middlewares'

// logger
import {
    loggerHttp,
    loggerDebug,
    loggerError,
    loggerInfo 
    } from '../config/winston';

// selenium
import webdriver from 'selenium-webdriver'
import chrome from 'selenium-webdriver/chrome'

router.get('/',httpLoggingMiddleware, async function (req: Request, res: Response, next: NextFunction) {
    try {
        // 웹드라이버 설정
        let driver = await new webdriver.Builder()
        .forBrowser('chrome')
        .setChromeOptions(new chrome.Options().windowSize({width: 1920, height: 1080}))
        .setChromeService(new chrome.ServiceBuilder(process.env.CHROMEDRIVER_PATH))
        .build()

        // 브라우저에 접속
        await driver.get('https://typo.tistory.com/')

        // 현재 주소 가져오기
        const text = await driver.getCurrentUrl();

        // 현재 주소 출력
        console.log(text)

        res.send({success: true, result : text});
    }
    catch(Err) {
        logger.error(JSON.stringify(Err))
        console.log(Err)
        res.send({success: false, result : Err})
    }
});

export default router;

 

4. 서버를 실행하고 localhost:1234/one 로 접속한다.

정상적으로 뜨는 것을 확인할 수 있다.

728x90
반응형
728x90
반응형

Selenium을 파이썬에서만 쓸 수 있을 줄 알았는데 NodeJS에서도 된다는 말을 듣고 javascript로 만들어볼까 한다.

 

1. Chrome 을 다운로드한다.

https://www.google.com/chrome/index.html

 

Chrome 웹브라우저

더욱 스마트해진 Google로 더 간편하고 안전하고 빠르게.

www.google.com

 

2. 아래 사진처럼 크롬 버전을 익혀둔다.

3. 크롬 버전을 확인했으면 앞자리 수에 맞는 크롬 드라이버를 설치한다.

 

ChromeDriver - WebDriver for Chrome - Downloads

Current Releases If you are using Chrome version 104, please download ChromeDriver 104.0.5112.29 If you are using Chrome version 103, please download ChromeDriver 103.0.5060.53 If you are using Chrome version 102, please download ChromeDriver 102.0.5005.61

chromedriver.chromium.org

 

4. 윈도우로 설치해준다. ( 버전은 다를 수 있습니다. )

 

5. 압축을 해제한 후 해당 파일 경로를 알아둔다. ( 나중에 셀레니움에서 사용 )

 

 

5. NodeJS와 VSCode를 설치한다.

https://nodejs.org/ko/download/

 

다운로드 | Node.js

Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine.

nodejs.org

 

개인적으로 생각하는 쓰기 좋은 vscode typescript 확장들

  1. ES7+ React/Redux/React-Native snippets
  2. ESLint
  3. Live Server
  4. Prettier

 

6. npm init 및 필요 구성 모듈을 설치해준다.

$ npm init
$ npm i express typescript ts-node nodemon @types/node @types/express

 

7. typscript를 위한 옵션을 설정하기 위해 다음 명령어를 입력해준다.

$ npx tsc --init

 

/tsconfig.json

{
  "compilerOptions": {
    "target": "es6", // 어떤 버전으로 컴파일할지 작성 
    "module": "commonjs", //어떤 모듈 방식으로 컴파일할지 설정
    "outDir": "./dist",	//컴파일 후 js 파일들이 생성되는 곳
    "rootDir": ".",	//루트 폴더
    "strict": true,	//strict 옵션 활성화
    "moduleResolution": "node",	//모듈 해석 방법 설정: 'node' (Node.js)
    "esModuleInterop": true,
     "jsx": "react"
  }
}

 

8. app.ts 파일을 만든다.

/app.ts

import express, { Request, Response, NextFunction } from 'express';

const app = express();


app.get('/welcome', (req: Request, res: Response, next: NextFunction) => {
    res.send('welcome!');
});

app.listen('1234', () => {
    console.log(`
  ################################################
  🛡️  Server listening on port: 1234🛡️
  ################################################
`);
});

 

9. pakage.json 파일을 수정해준다.

...
"scripts": {
    "start": "node dist/app.js", 
    "build": "tsc -p .", 
    "dev": "nodemon --watch \"src/**/*.ts\" --exec \"ts-node\" app.ts"
  }
 ...
  • build : typscript 파일을 js 파일로 컴파일해준다.
  • start : 컴파일한 js 파일로 서버를 실행한다.

 

10. 빌드하고 시작해본다.

$ npm run build
$ npm run start

 

정상적으로 뜨는 것을 확인할 수 있다.

 

또한 내가 express를 쓸 때 기본적으로 사용하는게 body-parser와cookie-parser, dotenv인데, 자세한 내용은 검색하면 나온다.

 

나중에 json 형식으로 통신할 때나 쿠키를 사용할 때, 중요한 정보를 환경 변수로 사용할 때 대비하자.

 

11. body-parser , cookie-parser, dotenv를 설치한다.

$ npm install --save body-parser @types/body-parser cookie-parser @types/cookie-parser dotenv

 

 

12. app.ts 파일을 수정해준다.

import express, { Request, Response, NextFunction } from 'express';

// parser
import bodyParser from 'body-parser';
import cookieParser from 'cookie-parser';

// dotenv
import dotenv from 'dotenv'
dotenv.config();

const app = express();

app.use(bodyParser.json())
app.use(cookieParser())


app.get('/welcome', (req: Request, res: Response, next: NextFunction) => {
    res.send('welcome!');
});

app.listen('1234', () => {
    console.log(`
  ################################################
  🛡️  Server listening on port: 1234🛡️
  ################################################
`);
});

 

13. 좀 더 짜임새있는 구조를 위해 미리 폴더들을 만들어두자.

14. routers 밑에 파일을 만들고, app.ts 파일을 수정해준다.

/routers/one.ts

import express, { Request, Response, NextFunction } from 'express';
var router = express.Router();

router.get('/', async function (req: Request, res: Response, next: NextFunction) {
    res.send('welcome!');
});

export default router;

 

/app.ts

import express, { Request, Response, NextFunction } from 'express';

// parser
import bodyParser from 'body-parser';
import cookieParser from 'cookie-parser';

// router
import oneRouter from './routers/one'

const app = express();

app.use(bodyParser.json())
app.use(cookieParser())

app.use('/one',oneRouter)

app.listen('1234', () => {
    console.log(`
  ################################################
  🛡️  Server listening on port: 1234🛡️
  ################################################
`);
});

 

15. logging 미들웨어를 만들기 위해 다음 명령어로 설치해준다.

$ npm install winston @types/winston winston winston-daily-rotate-file

 

/config/winston.ts

import winston, { info } 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 loggerError = winston.createLogger({
  format: combine(
    timestamp({
      format: 'YYYY-MM-DD HH:mm:ss',
    }),
    logFormat,
  ),
  transports: [
    // error 레벨 로그를 저장할 파일 설정
    new winstonDaily({
      level: 'info',
      datePattern: 'YYYY-MM-DD',
      dirname: logDir + '/error',  // error.log 파일은 /logs/error 하위에 저장 
      filename: `%DATE%.error.log`,
      maxFiles: 30,
      watchLog: true,
      zippedArchive: true,
    }),
  ],
});

const loggerInfo = 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 + '/info',
      filename: `%DATE%.info.log`,
      maxFiles: 30,  // 30일치 로그 파일 저장
      watchLog: true,
      zippedArchive: true, 
    }),
  ],
});

const loggerHttp = winston.createLogger({
  format: combine(
    timestamp({
      format: 'YYYY-MM-DD HH:mm:ss',
    }),
    logFormat,
  ),
  transports: [
    // http 레벨 로그를 저장할 파일 설정
    new winstonDaily({
      level: 'info',
      datePattern: 'YYYY-MM-DD',
      dirname: logDir + '/http',
      filename: `%DATE%.http.log`,
      maxFiles: 30,  // 30일치 로그 파일 저장
      watchLog: true,
      zippedArchive: true, 
    }),
  ],
});

const loggerDebug = winston.createLogger({
  format: combine(
    timestamp({
      format: 'YYYY-MM-DD HH:mm:ss',
    }),
    logFormat,
  ),
  transports: [
    // debug 레벨 로그를 저장할 파일 설정
    new winstonDaily({
      level: 'info',
      datePattern: 'YYYY-MM-DD',
      dirname: logDir + '/debug', 
      filename: `%DATE%.debug.log`,
      maxFiles: 30,
      watchLog: true,
      zippedArchive: true,
    }),
  ],
});


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

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

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

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

export { 
  loggerInfo,
  loggerError,
  loggerHttp,
  loggerDebug,
 };

 

/middlewares/index.ts

export * from "./http"

 

/middlewares/http.ts

import express, { Request, Response, NextFunction } from 'express';
import {   
    loggerInfo,
    loggerError,
    loggerHttp,
    loggerDebug,
 } from '../config/winston'

interface LOGSTR {
    url : string;
    method : string;
    query? : Object;
    body? : Object;
}

export const httpLoggingMiddleware = async (req: Request, res: Response, next: NextFunction) => {

    let logStr: LOGSTR = {
        url : "",
        method: "",
        query: "",
        body: "",
    }

    try {
        // 접속 경로
        logStr.url = req.originalUrl;

        // 메소드
        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;
        }

        loggerHttp.info(JSON.stringify(logStr))

        next();
    }
    catch (Err) {
        loggerError.info(Err)
        res.send({ success: false });
    }
}

 

/routers/one.ts

import express, { Request, Response, NextFunction } from 'express';
var router = express.Router();

// middlewares
import { httpLoggingMiddleware } from '../middlewares'

// logger
import {
    loggerHttp,
    loggerDebug,
    loggerError,
    loggerInfo 
    } from '../config/winston';

router.get('/',httpLoggingMiddleware, async function (req: Request, res: Response, next: NextFunction) {
    res.send('welcome!');
});

export default router;

 

 

16. /one 라우터에 접근해보고 로그 데이터가 잘 생성되는지 확인한다.

get 메소드는 브라우저 url 창에 입력해도 된다.

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
반응형

DB에서 데이터를 가져와 fileState 에 담아주었다면, fileState 의 file_location에 S3 파일 저장위치가 담겨있을 것이다.

( fileReducer 참고)

 

리스트 항목을 클릭했을 때 file_location이 있는 상태면 다운로드를 할 수 있게끔 구현해보자.

1. 이전 코드에 추가해준다.

<WrapperDiv
    kindOf={`input`}
>
    <TextField
        label={`사업자등록증`}
        type='file'
        color='primary'
        size='small'
        InputLabelProps={{
            shrink: true,
        }}
        name={`file_object`}
        onChange={(e: any) => {
            let reader = new FileReader();
            let file = e.target.files[0];
            reader.onloadend = () => {
                fileDispatch({ name: e.target.name, value: file });
                fileDispatch({ name: 'file_url', value: reader.result });
            };
            reader.readAsDataURL(file);
        }}
    />
</WrapperDiv>
{fileState.file_url !== '' &&
    <WrapperDiv>
        <img style={{ width: "100%", height: "100%" }} src={fileState.file_url} />
    </WrapperDiv>
}
{(!fileState.file_url && fileState.file_location !== '') &&
    <GridBoxDiv
        gtc={`1fr 1fr`}
        gap={`10px`}
        ai={`center`}
        kindOf={`input`}
    >
        <TextField
            label={`사업자등록증 파일`}
            type='text'
            color='primary'
            size='small'
            InputLabelProps={{
                shrink: true,
            }}
            value={'image.png'}
            disabled={true}
        />
        <Button
            width={`100%`}
            height={`35px`}
            padding={`0`}
            margin={`0`}
            type={`button`}
            kindOf={`greyButton`}
            hoverGrey={true}
            id={fileState.file_location}
            onClick={download}
        >
            download
        </Button>

    </GridBoxDiv>
}

 

리스트 항목을 클릭했을 때 file_url은 없는 상태(undefined)일 것이고 ( DB에 없는 내용이기 때문 ) file_location은 s3 저장위치를

나타낼 것이다.

 

2. 다운로드 버튼을 만들었고 이제 버튼 이벤트를 추가해준다.

    const download = (e: any) => {
        console.log(e.target.id);
        fetch(e.target.id, {
            method: "GET",
            headers: {}
        })
            .then(response => {
                response.arrayBuffer().then(function (buffer) {
                    const url = window.URL.createObjectURL(new Blob([buffer]));
                    const link = document.createElement("a");
                    link.href = url;
                    link.setAttribute("download", "image.png"); //or any other extension
                    document.body.appendChild(link);
                    link.click();
                });
            })
            .catch(err => {
                console.log(err);
            });
    }

 

url 데이터가 있을 때 클릭하면 다운로드를 할 수 있게 해주는 함수이다. 파일의 이름도 지정해줄 수 있다.

여기까지 내용을 정리해보면

 

  1. input type file 을 이용해 클라이언트 State 안에 file 내용을 저장하고, formData 로 만들어 백엔드에 전송한다.
  2. 백엔드에선 multer 로 file을 인식한 뒤 s3.upload 메소드로 파일을 저장해준다. 저장이 성공하면 DB에 key값이랑 location을 저장한다 (file은 인코딩 해아함 )
  3. 클라이언트에서 이미 등록된 사진을 확인할 경우 먼저 DB에 해당 key에 부합하는 데이터가 있는지 확인 후 있으면 location을 가져온다.
  4. location을 Reducer에서 s3 파일 위치로 만들어 state에 담아준다.
  5. url download 함수와 버튼을 만들어 다운로드 가능하게끔 만들어준다.

 

@@ 혹시 큰 용량의 파일이 업로드가 413에러가 뜰 때 nginx가 설치되어있다면 /etc/nginx/nginx.conf 파일에 

http {
    client_max_body_size 5M;

    ...
}

이 구문을 추가해주자.

728x90
반응형
728x90
반응형

전 포스트에서 업로드 하는 것까지 구현해보았다.

이번 포스트에서는 클라이언트에서 파일을 조회하는 것을 구현해보겠다.

 

1. 먼저 리스트 항목 등을 클릭했을 경우의 이벤트 함수를 구현해준다. 파라미터는 원하는 값으로 하면 된다.

// 이미 등록된 파일이 있는지 확인
axios.get(`/api/file`, {
    params: {
        file_customer_code: customerKey
    }
}).then(({ data }) => {
    if (data.success) {
        // 파일이 존재할 경우 
    }
})

 

2. 백엔드에서 데이터베이스를 조회하고 있으면 결과값을 반환해주는 라우터를 작성한다.

router.get('/', function (req, res, next) {
    const query = req.query;
    const customer_code = query.file_customer_code

    try {
        File.findOne(
            {
                where: {
                    file_customer_code: customer_code
                }
            }).then((result) => {
                if (result) {
                    res.send({ success: true, result })
                }
                else {
                    res.send({ success: false })
                }
            })
    }
    catch (Err) {
        res.send({ success: false })
    }
})

 

3. 데이터가 존재할 경우 클라이언트의 file State 를 변경해주기 위해 fileReducer를 수정한다.

( 데이터베이스의 값을 불러와 다운로드 할 때 필요한 파라미터들을 클라이언트로 전송 )

function fileReducer(state: any, action: any) {

    switch (action.type) {
        case 'init':
            return initFileState;
        case 'listClick':
            var newState = state;
            Object.keys(newState).map((item: any, index: any) => {
            	if(item === 'file_location') { // 위치로 s3 url을 만들어 다운로드 기능을 구현한다.
                	newState[item] = 'https://  s3 url ' +action.value[item]
                }
                else {
                	newState[item] = action.value[item]
                }
            })
            return newState;
        default:
            return {
                ...state,
                [action.name]: action.value
            }
    }
}

 

4. fileDispatch를 적용해준다. (DB 내용이 state에 잘 들어오게끔)

// 이미 등록된 파일이 있는지 확인
axios.get(`/api/file`, {
    params: {
        file_customer_code: customerKey
    }
}).then(({ data }) => {
    if (data.success) {
        fileDispatch({ type: 'listClick', value: data.result })
    }
})

 

 

이 파일의 key 값인 s3에 저장된 경로가 클라이언트로 넘어오면 클라이언트쪽에서 파일 다운로드 하는 로직을 구현할 수 있다.

 

  1. 리스트 항목같은 데이터를 조회하고자 하는 버튼을 클릭했을 때 해당 데이터에 맞는 파일이 존재하는지 데이터베이스를 조회한다.
  2. 데이터베이스에 없으면 그냥 패스. 만약 있을 경우 데이터베이스에 저장된 내용 중 file_location( s3 file key )를 클라이언트로 가져온다.
  3. 클라이언트에서 s3에 접속하여 다운로드를 한다.
728x90
반응형
728x90
반응형

전 포스트에서 클라이언트에서 백엔드에 데이터를 전송하는 것 까지 구현해보았다.

 

이번 포스트에서는 백엔드에서 s3에 접근하여 파일을 업로드 하는 것까지 구현해보겠다.

 

1. .env 파일을 만들어준다. ( 전에 버킷 생성할 때 key ID 와 secret key 필요 )

S3_ACCESS_KEY='액세스 키'
S3_SECRET_ACCESS_KEY='비밀 액세스 키'
S3_REGION='리전'
S3_BUCKET_NAME='버킷이름'

 

2. 서버 파일에 필요한 것들을 선언하고 라우터를 만들어준다. ( 서버 파일에 body-parser 필요 )

file.js

const multer = require('multer');
const aws = require('aws-sdk');
require('dotenv').config();

const s3 = new aws.S3({
    accessKeyId: process.env.S3_ACCESS_KEY,
    secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
    region: process.env.S3_REGION,
});

let upload = multer({
    limits: { fileSize: 5 * 1024 * 1024 } // 용량 제한
});


router.post("/upload", upload.array("file_object"), function (req, res, next) {
    try {

        var base64data = new Buffer(req.files[0].buffer, 'binary');

        const params = {
            Bucket: process.env.S3_BUCKET_NAME,
            Key: 'sample.png', // file name that you want to save in s3 bucket
            Body: base64data,
            ACL: "public-read",
            ContentType: "image/png"
        }

        s3.upload(params, (err, data) => {
            if (err) {
                console.log("err : ", err)
                res.send({ success: false });
            }
            else {
                console.log("data : ", data)
                res.send({ success: true, result: data })
            }
        });

    }
    catch (ERR) {
        console.log("ERR : ", ERR)
        res.send({ success: false })
    }

});



module.exports = router;

 

params의 key 부분에서 원하는 파일의 이름과 경로를 지정해줄 수 있다.

 

 

추가로 데이터베이스에 파일에 관한 내용을 추가하여 나중에 불러올 때 편하도록 설계하였다.

s3.upload(params, (err, data) => {
            if (err) {
                console.log("err : ", err)
                res.send({ success: false });
            }
            else {
                File.findOne(
                    {
                        where: {
                            file_customer_code: customer_code,
                        }
                    }).then((result) => {
                        if (result) {
                            File.update({
                                file_url: req.body.file_url,
                                file_location: key
                            }, {
                                where: {
                                    file_customer_code: customer_code,
                                }
                            })
                                .then((result) => {
                                    res.send({ success: true, result: data })
                                })

                        }
                        else {
                            File.create({
                                file_customer_code: customer_code,
                                file_url: req.body.file_url,
                                file_location: key
                            })
                                .then((result) => {
                                    res.send({ success: true, result: data })
                                })

                        }
                    })

            }
        });

 

나중에 파일이 존재하는지 확인할 때 데이터베이스 먼저 확인하고 파일에 접근할 수 있도록 했다.

 

  1. fileState, fileDispatch 설정
  2. input을 만들고 파일 선택 후 state에 담기도록 함.
  3. 담긴 데이터들을 formData로 만들어서 백엔드에 전송
  4. 백엔드에서 해당 파일을 s3에 저장함.
  5. 데이터베이스에 저장된 파일에 대한 내용 (경로 , 고유 키값 등)을 저장함

 

@@ 참고로 파일을 삭제하는 메소드는 deleteObject 이다.

ex)

s3.deleteObject({ Bucket: process.env.S3_BUCKET_NAME, Key: result.file_location }, (err, data) => {
    if (err) {
        console.log("err : ", err)
    }
})

 

728x90
반응형

+ Recent posts