너무 당연해서 알려주지 않는 건 팩트

 

편안


 자바스크립트로 코딩을 하다 보면 리스트 형식의 데이터를 다뤄야 하는 일이 당연히 많습니다. 그리고 그 데이터를 다루는 데 있어서 서버에서 어떤 식으로 보내줄지 항상 고민을 하는데요, 그냥 결론부터 말씀드립니다.

 

가능하면 배열안에 객체를 넣으세요.

 

 앞으로 자바스크립트를 쓰면서 코딩을 하게 되면 filter, find, map, sort 등 리스트에 특화된 많은 함수들을 만날 수 있게 됩니다. 그런데 함수들을 찾아보면 알겠지만 배열일 경우에 사용하는 함수들입니다. 아마 코딩 좀 해본 분들은 "당연히 리스트 형식의 데이터는 배열 형식이지 다른 게 있나?"라고 하겠지만 초보의 눈에서 보면 당연한 것이 아니게 됩니다. 제가 그랬거든요.

 

 예전에 코딩을 독학하면서 만들었던 첫번째 프로젝트에서 리스트를 다룬 제 방법을 보여드립니다.

const informaion = {
    client: 'arikong',
    age: '4',
    gender: 'mail',
    codes: [ 'a1', 'a2', 'a3', 'a4', 'a5' ],
    values: [ 'b1', 'b2', 'b3', 'b4', 'b5' ]
}

 어떤 고객의 정보, 해당하는 코드, 코드에 대한 점수를 객체로 표현했습니다. 위 코드는 짧게 해 놓았지만 사실 20개가 넘고 values의 값도 간단한 배열로 표현했지만 7~8가지 값을 가지고 있었어요. 그리고 데이터베이스에는 각 code값을 설명하는 데이터가 들어있습니다.

 

 그 당시 생각으로는 이제 데이터베이스로 codes값만 보내면 따로 가공할 필요없이 바로 설명하는 값을 얻을 수가 있고, 리스트를 만드는 데 있어서도 codes값을 for문으로 돌려서 쭉 표현하고 values값은 index값으로 가져올 수 있으니 되겠다!라는 생각이었습니다.

for ( let i=0; i < informaton.codes.length; i++ ) {
	let code = information.codes[i]
	let value = information.values[i]
	console.log( code, value)
}

 사실 기능상으로는 문제가 없습니다. 구현하면서 values의 값이 있는지 체크해보는 함수가 늘어나서 그게 좀 짜증났지만 가능했었거든요. 그런데 이 리스트에서 특정한 값만 찾거나, 특정값만 삭제하는 작업을 할 때 혼돈이 찾아왔습니다. 누가 그랬죠? 시작이 반이라고? 제 생각엔 그 이상인거 같아요. 처음부터 뜯어고쳐야 했거든요.

 


 만약 위 데이터를 배열로 만들었으면 어땠을까? 라고 생각하는 순간 새로운 세상이 열렸습니다.

const information = {
    name: 'arikong',
    age: '4',
    gender: 'male',
    codes: [
    	{ code: 'a1', value: 'b1' },
        { code: 'a2', value: 'b2' },
        { code: 'a3', value: 'b3' },
        { code: 'a4', value: 'b4' },
        { code: 'a5', value: 'b5' }
    ]
}

 언뜻보면 무슨 차이인가 싶겠지만 이제 함수를 사용함에 있어서 객체 하나를 찾아서 그 값을 찾고 수정하고 삭제하는데 상당히 편해질 것이라는 것을 예상할 수 있습니다. 이전 데이터에서는 index를 찾아서 각각의 배열마다 값을 수정해야 하고 확인해야 하는 반면에, 지금 데이터는 find함수를 통해서 바로 찾을 수 있고, filter를 이용해서 걸러낼 수도 있습니다.

 

 왜 그런지 모르겠지만... 저 배열안에 있는 객체의 모습을 보면 기분이 좋아져요. 훗...


 그리고 생각해봤습니다. 객체는 어떻게 표현되어야 하는 것일까? 예전 프로젝트에서는 그냥 Key값과 Value값을 갖는 데이터 표현방식, 또는 집합체라고 생각했었는데, 지금은 하나의 물건 또는 사람, 그 자체라는 생각을 합니다.

 

 예를 들어 사람 A, B, C가 있다고 했을 때, 예전에는 A, B, C의 나이를 묶고, 성별을 묶어서 객체로 만들었습니다. 특정한 집합을 만들었죠. 그렇게 되면 나이가 어떤지, 성별이 어떤지 한눈에 들어오지만, 어떤 사람의 나이와 성별을 같이 생각해야 할 때는 복잡해지게 됩니다. 

 

 이제 A, B, C를 따로 객체 하나하나 만들게 되면, 사람 한 명 한 명을 바라볼 수 있고 누가 어떤지 확인이 쉬워집니다. 그런데 나이만 따로 알고 싶고, 성별만 알고 싶은 일이 생기는데, 그럴 때는 자바스크립트 함수가 일을 쉽게 할 수 있게 도와줍니다. 코드가 짧아지죠. 읽기 쉬워집니다.

 

 코드 좀 해본 분이라면 당연히 알고있고, 자연스럽게 알게 되는 이 정보를 누군가는 궁금해하지 않을까 하면서 포스팅해봅니다.

 

제가 그래요... 내가 하고 있는 코딩이 맞나 싶을때가 있어요...
너는 ㄷㄷㅊ 무엇이냐

이름이 참 예쁜 그대

 


"장단점 정리는 중간부터! 급하신 분은 스크롤 다운하세요!"

 

 생활코딩 PHP수업을 졸업하고 제 첫 프로젝트는 프레임워크 없이 진행되었습니다. 로그인, 이메일 비번 찾기, 데이터베이스 등 제 손으로 한 땀 한 땀 코딩을 했습니다. 1개월 정도 그 프로젝트에 빠져 살았던 것 같아요. 그때는 그게 맞는지 틀렸는지 모르고 막 코딩을 했습니다. 구현이 되면 성공한거고, 안되면 실패하는 단순하지만 강력한 피드백이 있었죠.

 

 그런데 그런데 그 이후 다른 프로젝트를 진행할 일이 생겼습니다. 그 프로젝트가 개인정보를 다뤄야 하는 프로젝트라서 두려움이 앞섰습니다. 코딩은 하면 됐지만 초보인 저에게는 "직접 코딩했다가 누군가에게 해킹을 당할지도 몰라!"라는 막연한 두려움이 가장 컸습니다. 

 

 그래서 알아보았습니다. 물론 초보이긴하지만 가장 자신 있는 PHP 언어로 만든 안전한 프레임워크를 검색해보았습니다. 그래서 찾은 것이 라라벨(Laravel)이었죠.

 

우오옷!

 제가 하려고 하는 모든 기능이 다 들어있었습니다. 없는 게 없는, 말 그대로 종합 선물 패키지!

 처음 라라벨을 실행하고 그 화면을 보는 순간 그 경이로움은 이로 말할 수 없었습니다. 그제서야 제가 왜 그 이전 프로젝트를 직접 코딩했는지 이해할 수 있었습니다. 그렇습니다. 그 날의 그 환희를 느끼기 위해서 1개월을 고생한 것이었습니다.

 

 맨땅에 헤딩을 해 본 자만 느낄 수 있는 도구의 경이로움!

 

 그렇게 시작된 저의 프레임워크 입문기는 시작되었습니다.

 

 처음 공식문서를 읽을 때 욕이 나왔습니다. 초보들은 이해하지 못할 말들의 향연이었죠. 지금은 그게 당연하다고 생각되지만 초보의 눈에 전체를 보는 눈 따위 개나 줘버렸..

 

 오픈 채팅방에 들어가서 너무 초보 같은 질문이었는지(지금은 생각나지 않지만) 질문을 올리면 하면 공식문서 정독하고 오라는데... 서비스 컨테이너 읽다가 포기한 게 몇 번인지 기억도 나지 않습니다. 그렇게 포기를 반복하다가 안 되겠다 싶어서 그냥 무작정 프로젝트를 시작했습니다. 어떻게든 되겠지라는 생각으로요. 그리고 프로젝트는 성공했습니다. 물론 메이저급의  서비스가 아니라 프로토타입 정도니까 가능했겠죠.

 

 써보니까 좀 알겠더군요. 그리고 생각했습니다.

내가 시작부터 너무 큰 놈을 만났구나. 

 프로그래밍 도구를 사용하는데 순서가 있다면 끝판왕을 만난 것이었어요. 도구치고는 너무 큰 도구를 만나버렸달까. 아기에게 전동드릴을 쥐어준 것 같은 상황이었던 것입니다.

 

그렇게 3년이 지난 지금도 공부 중입니다...

 

 그래도 성과라면 성과랄까. 어느 정도 어떻게 돌아가는지 알게되니 다른 프레임워크(예를 들어 express같은)를 쓰는데 보는 눈이 생겼습니다. 어떤 기능이 부족하고, 어떤 기능이 편리한지. 어찌보면 힘들었지만 좋은 공부가 되었던 것이었죠. 그래서 앞으로 라라벨에 입문할 사람들을 위해 무엇인가를 알려드리고 싶습니다.

 

 어느정도 어려운 것은 동기부여가 되지만 포기할 정도로 어렵지 않았으면 좋겠습니다. 그래서 제가 이해한 내용을 앞으로도 시리즈 형식으로 포스팅하려고 합니다. 짧게 짧게 이해하기 좋게 간단하게요. 어차피 전문적인 내용을 이해가 되게 설명할 정도로 내공이 깊지 않아요.


 그래서 그 첫 번째, 제가 느낀 라라벨이라는 프레임워크의 장단점을 나열해보려고 합니다.

 

1. 장점

① 폭넓은 커뮤니티

 커뮤니티가 좁거나 정보가 적으면 처음 시작하는 사람들은 프레임워크가 아무리 좋아도 시작하기가 어렵습니다. 공부가 어려운 정도가 아니라 불가능해지죠. 놀랍게도 라라벨은 깃헙스타가 2020년 11월 2일 기준 62000개가 넘습니다. 백엔드 프레임워크 중에는 탑급입니다. 프로그래머들 사이에서 인정받고 있다는 근거입니다. 인기가 있는 만큼 커뮤니티도 잘 구성되어있습니다. 라라캐스트라라벨IO 같은데서 필요한 정보들을 잘 알려주고 있습니다. 대부분의 정보들을 얻을 수 있지만 영어인 탓에 접근이 어려운 분들을 위해 한국인 라라벨러들이 모여있는 카카오톡 오픈 채팅방도 있으니 참고하세요.

 

빠른 생산성

 라라벨을 이용하면 빠르게 어플리케이션을 시작할 수 있습니다. 기본적으로 로그인이나 이메일 인증, 레이아웃 등이 지원되기 때문입니다. artisan 이라는 CLI(command line interface)를 이용하면 더 빠르게 적용할 수 있죠. 최근에는 라라벨 Jetstream이라고 불리는 스캐폴딩 기능까지 제공하면서 SPA(single page application)를 빠르게 만들 수 있습니다. 제가 좋아하는 tailwindcss도 자동으로 더해지다니 놀랍지 않을 수가 없어요.

 

2. 단점

① 비동기 처리

 아쉽게도 많은 양의 정보를 동시에 처리하는 기능이 조금 떨어집니다. 제가 앞에서 포스팅했던 Nodejs로 크롤링하기 같은 비동기적인 처리가 아쉽게도 구현이 어렵습니다. 불가능하진 않아요. 찾아보면 방법은 있습니다. 하지만 비교해보면 비동기적인 처리를 하기에 PHP가 불편해요.

 

② 느린 속도

 라라벨이 많은 기능을 포함하고 있어서 그런지는 몰라도 요청을 처리하는데 다른 프레임워크에 비해 느린 속도를 보입니다.

출처: https://decorus.postype.com/post/3538443

 위는 초당 요청 개수를 나타내는데 라라벨은 정말 느린 속도를 보입니다. 장단점이 확실한 프레임워크라는 것이죠.

 

3. 그 외

 제가 라라벨을 사용하면서 구글링을 하면 좀 이상했습니다. PHP를 평가하는 글에는 항상 부정적인 댓글들이 있었거든요. PHP를 옹호하는 사람들과 비판하는 사람들의 의견이 제 눈에는 어려워서 평가를 못하겠지만, 시작한 지 얼마 안 되는 저의 눈에 PHP는 뭔가 아쉬운 언어고, 라라벨은 대단한 프레임워크 같아요. 

 

 그래서 저는 장단점이 확실해서 빠르게 뭔가를 만들어봐야 하는 프로젝트나 홈페이지 같은 가벼운 애플리케이션 같은 경우에는 라라벨을 쓸 것 같아요. 그 외에 많은 데이터를 요청하거나 처리해야하는 프로젝트에는 라라벨을 쓰지 않을 것 같아요. NodeJS, express를 사용할 거고, 연습 프로젝트는 NodeJS, express로 진행하고 있어요.

 

 모든 것을 만족하는 프레임워크를 찾기보다 여러 가지를 익혀서 본인의 목적에 맞게 쓰는 게 답인 것 같습니다. 다음 라라벨 포스팅부터는 심플한 사용기를 포스팅할게요.

 

 

laravel/laravel

A PHP framework for web artisans. Contribute to laravel/laravel development by creating an account on GitHub.

github.com

 


그림 출처 : Human, All Too Human https://decorus.postype.com/post/3538443

홈페이지 정도는 뚝딱 만들어요. 신세계!
내쓸내정(내가 쓸 거 내가 정리한다) 시리즈의 시작

 

쓰려고 하면 헷갈려 이놈들

 


 자바스크립트로 코딩을 하다 보면 항상 배열 또는 문자열을 자르고 붙이는 일이 자주 생깁니다. 특히 배열이나 문자열을 자를 때는 slice 함수와 splice 함수는 항상 헷갈리게 되죠. paramete에 넣는 게 시작이 먼저인지 끝이 먼저인지, 반환 값이 배열인지 원래 배열을 수정하는 건지 매번 찾아보다 지쳐서 그냥 정리하게 되었습니다.

 

 남들도 많이 포스팅해서 차고 넘치지만 내가 쓸 거 내가 정리한다 시리즈 1탄.

 

slice() vs splice()


slice

▷ array.slice([begin[, end]])

  ☆ 반환 값 : 새로운 배열, 기존의 배열의 값을 수정하지 않는다.

 

  ① begin : 어디서부터 자를지 시작점을 정한다. 인덱스 값이다.

let arr = [1, 2, 3, 4, 5]

      - 0일 경우 : 시작점부터 잘라 가져온다.

let result = arr.slice(0)	//result : [1, 2, 3, 4, 5]

      - 양수일 경우 : 인덱스(start값의 자리에 있는 배열 값)를 포함하고 값을 가져온다.

let result = arr.slice(2)	//result : [3, 4, 5]

      - 음수일 경우 : 배열의 마지막 값이 (-1)이다. (-2) 일 경우 배열의 마지막 두 개의 값을 잘라서 가져온다.

let result = arr.slice(-2)	//result : [4, 5]

      - undefined인 경우 : 0일 경우와 같다.

let result = arr.slice(undefined)	//result : [1, 2, 3, 4, 5]

      - 배열의 길이보다 숫자가 큰 경우 : 빈 배열을 반환한다.

let result = arr.slice(9	//result : []

 

  ② end : 어디까지 자를지 정한다. 인덱스 값이다. 생략이 가능하다. 생략할 경우 시작점부터 배열의 마지막까지 가져온다.

      - 0일 경우 : 시작점이 어디든 빈 배열을 반환한다.

let result1 = arr.slice(1, 0)		//result1 : []
let result2 = arr.slice(-1, 0)		//result2 : []
let result3 = arr.slice(0, 0)		//result3 : []
let result4 = arr.slice(10, 0)		//result4 : []
let result5 = arr.slice(undefined, 0)	//result5 : []

      - 양수일 경우 : begin 인덱스 값은 포함, end 인덱스(end값의 자리에 있는 배열 값) 값은 제외하고 가져온다.

let result1 = arr.slice(0, 2)		//result1 : [1, 2]
let result2 = arr.slice(undefined, 2)	//result2 : [1, 2]
let result3 = arr.slice(-3, 5)		//result3 : [3, 4, 5]
let result4 = arr.slice(-3, 1)		//result4 : []
//result4의 경우 begin 인덱스의 위치보다 end 인덱스의 위치가 더 앞(왼쪽, 작은값)이라서 빈 배열 반환

      - 음수일 경우 : (-2)일 경우 배열의 마지막 두 개의 값을 잘라서 버리고 나머지만 가져온다.

let result1 = arr.slice(0, -2)		//result1 : [1, 2, 3]
let result2 = arr.slice(undefined, -2)	//result2 : [1, 2, 3]
let result3 = arr.slice(-3, -5)		//result3 : []
//result3의 경우 begin 인덱스의 위치보다 end 인덱스의 위치가 더 앞(왼쪽, 작은값)이라서 빈 배열 반환
let result4 = arr.slice(-3, 1)		//result4 : [3, 4]

      - undefined인 경우 : 배열의 시작점부터 마지막 값까지 포함해서 가져온다.

let result1 = arr.slice(0, undefined)		//result1 : [1, 2, 3, 4, 5]
let result2 = arr.slice(undefined, undefined)	//result2 : [1, 2, 3, 4, 5]
let result3 = arr.slice(-3, undefined)		//result3 : [3, 4, 5]

      - 배열의 길이보다 숫자가 큰 경우 : undefined일 경우와 같다.

let result1 = arr.slice(0, 7)		//result1 : [1, 2, 3, 4, 5]
let result2 = arr.slice(undefined, 7)	//result2 : [1, 2, 3, 4, 5]
let result3 = arr.slice(-3, 7)		//result3 : [3, 4, 5]

 splice

▷ array.splice(start[, deleteCount[, item1[, item2[, ...]]]])

  ☆ 반환 값 : 제거한 요소 배열. 기존 배열의 값을 수정한다. 조심하자.

  ☆ 값을 가져올 경우 기존 배열에서 가져온 값이 삭제가 된다.

  ☆ 값을 추가할 경우 기존 배열에 값이 추가된다.

 

  ① start : 어디서부터 자를지 시작점을 정한다.

let arr = [1, 2, 3, 4, 5]
//spilce()는 기존 배열의 값을 바꾸기 때문에 연습코드마다 배열을 다시 정의해야 한다.

      - 0일 경우 : 시작점부터 잘라 가져온다.

let arr = [1, 2, 3, 4, 5]
let result = arr.splice(0)
//result : [1, 2, 3, 4, 5]
//arr: []

      - 양수일 경우 : 인덱스(start값의 자리에 있는 배열 값)를 포함하고 이후 배열의 마지막 값까지 가져온다.

let arr = [1, 2, 3, 4, 5]
let result = arr.splice(2)
//result : [3, 4, 5]
//arr: [1, 2]

      - 음수일 경우 : 배열의 마지막 값이 (-1)이다. (-2) 일 경우 배열의 뒤에서 두 번째 값을 나타낸다.

let arr = [1, 2, 3, 4, 5]
let result = arr.splice(-2)
//result : [4, 5]
//arr: [1, 2, 3]

      - undefined인 경우 : 0일 경우와 같다.

let arr = [1, 2, 3, 4, 5]
let result = arr.splice(undefined)
//result : [1, 2, 3, 4, 5]
//arr: []

      - 배열의 길이보다 숫자가 큰 경우 : 빈 배열을 반환한다.

let arr = [1, 2, 3, 4, 5]
let result = arr.splice(7)
//result : []
//arr: [1, 2, 3, 4, 5]

 

  ② deleteCount : 몇 개를 가져올지 정한다. 생략이 가능하다. 생략할 경우 시작점부터 배열의 마지막까지 가져온다.

      - 0일 경우 : 시작점이 어디든 빈 배열을 반환한다. 가져올 개수가 0이기 때문이다.

let arr = [1, 2, 3, 4, 5]
let result1 = arr.splice(0, 0)
let result2 = arr.splice(1, 0)
let result3 = arr.splice(undefined, 0)
let result4 = arr.splice(-1, 0)
//all result : []
//arr: [1, 2, 3, 4, 5]

      - 양수일 경우 : start 인덱스 값을 포함한 숫자만큼 값을 가져온다.

let arr = [1, 2, 3, 4, 5]
let result1 = arr.splice(0, 2)
//result1 : [1, 2]
//arr: [3, 4, 5]

let arr = [1, 2, 3, 4, 5]
let result2 = arr.splice(2, 2)
//result2 : [3, 4]
//arr: [1, 2, 5]

let arr = [1, 2, 3, 4, 5]
let result3 = arr.splice(undefined, 2)
//result3 : [1, 2]
//arr: [3, 4, 5]

let arr = [1, 2, 3, 4, 5]
let result4 = arr.splice(-2, 2)
//result4 : [4, 5]
//arr: [1, 2, 3]

      - 음수일 경우 : 0일 경우와 같다. 

let arr = [1, 2, 3, 4, 5]
let result1 = arr.splice(0, -2)
let result2 = arr.splice(1, -2)
let result3 = arr.splice(undefined, -2)
let result4 = arr.splice(-1, -2)
//all result : []
//arr: [1, 2, 3, 4, 5]

      - undefined인 경우 : 0일 경우와 같다.

let arr = [1, 2, 3, 4, 5]
let result1 = arr.splice(0, undefined)
let result2 = arr.splice(1, undefined)
let result3 = arr.splice(undefined, undefined)
let result4 = arr.splice(-1, undefined)
//all result : []
//arr: [1, 2, 3, 4, 5]

      - 배열의 길이보다 숫자가 큰 경우 : 배열의 마지막 요소까지 포함하여 가져온다.

let arr = [1, 2, 3, 4, 5]
let result1 = arr.splice(0, 7)
//result1 : [1, 2, 3, 4, 5]
//arr: []

let arr = [1, 2, 3, 4, 5]
let result2 = arr.splice(2, 7)
//result2 : [3, 4, 5]
//arr: [1, 2]

let arr = [1, 2, 3, 4, 5]
let result3 = arr.splice(undefined, 7)
//result3 : [1, 2, 3, 4, 5]
//arr: []

let arr = [1, 2, 3, 4, 5]
let result4 = arr.splice(-2, 7)
//result4 : [4, 5]
//arr: [1, 2, 3]

 

  ③ item... : 추가할 요소. 생략이 가능하다. 생략할 경우 요소를 제거하기만 한다.

      - 요소가 추가될 위치는 start 인덱스 값의 위치가 기준이다.

let arr = [1, 2, 3, 4, 5]
let result1 = arr.splice(0, 2, "a", "b", "c")
//result1 : [1, 2]
//arr: ["a", "b", "c", 3, 4, 5]

let arr = [1, 2, 3, 4, 5]
let result2 = arr.splice(2, 2, "a", "b", "c")
//result2 : [3, 4]
//arr: [1, 2, "a", "b", "c", 5]

      - start 값이 0 또는 양수일 경우 : 선택된 값 바로 다음 인덱스부터 차례로(오른쪽으로) 값이 쌓인다.

let arr = [1, 2, 3, 4, 5]
let result = arr.splice(2, 0, "a", "b", "c")
//result : []
//arr: [1, 2, "a", "b", "c", 3, 4, 5]

      - start 값이 음수일 경우 : 선택된 값 이전 인덱스부터 차례로(오른쪽으로) 값이 쌓인다.

let arr = [1, 2, 3, 4, 5]
let result = arr.splice(-2, 0, "a", "b", "c")
//result : []
//arr: [1, 2, 3, "a", "b", "c", 4, 5]

 


정리하고 보니 나는 이해가 되는데 남이 보기 어렵게 되어버린 거 같은데?
숨기고 싶은 내 마음

니가 모르는 문자로


 오늘은 단방향 암호화 기법 Hash에 대해 글을 써보겠습니다.

 

쇼핑몰 사이트에서 API를 이용해서 이것저것 정보를 가져올 때, 이 쇼핑몰에서는 당연하게도 제가 요청한 내용과 저를 증명할 수 있는 방법을 요구하는데요. 이 쇼핑몰에서는 SHA-256 알고리즘(SHA는 Secure Hash Algorithm의 줄임말)을 이용해서 암호화를 하길래 NodeJS 모듈인 Crypto를 이용해서  Hash값과 HMAC값을 만들었습니다.

 

 여기서 잠깐, Hash는 뭐고 HMAC이 무엇인지 간단하게 설명하고 갈게요.

 

※ Hash(해시)

 단방향 암호화 기법 중 하나인 해쉬 알고리즘을 이용해서 일정한 길이의 암호화된 문자열을 생성하는 일을 Hashing이라고 하고, 그 Hashing 이후의 값을 Hash 값, Hash 값을 만들어내는 함수를 Hash 함수라고 합니다. Hash라고 하면 대략 Hash 알고리즘을 이용한 암호화라고 이해하면 될거 같아요.

 

※ HMAC(Hash based Message Authentication Code)

 HMAC에서 MAC는 메세지를 주는 사람과 받는 사람 사이에 그 메세지가 변형되지는 않았는지 확인하는 방법(변조 여부)으로 앞에 "H"가 붙으면 Hash 알고리즘을 이용한다는 뜻입니다. 

 

 Hash 함수 안에는 여러 가지 알고리즘이 있는데 요즘은 제가 쓰고 싶은 API는 SHA-256 방식의 알고리즘을 이용하라고 하니 그대로 적용해보겠습니다.


▷ Hash 값

1. 모듈을 import 해줍니다.

import crypto from "crypto"

 

2. 암호화 해주고 싶은 base_str을 다음처럼 암호화합니다.

const base_str = "this_is_base_string"

const result = crypto.createHash('sha256').update(base_str).digest('hex')

 여기서 결과값은 result에 담기게 됩니다.

 

 내용을 대략적으로 설명해보자면 다음과 같습니다.

 

 crypto 모듈을 이용해서 SHA-256 알고리즘('sha256')으로 해시 함수인 ceateHash를 통해 암호화를 한다. 암호화할 문자열은 base_str이고, 결과값을 hex 방식으로 표현한다.

 

 여기서 digest에 hex라는 말은 표현 방식인데, 가능한 표현방식으로는 'binary', 'hex', 'base64'가 있습니다. 'binary'는 2진수, 'hex'는 16진수, 'base64'는 64진수를 나타는데, 제가 쓸 API는 'hex' 방식을 요구하니까 'hex'를 사용합니다.


▷ HMAC 값

1. hash와 같습니다.

import crypto from "crypto"

 

2. 여기서는 암호화에 필요한 임의의 문자열(Secret Key)이 필요합니다.

const base_str = "this_is_base_string"

const secret_key = "thisIsSecretKeyPleaseDoNotOpenItToOtherPerson"

const result = crypto.createHmac('sha256', secret_key).update(base_str).digest('hex')

 

 crypto 모듈을 이용해서 SHA-256 알고리즘('sha256')으로 createHmac 함수를 통해 secret_key를 이용해서 암호화를 한다. 암호화할 문자열은 base_str이고, 결과값을 hex 방식으로 표현한다.

 

 여기서 secret_key는 공개되면 해커가 제 쇼핑몰을 다 망쳐놓을 수 있으니 절대 노출되어서는 안됩니다.

 

 그리고 이 secret_key는 저와 쇼핑몰 서버만 알고 있고 이를 통해서 제가 요청한 API가 안전한지, 변조되지는 않았는지 확인하는 작업을 거치게 되며 이런 방식으로 API 요청에 반응하게 됩니다.

 

  항상 느끼는 것이지만 이해하려고 하면 어렵지만 사용하기는 쉽군요.


역시 프로그래머들은 멋있어
앵간하면 다 가능

nodejs + poppeteer


링크

웹 크롤링 해보기 #1 - NodeJS, cheerio (feat. VueJS)

② 웹 크롤링 해보기 #2 - NodeJS, puppeteer

 

 이번 포스팅에는 저번에 이어서 한번 더 크롤링(crawling or scrapping)을 해볼까 합니다.

 

 이번 포스팅에서는 cheerio라는 라이브러리를 다뤘었는데요. 이번에는 웹에 들어가서 여러 가지 "javascript의 일"을 할 수 있는 라이브러리를  다뤄보려고 합니다. 바로 "꼭두각시"라는 뜻을 가지고 있는 듯한 "Poppeteer"입니다.

 

cheerio와 다른 점이라면 웹사이트에 로그인을 한다던가, 웹사이트 스크린샷을 찍어서 파일로 저장한다던가, PDF로 만들어서 저장한다던가 하는 일들을 할 수 있습니다.

 

 이번에 제가 보여드릴 예제는 "상품 긁어오기"입니다. 상품을 긁어오는데 cheerio면 충분할 줄 알았는데 복병이 있었습니다. 바로 네이버..

 

 네이버는 상품의 옵션을 보여줄 때 옵션 버튼을 클릭하기 전까지는 옵션의 엘리먼트들을 "hidden"처리를 해놓기 때문에 cheerio로는 긁어오는 것이 불가능합니다. 

 

삼다수를 제주도에서 시키면 4천원 배송비가 더 드는 현실

 요로코롬 눌러줘야 옵션 창이 뜹니다. 그래서 제가 이야기했던 "자바스크립트의 일"인 버튼을 클릭하는 일을 할 수 있는 라이브러리를 찾던 중에 Puppteer를 발견한 것이었습니다.

 

 이 라이브러리는 chromium의 렌더링 엔진을 사용해서 headless browser 제어를 도와줍니다.

 

 말이 어렵군요.. 제가 이해하는 방식으로 설명해보겠습니다.(저처럼 이해해도 사용하는데 문제가 "아직은" 없었으니까요.)

 

 일반적으로 우리가 사용하는 크롬처럼 GUI방식의 웹브라우저를 CLI(Command Line Interface) 방식으로 제어한다고 생각하면 쉬울 것 같습니다. 맥이면 터미널, 윈도우면 cmd창으로 브라우저를 제어한다는 의미입니다. 크롬 창을 띄우지 않았는데 크롬창을 띄운 것처럼 실행하고 반응하고 원하는 작업을 할 수 있는 것입니다.

 

 자, 그럼 대충 바로 시작해볼게요.

 

1. 설치를 해야겠죠?

// npm
npm install puppeteer

//yarn
yarn add puppeteer

 

2. 그리고 nodeJS 파일에 임포트

import puppeteer from 'puppeteer'

 

3. 그리고 비동기 함수를 만들어 주기

async function scrapeWeb {

    // headless browser start
    const browser = await puppeteer.launch()

    // open new page
    const page = await browser.newPage()

    // connect web link
    await page.goto('http://example.com')

    // wait page
    await page.waitForNavigation({timeout: 10000})
    
    // click event
    await page.click("a.a_class")
    
    // wait option popup
    await page.waitForSelector('ul.li_class', {timeout: 10000})

    // scrape web data
    data = await page.evaluate( () => {
    	
        // do something you want
    	title = document.querySelector("._3oDjSvLwq9").textContent

    	return {
     		title: title
        }
        
    })

    // close browser
    await browser.close()

}

 poppeteer.launch()는 기본값으로 headless로 실행이 됩니다.

 page.waitForNavigation({timeout:10000})은 페이지가 완전히 로딩되기를 기다립니다. 이 메소드의 실행 완료를 기다리지 않고 클릭 메소드를 실행해도 ul 엘리먼트가 감지 되지 않아서 꼭 필요합니다. timeout은 기본값이 30초인데, 페이지의 완전한 로딩을 30초 동안 기다려준다는 뜻입니다. 제가 성격이 느긋하다지만 오류가 생겨도 30초는 못기다리겠더라구요. 오류가 생겼으면 빨리 뱉으라는 심정으로 10초로 줄였습니다.(열리더라도 10초 넘게 기다려야되면 그것도 오류아닌가요 여러분?)

 페이지의 로딩을 기다리고 클릭도 하고 ul 엘리먼트가 생기는 것을 확인한 후에 page.evaluate() 메소드를 이용해서 하고 싶은 작업을 맘껏 하시면 됩니다.

 그리고 잊지 말고 browser.close() 하면 끝.

 

4. 끝

 끝입니다.


5. 번외 & 팁

  5-1. page.evaluate에 argument(인자)를 전달하고 싶을 때

page.evaluate( (name) => {

    const data = `${name}`

    return data
    
}, name)

 

  5-2. page.evauluate에 인자를 여러 개 전달하고 싶을 때

    5-2-1. 객체 법

page.evaluate( ({name, age, gender}) => {

    const data = `${name}${age}${gender}`

    return data
    
}, {name, age, gender})

    5-2-1. 직렬 법

page.evaluate( (name, age, gender) => {

    const data = `${name}${age}${gender}`

    return data
    
}, name, age, gender)

 원하시는 방법대로 인자를 전달하세요.

 

  5-3. page.evaluate() 안에서 console.log는 에러를 뱉어냅니다.

 console.log를 하면 에러( Evaluation failed: ReferenceError: _ac2‍ is not defined )를 뱉어냅니다. _ac2는 랜덤하게 바뀌는 문자열입니다. 정확한 원인을 찾지 못해서, 또는 찾은걸 해봤는데 안 돼서, 또는 찾았는데 이해가 안돼서 그냥 저는 값을 계속 return 시켜보면서 확인했습니다.

=>2021.3.13 추가

 역시 모르면 공부하면 알게되는데 귀찮아서 미루고 있다가 지금 알게되었습니다. evaluate는 저쪽 크롤링을 시전하고 있는 puppeteer쪽 크롬창에 명령을 한번에 전달하는 메소드입니다. 그래서 그 안에 console.log를 하면 저쪽 크롬창에 뜨게 되는 것이죠. puppeteer를 launch할 때 옵션으로 {headless: false}옵션을 주면 창이 하나 뜨면서 확인이 가능합니다. 물론 나중에는 다시 꺼놔야겠지요!

 

링크

① 웹 크롤링 해보기 #1 - NodeJS, cheerio (feat. VueJS)

② 웹 크롤링 해보기 #2 - NodeJS, puppeteer


5-3 해결 방법 아시는 분? 해답이 이제는 필요 없지만 찍히는 거 보고 싶어요.
모두가 필수 코스로 해본다는 크롤링, 제가 해보겠습니다.

 


링크

① 웹 크롤링 해보기 #1 - NodeJS, cheerio (feat. VueJS)

웹 크롤링 해보기 #2 - NodeJS, puppeteer

 

 오늘은 웹 크롤링(스크래핑)에 대한 내용입니다.

 

 저는 뭔가를 빠르게 구현해보고 싶을 때 PHP framework인 라라벨을 주로 사용하는데 웹 크롤링을 하기 위해 빠르게 비동기적으로 처리가 가능한 nodeJS를 선택했습니다. PHP는 언어의 특성상 크롤링을 하는데 적합하지 않다고 판단했습니다. 하나의 명령을 처리하고 그 명령을 완료하기 전에 다음으로 넘어가지 않아 많은 명령을 동시에 처리하지 못하는 한계를 가지고 있기 때문입니다. 물론, 그것을 보완하는 방법들도 있기는 하지만 그냥 nodeJS로 구현하면 쉬워져요.. 심지어 빠릅니다.

 

슈로록

 

 쿠팡에서 아이템의 수량을 주기적으로 파악해야 할 일이 생겨서 웹사이트를 들락날락하던 중에 클릭이 귀찮아져서 그냥 하나 만들어 봤습니다.

 

 자세히 보면 1번 행부터 순차적으로 진행되는 것이아니라 순서 없이 요청이 완료되는 순서대로 값이 보입니다. 이것이 nodeJS의 힘. 그럼 바로 하나씩 진행해 보겠습니다.

 

1. vue cli를 이용하여 프로젝트 만들기

 여기서 두가지 방법으로 나누어집니다.

 

 첫 번째는, 폴더를 두 개 만들어서(frontend, backend) frontend폴더에 vue-cli를 이용해서 프로젝트를 만들고, backend 폴더에 nodeJS, express 프로젝트를 만들어서 api 통신하게 만드는 방법!

 두 번째는 vue-cli로 프로젝트를 만들어서 express plugin을 이용하는 방법!

 

 첫 번째 방법은 다른 분들도 포스팅을 많이 하기도 했고, plugin을 사용해보고 싶어서 두 번째 방법으로 해보았습니다(vue-cli 버전이 3.x.x에서 작동합니다. vue --version으로 확인하세요).

 

vue create project-name

 먼저 폴더로 가서 프로젝트를 만들어 줍니다.

 

2. express plugin install

cd project-name
vue add express

 다음은 프로젝트 폴더로 들어가서 express를 add 해주면 끝. 간단하지요?

 

 package.json

"scripts": {
	...
	"express:watch": "vue-cli-service express:watch",
    ...
},

 개발을 하면서 코드 변경이 있으면 자동으로 서버를 재시작해주는 명령어입니다. 이거 없으면 기분이 안 좋아요. 꼭 하세요.

 

3. frontend, backend 실행

 

 

 폴더 구조는 다음과 같아지는데 src가 frontend, srv가 backend 폴더라고 보면 쉽습니다.

 

 그럼 실행!

// frontend start - http://localhost:8080
npm run serve


// backend start - http://localhost:3000
npm run express:watch

 이렇게 하면 시작할 수 있는데 저 같은 경우에는 port가 frontend는 8080, backend는 3000입니다. 개발환경에 따라 port는 달라질 수 있으니 확인하세요!

 

4. 라이브러리 설치

 이제 웹 크롤링에 사용할 cheerio 라이브러리를 설치!

npm install cheerio

 

Q. cheerio 말고 다른 라이브러리는 없나요?

A. 기본적으로 웹을 크롤링할 때는 목적이 있습니다. 저처럼 로그인이 필요 없고, 그냥 웹페이지의 내용만 가져올 때는 cheerio가 적합하죠. 가볍고 빠릅니다.

 하지만 웹에 들어가서 자동으로 로그인을 한다던가, 클릭해서 팝업창을 열어서 그 내용을 가져온다던가, 드래그 앤 드롭을 한다던가 하는 "javascript의 일"을 해줄 순 없습니다. 그래서 이런 "javascript의 일"을 하기 위해서 selenium, phantomJS, nightmareJS 등 다른 라이브러리를 이용하기도 하니까 목적이 다르다면 위 라이브러리를 검색해보세요.

 

5. 코드

 srv/index.js

import express from 'express'
import cors from 'cors'
import { checkProducts } from "./apis/checkProducts"

export default (app, http) => {

  app.use(express.json())
  app.use(cors({
    origin: 'http://localhost:8080',
    credentials: true,
  }))

  app.post('/check', (req, res) => {
    let id = req.body.id
    let link = req.body.link

    checkProducts(id, link).then( product => {
      res.json(product)
    }).catch( err => console.log(err) )
  })
  
}

 cors는 frontend에서 api 호출할 때 오류가 나서 추가했어요. 오류가 안 난다면 그냥 진행하셔도 무방합니다.

 

srv/apis/checkProducts.js

import axios from 'axios'
import cheerio from 'cheerio'

function checkProducts(id, url) {

    return axios.get( url ).then( ({data}) => {
        let $ = cheerio.load(data)
        let $prodName = $("div.prod-buy-header h2").text()
        let $salePrice = $("div.prod-sale-price span.total-price").text().replace(/[^0-9]/g,'')
        let $outOfStock = $('div.oos-label').text().trim()
        

        return {
            id: id,
            title: $prodName,
            salePrice: $salePrice,
            outOfStock: $outOfStock
        }
    })
    
}

export { checkProducts }

 코드는 모두 srv/index.js에 몰아넣어도 되는데, 한 파일에 코드량이 많아지면 읽기가 불편하더라구요, 그래서 그냥 파일을 나눈 것뿐입니다. 

 

 진행은 간단합니다.

1. "http://localhost:3000/check"라는 URL로 POST방식을 사용해서 id와 link 파라미터를 전달한다.

2. 전달된 link(url)에 있는 내용을 읽어와서 cheerio를 이용해 원하는 내용만 읽어낸다.

3. 읽어낸 내용을 return 한다(여기서는 frontend로 데이터를 보내준다).

 

cheerio 라이브러리는 jQuery를 써본 분들은 DOM에 접근하는 방식이 같아서 무난하게 사용할 수 있습니다. 그래도 처음 접하는 분들을 위해 설명을 붙여보자면

 

 제가 목이 말라서 방금 쿠팡에서 "물"을 검색해서 상품을 하나 골랐습니다. 이 상품의 가격에 접근하는 방법은 크롬 개발자 도구를 이용하면 더 쉽습니다. Elements 탭 가장 하단을 보시면 자기가 클릭한 내용이 어떤 트리로 구성되어있는지 쉽게 보여줍니다.

 

srv/apis/checkProducts.js

let $salePrice = $("div.prod-sale-price span.total-price").text().replace(/[^0-9]/g,'')

 이 부분만 떼어서 보겠습니다. 이 물의 가격에 접근할 때 span.total-price를 통해 접근 가능한데, 혹시 다른 곳에서 같은 태그와 클래스(span태그와 total-price클래스)를 사용할지도 모르니 더 정확하게 상위 태그와 클래스(div.prod-sale-price)를 추가했습니다.

 

 선택된 내용에서 텍스트만 추출(text())해서 따옴표("), 쉼표(,) 같은 녀석들은 정규표현식(replace(/[^0-9]/g,''))을 이용해서 걸러내 줍니다(정규표현식은 구글에서 검색하면 고수들이 알려주니 여기서는 외우거나 이해할 필요는 없습니다. 저도 몰라요).

 

 자 이렇게 구현은 끝났습니다. 정말이지 nodeJS는 정말 강력한 것 같습니다. 제가 들이는 업무의 시간이 1/10로 줄어들었거든요. 앞으로 확인해야 할 상품이 늘어날 걸 예상한다면 시간을 더 아끼는 셈이 되겠네요.

 

 여러분은 크롤링(스크래핑)을 이용해서 해결할 일이 있으신가요?

 

링크

① 웹 크롤링 해보기 #1 - NodeJS, cheerio (feat. VueJS)

 웹 크롤링 해보기 #2 - NodeJS, puppeteer


역시 코딩은 즐겁고, 결과물은 저를 행복하게 하는군요.
데스크탑 앱 만들기 마지막 포스팅


링크

① 데스크탑 앱 만들기 #1 - Vue CLI + Electron

② 데스크탑 앱 만들기 #2 - ExcelJS, SheetJS, js-xlsx, js-xlsx-style

③ 데스크탑 앱 만들기 #3 - ipcMain, ipcRenderer

④ 데스크탑 앱 만들기 #4 - ExcelJS, ipcRenderer, ipcMain, removeListener

 

 그럼 이번 포스팅에서는 ipc 통신을 이용해서 frontend(vue)와 backend(electron) 사이에서 데이터를 교환해보겠습니다. 그리고 그 데이터는 시스템에 저장되어있는 엑셀 파일을 ExcelJS로 변환해서 가공해볼게요.(코드에 문제가 있거나 더 좋은 방법 아시는 분은 친절하게 댓글 남겨주세요!)

 

 순서는 다음과 같습니다.

1. frontend에서 엑셀 파일 로드를 요청한다.

2. backend에서 엑셀 파일을 로드한다. ExcelJS 라이브러리를 이용해서 데이터를 가공한다. 가공한 데이터를 frontend로 보낸다.

3. frontend에서 데이터를 갖고 논다.

4. backend로 갖고 놀았던 데이터를 보내 저장할 것을 요청한다.

5. 받은 데이터를 엑셀에 덮어 씌운다.

6. 끝.

 

 하나씩 시작해보겠습니다.

1. frontend에서 엑셀파일 로드를 요청한다.

App.vue

let filePath = '/Users/username/excel/test.xlsx'

ipcRenderer.send('load-excel-files', filePath)

 이 코드는 mounted 또는 created에 넣어서 frontend page가 로드되면 바로 실행되게 했습니다. 그리고 filePath는 엑셀 파일이 있는 위치를 적어줍니다. 저는 맥을 사용하고 있으니 /Users가 시작이고, 윈도우 유저라면 C:\로 시작하겠지요. 그리고 send 메소드를 통해 backend로 파일 경로만 패스해줍니다.

 

2. backend에서 엑셀 파일을 로드한다. ExcelJS 라이브러리를 이용해서 데이터를 가공한다. 가공한 데이터를 frontend로 보낸다.

background.js

ipcMain.on('load-excel-files', async (event, filePath) => {
  const sheetData = []
  const workbook = new ExcelJS.Workbook()
  await workbook.xlsx.readFile(filePath)
  const worksheet = workbook.worksheets[0] // 첫번째 sheet 선택

  const option = { includeEmpty: true }

  await worksheet.eachRow(option, (row, rowNum) => {
    sheetData[rowNum] = []
    row.eachCell(option, (cell, cellNum) => {
      sheetData[rowNum][cellNum] = { value:cell.value, style:cell.style }
    })
  })

  event.reply('reply-excel-files', sheetData)
})

 backend에서 ipcMain.on 메소를 통해 데이터를 받습니다. 그리고 ExcelJS 라이브러리를 이용해서 각 셀을 순회합니다.

 

 eachRow는 각 행을 순회하는 메소드입니다. 첫 번째 줄부터 한 칸씩 아래 방향으로 순회합니다.

 eachCell은 오른쪽 방향으로 각 cell을 순회합니다.

 이제 값을 얻어옵니다. 저 같은 경우에는 cell값과 style만 필요하니까 두 가지만 받아왔어요.

 

 값을 얻어왔으면 다시 frontend로 넘겨줍니다. event 인자에 reply 메소드를 통하면 바로 보내는 것이 가능하다고 합니다.

 

App.vue

ipcRenderer.on('reply-excel-files', (event, sheetData) => {
	console.log(sheetData)
})

 위 코드는 created에 넣어주셔야 위 event.reply 메소드에 반응해서 frontend console창에 sheetData를 출력해줍니다.

 

 

3. frontend에서 데이터를 갖고 논다.

App.vue

function deepClone(obj) {
    if(obj === null || typeof obj !== 'object') {
        return obj;
    }
    const result = Array.isArray(obj) ? [] : {};
    for(let key of Object.keys(obj)) {
        result[key] = deepClone(obj[key])
    }
    return result;
}

myCell.style = await deepClone(mySheetData[7][2].style)
myCell.style.fill = {
	fgColor: {argb: "09B050"},
	pattern: "solid",
	type: "pattern"
}
myCell.value = "myCellValue"

  이제 데이터를 마음껏 가지고 놀면 됩니다.

 

 가지고 놀 때 실제로 해보면서 알게 된 점은 cell에 있는 value는 값이 잘 적용이 되는데 style은 잘 적용이 안되더라구요. 그래서 ExcelJS를 통해 데이터를 읽을 때 가지고 왔던 임의의 원본 cell을 deepClone 해서 style을 바꾸니 잘 되었습니다. 코드에 deepClone 코드도 추가해 놓았으니 참고하세요.(나중에 기회가 생기면 clone에 대한 내용도 쓰고 싶군요!)

 

 fgColor는 color HEX 값인데 앞에 "#"을 지워서 입력하면 잘 작동합니다.

 

4. backend로 갖고 놀았던 데이터를 보내 저장할 것을 요청한다.

App.vue

ipcRenderer.send('write-excel-files', filePath, sheetData)

 

 잘 가지고 놀았던 데이터 뭉치를 ipcRenderer.send 메소드를 통해 전달합니다. 어디에 저장할지도 전달해 주어야 하니 1번에서 사용되었던 filePath('/Users/username/excel/test.xlsx')를 인자로 전달하고, 데이터(sheetData)도 인자로 전달합니다.

 

5. 받은 데이터를 엑셀에 덮어 씌운다.

background.js

ipcMain.on('write-excel-files', async (event, filePath, sheetData) => {
  const workbook = new ExcelJS.Workbook()
  await workbook.xlsx.readFile(filePath)
  const worksheet = workbook.worksheets[0]

  for (let rowNum=1; rowNum<sheetData.length; rowNum++) {
    let row = worksheet.getRow(rowNum)
    for (let cellNum=1; cellNum<sheetData[rowNum].length; cellNum++) {
      let cell = row.getCell(cellNum)
      if ( cell ) {
        cell.value = sheetData[rowNum][cellNum].value
        cell.style = sheetData[rowNum][cellNum].style
      }
    }
  }

  workbook.xlsx.writeFile(filePath).then( ()=>{
    event.reply('reply-excel-files', sheetData)
  }).catch( (error) => {
    event.reply('reply-excel-files', error)
  })
})

 제가 선택한 방법은 기존의 엑셀 파일을 불러와서 row(행)와 cell을 순회하면서 저희가 만지작 거렸던 데이터 뭉치에 있는 value와 style값을 집어넣는 것이었습니다.

 

※ 주의 : 제가 쓰는 방법을 쓰게 되면 문제가 생길 수 있습니다. 예를 들어, frontend에서 데이터를 만지작 거릴 때 프로그래밍 오류로 인해 값이 전부 사라지게 되고, 확인 하지 않고 기존 파일에 덮어 씌우면 기존 데이터들이 다 날아가 버리는 신비를 확인할 수 있습니다.

두 가지를 기억해 주세요.

1. 엑셀 파일을 작업하기 전에 다른 곳에 파일을 복사해 놓는다. 또는 자동으로 복사해 놓는 코딩을 한다.

2. 만지작 거린 데이터는 데이터가 잘 살아있는지 확인하는 코드를 집어넣는다.

 

 잘 입력이 되면 다시 frontend로 reply 합니다. 1번에서 미리 만들어 놓은 코드가 있어서 또 작성할 필요는 없습니다. 1석 2조.

 

6. 끝.

 자 이렇게 되면 과정이 끝나게 됩니다. 그런데 frontend page가 로드될 때 한 번만 불러와져야 할 엑셀 파일이 page가 로드될 때마다 중첩이 되더라구요. 예를 들면 한번 열면 1, 두 번째 열면 2. 이런 식으로 로드되는 횟수만큼 통신이 이루어져서 고치는 방법을 찾게 되었습니다.

 

 위에 보면 2, 3번씩 엑셀 파일 로딩 확인이 되는 것을 볼 수 있습니다.(실제로 로딩을 3번 하는 건 아니고 'reply-excel-files'의 중첩) 프로그램을 완전히 다시 로드(Ctrl + R 또는 ⌘ + R)하면 다시 초기화되는데 라우팅을 설정해서 입장 퇴장을 반복하면 점점 더 쌓이게 됩니다. 이유는 페이지가 로드될 때마다 저희가 1번에서 설정해 놓은 ipcRenderer.on 메소드로 만들어놓은 listener가 중첩되는 현상 같아서 방법을 좀 찾아본 결과 수정하는 데 성공했습니다.

 

App.vue

const listener = ipcRenderer.rawListeners('reply-excel-files')
if  ( listener.length > 1)
	ipcRenderer.removeListener('reply-excel-files', listener[1])

 

 저 같은 경우에는 for문을 이용해서 엑셀 파일을 여러 개 호출하기 때문에 ipcRenderer.on 메소드를 이용하고 Listener가 늘어나면 삭제하는 방법을 사용했지만, 엑셀 파일을 한 개만 호출할 경우 ipcRenderer.on이 아닌 ipcRenderer.once 메소드를 이용하면 Listener가 중첩되지 않는 것을 확인했습니다.

 

 이렇게 프로그램 만들어 보았습니다. 엑셀 파일은 업무에서 많이 사용하고 여러분들도 반복적으로 하는 일이 있을 것이라고 생각합니다. 제 경험이 누군가에게 도움이 되길 바래요. 한번 프로그램을 만들어 놓으면 누구보다 빠르게 업무를 처리할 수 있으니(빠르게 퇴근한다고는 안 했습니다.) 아직 업무 중인 척 연기하시면서 여유로운 회사생활되시기를 바랄게요.

 


프로그램 만드는데 시간은 들어가지만 재밌군요.

 

생소해서 어려운. 하지만 의외로 쉬운.

 

너 거기있고, 나 여기있고.

 

 


링크

데스크탑 앱 만들기 #1 - Vue CLI + Electron

데스크탑 앱 만들기 #2 - ExcelJS, SheetJS, js-xlsx, js-xlsx-style

데스크탑 앱 만들기 #3 - ipcMain, ipcRenderer

④ 데스크탑 앱 만들기 #4 - ExcelJS, ipcRenderer, ipcMain, removeListener

 

 위 포스팅에서 Vue CLI를 이용해서 frontend(renderer)를 만들고, electron-builder를 이용해서 backend(Main)를 만들었습니다. 그리고 excel 파일을 읽는 라이브러리를 ExcelJS를 이용해서 로딩에 성공하였으니 이제 이 객체를 frontend로 보내줘야 데이터를 가지고 이것저것 만지작 거릴 수 있겠죠? 그 이후에 만지작 거린 객체를 다시 백엔드로 보내서 엑셀 파일에 저장해주면 제 프로젝트가 성공하게 됩니다.

 

 아, 그 전에 애초에 frontend에서 파일을 읽고 바로 수정하면 되지 않냐 생각도 했었는데 시스템 안에 있는 파일을 직접 읽고 쓰기 위해서는 일렉트론 같은 시스템 프로그램이 필요하더라구요. 시스템 프로그램이 없으면 프로젝트 로컬 폴더 안에 있는 내용만 접근 가능하고, 폴더 밖에 있는 파일들은 접근이 안되니 업로드하는 방식으로 밖에 없는 것 같았습니다. 애초에 기획이 자동화니까 일렉트론을 쓰기로 결정했었죠.


 그럼 이제 본격적으로 fontend와 backend의 데이터 교환 방법을 알아보겠습니다.

 

 일렉트론 공식 문서에 뭐라고 설명되어있는지 부터 확인하고 갈게요!

 

★ ipcRenderer - renderer 프로세스에서 main 프로세스로 비동기적인 통신을 합니다.

★ ipcMain - main 프로세스에서 renderer 프로세스들로 비동기적인 통신을 합니다.

 

 내용만 봐서는 뭔지 정확히 감이 오질 않습니다. 혹시 Javascript 좀 해보신 분들은 좀 익숙하실지도 모르겠습니다. 웹페이지에서 event emit, listen과 많이 닮아 있다고 생각이 들었거든요.

 

 그럼 공식 문서에 나와있는 테스트 코드를 확인해봅니다.

 

 다음 코드는 frontend에 있는 아무 파일에 넣으면 되는데 저는 App.vue 파일에 넣어서 테스트를 해보겠습니다.

// renderer 프로세스(웹 페이지)안에서
const { ipcRenderer } = require('electron')
console.log(ipcRenderer.sendSync('synchronous-message', 'ping')) // "pong"이 출력됩니다.

ipcRenderer.on('asynchronous-reply', (event, arg) => {
  console.log(arg) // "pong"이 출력됩니다.
})
ipcRenderer.send('asynchronous-message', 'ping')

 

 ipcRenderer 테스트 코드에서 사용된 메소드는 on, send, sendSync로 3가지입니다.

- on: backend에서 frontend로 통신을 시도하면 받아 주는 메소드

- send: frontend에서 backend로 통신을 보내는 메소드

- sendSync: send메소드는 비동기적으로 보내지고, 이 메소드는 동기적으로 작동한다(동기, 비동기는 여기서 설명하기엔 내용이 복잡해지므로 패스!).

 

 아래 코드가 backend 파일인 background.js에 들어갑니다.

// main 프로세스안에서
const { ipcMain } = require('electron')
ipcMain.on('asynchronous-message', (event, arg) => {
  console.log(arg) // "ping" 출력
  event.reply('asynchronous-reply', 'pong')
})

ipcMain.on('synchronous-message', (event, arg) => {
  console.log(arg) // "ping" 출력
  event.returnValue = 'pong'
})

 ipcMain 테스트 코드에서 사용된 메소드는 on, reply로 2가지입니다.

- on: frontend에서 background.js로 통신을 요청하면 받는 주는 메소드

- reply: on이 성공하면 frontend로 다시 통신을 넣어주는 메소드

 

 위의 테스트 코드를 실행했을 때의 결과는 다음과 같습니다.

 

▶︎ frontend(renderer) side

▶︎ backend(main) side

 

 frontend에서 pong 두 번, backend에서 ping 두번이 로그에 출력이 되었네요. 보내고 받고를 두번 했다는 뜻인데 코드를 한번 살펴볼게요.

 빨간색은 비동기(asychronous) 통신을 나타내고, 파란색은 동기(synchronous) 통신을 나타냅니다. 두 가지의 통신을 테스트하는 코드였네요. 그래서 frontend와 backend 콘솔에 pong이 2개, ping이 2개가 출력된 것입니다.

 

 자세히 좀 볼까요?

 

▶︎ 비동기 통신(분홍색, asychronous)

① frontend에서 ipcRenderer.send 메소드를 'asynchronous-message' 채널을 통해 'ping'이라는 데이터를 backend로 보냅니다.

② backend에서 ipcMain.on 메소드에서 정의한 'asynchronous-message' 채널을 통해서 인자를 받고 콘솔에 출력하네요.

그리고 바로 ipcMain.reply 메소드를 'asychronous-reply' 채널을 통해 'pong'이라는 데이터를 frontend로 보냅니다.

④ 마지막으로 frontend에서 ipcRenderer.on 메소드를 통해 정의된 'asychronous-reply' 채널을 통해 받아진 'pong'이라는 데이터는 콘솔에 출력되는 것으로 끝이 납니다.

 

▶︎ 동기 통신(파란색, sychronous)

① frontend에서 ipcRenderer.sendSync 메소드를 'synchronous-message' 채널을 통해 'ping'이라는 데이터를 backend로 보냅니다.

② backend에서 ipcMain.on 메소드에서 정의한 'synchronous-message' 채널을 통해서 인자를 받고 콘솔에 출력하네요.

③ 그리고 event의 returnValue를 'pong'으로 입력하면서 자동적으로 frontend로 데이터가 리턴됩니다.

④ 마지막으로 frontend로 ipcRenderer.sendSync의 리턴 값이 'pong'으로 전달되었고 바로 콘솔에 출력됩니다.

 

 

 어떤가요? 일렉트론의 frontend와 backend이 통신을 어떻게 하는지 같이 살펴보았는데 이해가 되셨으면 좋겠네요. 저는 테스트 코드를 해보면서 생각보다 쉽다는 생각을 했습니다. ipc라는 용어 자체가 헷갈릴 뿐이죠.

 

 그럼 다음 포스팅에서는 제 프로젝트에서 ExcelJS와 ipcMain, ipcRenderer를 이용해서 어떻게 구현하였는지, 그리고 Listner가 중첩돼서 중복적으로 통신 이벤트가 발생하는 현상을 수정하는 방법들을 적어보도록 하겠습니다.

 

 

※ 동기와 비동기의 차이점은 생활코딩님께서 영상으로 너무 잘 설명해주셔서 링크를 남깁니다. 영상은 개념 설명이고 아래 더보기에 강좌 링크는 남겨놓았으니 동기, 비동기가 궁금하신 분은 참고해주세요!

 

생활코딩 - 동기와 비동기의 개념

 


다음 : 데스크탑 앱 만들기 #4 - ExcelJS, ipcRenderer, ipcMain, removeListener

더보기 : 생활코딩 - nodejs에서의 동기, 비동기, 콜백

프로젝트가 끝나갈 무렵, 코딩은 참 즐겁다는 생각이 들었다.

 

+ Recent posts