목차

반응형

증상

nodejs의 sequilize로 ORM 설정하고 생성된 테이블에 데이터를 넣은 뒤 findall로 데이터를 조회하려고 해도 아무것도 조회가 안된다.

 

원인

sequlize는 기본적으로 설정한 테이블 이름에 s를 붙여서 쿼리를 생성한다.

(정말 라이브러리 제작자 오지랖이 너무너무 넓어서 생긴 이슈다. 니들만쓰냐?)

 

sequlize 관련 함수 실행시 테이블 정보에 s가 붙는지 확인해보자.

또한 조회부터 하지 말고 삽입부터 해보고 생성된 테이블에 s가 붙었는지 확인해보자.

 

해결방법

sequlize ORM 정의할때 위와 같이 freezeTableName:true 옵션값에 true를 주도록 하자.

그러면 원하는 테이블 이름과 정확히 일치하는 쿼리가 생성된다.

 

추가사항

만약 본인이 이 문제로 들어왔다면 추가로 아래 문제도 발생할 가능성이 높으니까 이것까지 보고가라

 

자꾸 테이블에 id 필드가 자동으로 생성됩니다.

ORM 생성시 특정 필드에 아래 옵션값을 넣어주면 pk역할을 하는 id 필드가 안생긴다.

primaryKey: true로 해결

아래 링크 참고

https://stackoverflow.com/questions/29233896/sequelize-table-without-column-id

 

자꾸 테이블에 `createdAt`, `updatedAt` 컬럼이 알아서 생성됩니다.

ORM에 정의시 아래 옵션 넣어주기

timestamps: false 로 해결

https://newbedev.com/sequelize-unknown-column-createdat-in-field-list

 

진짜 역대급 오지랖이다.

왜 본인들의 스타일을 강요해서 무조건 에러를 만들어 버리는걸까...?

 

 

반응형
반응형

1. 개요

비즈엠의 카카오톡 알림톡 확실히 엄청 좋은 기능이다. 별 다른 약관 동의 없이도 목적이 인정되면 카카오톡으로 보낼 수 있다는 것이 얼마나 좋은가?
근데 그건 알바아니고 개발자인 우리는 귀찮을 뿐
비즈엠 카카오톡 알림톡 전송 API를 개발하면서 정리할만한 부분을 정리해봤다.
다른 개발자들은 나보다는 빨리 원하는 결과에 도달하기를 바라며 오늘도 글을 싸 본다. 뿌직뿌직

특이사항이라면 보통 템플릿을 지원하면 request에 템플릿 id와 교체해줄 문구만 보내면 알아서 발송해주는 개념으로 생각했는데 그게 아니라 템플릿의 모든 정보(모든 문자, 기타 설정값)를 다 보내줘야 한다.
이 부분은 이해가 안 된다. 굳이...?
글고 뭔가 내가 특정 유저라는 것을 증명하는 API 키 같은 것도 없긴 하다. 근데 Send API 호출 난이도가 좀 높은 편이라서 필요 없는 것도 이해는 간다.

 

2. 필요한 정보

알림톡 전송 API를 호출하기 위해 필요한 최소한의 정보다.
전부 찾아오세요

 

비즈엠 로그인 ID

비즈엠 로그인할때 쓰는 로그인 ID

 

비즈엠 발신프로필키

발신 프로필 > 발신 프로필 목록 > 발신프로필키
전송하고자 하는 메시지의 발신프로필키 값과 메시지의 생성한 계정이 발신프로필이 동일해야 한다.

 

템플릿 ID

템플릿 > 템플릿 목록 > 보내고자 하는 메시지의 템플릿코드

 

메시지

저 메시지 내용을 1byte도 안 틀리고 똑같이 갖고 있어야 한다.

 

실제 받는 사람 전화번호

이건 그냥 본인 테스트용 전화번호로 해보자

 

버튼 정보

템플릿에 등록한 버튼 정보 전부 다 갖고 있어야 한다.

 

뭔 발송하는데 벌써부터 이렇게 복잡하냐...
일단 전부다 모아 왔으면 코드로 설명하기 전에 간단히 개념적으로 설명 들어가겠다.

 

비즈 엠 카카오톡 알림 톡 Send API의 호출하기 위한 정보는 아래와 같다.

let options = {
method:"POST",
"url":`https://alimtalk-api.bizmsg.kr/v2/sender/send`,
headers: {
"userid": "loginid",
"Content-Type": "application/json",
},
data: [{
        "message_type":"at","profile":"23456422626","phn":phone,
        "msg":tempMsg,
        "tmplId":join,
        "button1":

{
            "name": "마켓쿵 바로가기",
            "type": "WL",
            "url_mobile": "https://naver.com",
            "url_pc": "https://naver.com"
        }


    }]
}

비즈엠 로그인 ID는 헤더의 userid
이후 발송 요청에 대한 내용은 request body로 들어간다.(기본적으로 배열)

 

3. 코드

const templateInfo = {
    "join": {
        msgParamCount: 3,
        msgBody: "내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라",
        buttonInfo: {
            "name": "마켓쿵 바로가기",
            "type": "WL",
            "url_mobile": "https://naver.com",
            "url_pc": "https://naver.com"
        }
    },
    "payment":{msgParamCount:2, msgBody:"내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라",
        buttonInfo:{}
    },
    "login": {
        msgParamCount: 6,
        msgBody: "내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라",
        buttonInfo: {
            "name": "주문 내역 조회하기",
            "type": "WL",
            "url_mobile": "https://naver.com",
            "url_pc": "https://naver.com"
        }
    },
    "test": {
        msgParamCount: 5,
        msgBody: "내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라내용솰ㄹ라솰라",
        buttonInfo: {
            "name": "배송 조회하기",
            "type": "WL",
            "url_mobile": "https://naver.com",
            "url_pc": "https://naver.com"
        }
    }
}

exports.alrimtalkService = {
    sendAlrimtalkMessage(templateId, messageParams, phone) {
        return new Promise(async (resolve, reject) => {
            //1. 정보 전부 제대로 들어왔는지 확인
            if(!templateId || !messageParams || !phone)
            {
                console.log("[alimtalk] : sendKakaoTalkMessage 함수 호출시 인자 설정이 제대로 되지 않았습니다.")
                return resolve("[alimtalk] : sendKakaoTalkMessage 함수 호출시 인자 설정이 제대로 되지 않았습니다.")
            }

            //2. 템플릿 정보 있는지 확인
            if(!(templateId in templateInfo))
            {
                console.log(`[alimtalk] : 존재하지 않는 템플릿 정보 ${templateId}`)
                return resolve(`[alimtalk] : 존재하지 않는 템플릿 정보 ${templateId}`)
            }

            //3. 템플릿 정보의 메시지 인자 수와 함수 호출된 메시지 인자 개수가 같은지 확인
            if(templateInfo[templateId].msgParamCount !== messageParams.length)
            {
                console.log(`[alimtalk] : 메시지 인자 개수가 일치하지 않습니다. ${templateInfo[templateId].msgParamCount} : ${messageParams.length}`)
                return resolve(`[alimtalk] : 메시지 인자 개수가 일치하지 않습니다. ${templateInfo[templateId].msgParamCount} : ${messageParams.length}`)
            }

            //전송 바디 생성
            let body = buildRequestBody(templateId, messageParams, phone)

            let options = {
                method:"POST",
                "url":`https://alimtalk-api.bizmsg.kr/v2/sender/send`,
                headers: {
                    "userid": "633463466",
                    "Content-Type": "application/json",
                },
                data: body
            }

            //4. 전송 제대로 됐는지 확인
            return await axios(options).then(result =>{
                if(result.data[0].code !== "fail") resolve(result)
                else
                {
                    console.log(`[alimtalk] : ${result.data[0].message}`)
                    resolve(result.data[0].message)
                }
            }).catch(error => {
                console.log(`[alimtalk] : ${result.data[0].message}`)
                resolve(`[alimtalk] : 비즈엠 전송 실패 ${error}`)
            })
        });
    }
}


function buildRequestBody(templateId, messageParams, phone)
{
    let tempMsg = templateInfo[templateId].msgBody
    for(let i = 0;i < templateInfo[templateId].msgParamCount;i++)
        tempMsg = tempMsg.replace(/#{.+}/,`${messageParams[i]}`)

    return [{
        "message_type":"at","profile":"757545775753","phn":phone,
        "msg":tempMsg,
        "tmplId":templateId,
        "button1":templateInfo[templateId].buttonInfo
    }]
}

//실제 사용 코드
await alrimtalk.alrimtalkService.sendAlrimtalkMessage("test", [purchase.order.sender.username, purchase.productName, purchase.order.orderNo, purchase.order.receiver.address1, purchase.delivery.number], purchase.order.sender.phone)

코드의 buildRequestBody와 templateInfo에 대한 부분은 꼭 필요한 건 아니지만 최소한 이 정도 설계 없이는 개판일 것 같아서 설계한 거니까 적당히 이해해주세여

반응형
반응형

1. 개요

2. 사용방법

3. 주의사항

 

 

 

 

 

1. 개요

개발하다 보면 아마 가장 많이 마주치는 에러가 null이거나 undefiend인 값에서 무언가를 하려고 할 때 발생하는 null exception 에러일 것이다.

그래서 어린 시절에는 if문을 듬뿍 활용하여 null인 경우 아닌 경우 나눠서 처리하고 그랬는데 이걸 엄청 편하게 하는 방법이 있다.

처음 사용할 때는 좀 뭔가 위험하지 않을까 걱정도 되는데 잘 사용하다 보면 편리하며 문제가 없었다.

이번 포스트에서 다루고자 하는 기능의 키워드는 Optional Chaining라고 부른다.

 

2. 사용방법

공식 문서에서 떠왔다.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining

 

 

단순하다. ?를 문제가 생길 것 같은 구문에서 적절히 삽입해주면 된다.

myMap에 bar라는 프로퍼티가 없는 경우 일반적으로 myMap.get("bar").name을 하면 오류가 발생한다.

하지만 ?를 사용하여 myMap.get("bar")?.name를 호출하면 해당 값이 undefiend, null 인 시점에서 뒤의 구문을 진행하지 않고 종료한다.

 

 

조금 더 본격적으로 쓰면 위처럼 가능하다. 구문 사이사이에 물음표가 계속 들어가 있다.

 

 

사용법은 이 정도면 충분할 것이라고 생각한다.

 

 

3. 주의사항

node에서는 일반적으로 v14 이상부터 지원한다.

javascript의 문법이지만 node에서는 일반적으로 14버전부터 지원한다. 나도 이거 쓰려고 버전업했다.

 

많이 써도 되지만 잘 알고 쓰자.

a?.c?.b?.c?.d 이런식으로 습관성 백무빙마냥 애매해서 잘 모르겠으니 그냥 에러안나면 장땡이지 하고 막 쓰는 것은 아니라고 생각한다.

try catch를 쓸때 에러가 나는 부분 try catch 해놓고 아무런 표시를 안해놨다가 나중에 에러가 발생하는 것 조차 넘어가버리는 큰 상황이 되듯이 비어있으면 안되는 상황에서는 에러를 내 주는 것이 맞다.

에러를 회피하기보다는 의도적으로 비어있을 수 있는 값이라서 if를 쓰기 귀찮으니 ?를 사용하는 느낌이라고 해야할까...?

 

뒤 구문에 영향을 줄지 확인하자

?구문을 쓴다고 해서 만사 ok가 아니다. ? 연산자를 사용하여 당장 한줄의 라인에서는 문제없이 넘어갔지만 이번에 생기지 않는 에러로 인하여 바로 다음줄, 다다음줄, 또는 먼 뒤의 로직에서 해당 구문이 실행되지 않아서 발생하는 오류가 없는지도 파악해서 처리하자.

반응형
반응형

오류

axios를 한두번 요청하는 경우에는 문제없지만 몇천번의 요청을 하는 경우 한두번꼴로 request 이후 어떠한 결과값도 반환하지 않고 그대로 홀딩되버리는 경우가 발생한다.

당연히 timeout도 옵션값에 넣었는데 timeout 작동도 잘 하지만 그냥 그게 문제가 아니다. ㅇㅇ

정말 귀신이 곡할 노릇이라서 axios 라이브러리가 문제라고 생각하기도 쉽지 않았다.

여기까지 찾아 들어온 당신 꽤 하는거다.

보상으로 어렵게 얻어낸 해결 방법을 공유하도록 하겠다.

 

원인

원인은 잘 모르겠다. 아래에도 비슷한 상황이 있는데 흔한 케이스도 아니고 어쩌면 라이브러리 버전 문제일까 싶기도 한데 딱히 찾아봐도 뭐가 안나왔다.

https://github.com/axios/axios/issues/459

 

해결

해결방법은 axios의 timeout을 사용하지말고 자체적으로 타이머를 걸어서 cancel-token이라는 기능과 함께 해당 요청이 시간이 지나면 내가 직접 취소처리를 해버리는 것 이다.

nodejs axios cancel token 이라고 검색해서 찾아보면 사용 방법이 잘 나온다.

어떤 느낌인지 보고 싶으면 아래 내가 적용하기 전과 후 코드를 봐보자.

 

왼쪽이 before, 오른쪽이 after

좀 더러워지긴 했는데 이렇게 하니까 잘 된다.

반응형
반응형

1. 오류

nodejs 모듈 fileSystem의 rmdir 기능을 사용했다.

 

코드는 아래와 같다.

fileSystem.rmdir(`./images/${insertItem.option.forkHistoryId}/${insertItem.item.itemId}`, {recursive: true},()=>{})

 

그러면 아래와 같은 에러가 발생한다.

(node:498071) UnhandledPromiseRejectionWarning: TypeError [ERR_INVALID_CALLBACK]: Callback must be a function
    at makeCallback (fs.js:136:11)
    at Object.rmdir (fs.js:671:14)
    at router.get (/ServerCrwal.js:499:16)
    at Layer.handle [as handle_request] (/node_modules/express/lib/router/layer.js:95:5)
    at next (/node_modules/express/lib/router/route.js:137:13)
    at Route.dispatch (/node_modules/express/lib/router/route.js:112:3)
    at Layer.handle [as handle_request] (/node_modules/express/lib/router/layer.js:95:5)
    at /node_modules/express/lib/router/index.js:281:22
    at Function.process_params (/node_modules/express/lib/router/index.js:335:12)
    at next (/node_modules/express/lib/router/index.js:275:10)
(node:498071) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:498071) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

 

 

2. 원인

버전에 맞지 않는 구문을 사용해서 그렇다.

rmdir을 recursive 옵션 없이 사용하면 잘 작동하는데 문제가 발생한 환경의 node 버전은 10.x 버전이었다. 그리고 인터넷에 올라와있는 예시는 거의 12버전 이상에서 작동하는 코드들이다.

왼쪽이 10.x 구문, 오른쪽이 12.x 구문이다.

 

문제가 발생했다면 해당 환경해서 node -v로 버전을 확인해보고 12버전이상인지 확인해보자.

 

3. 해결방법

코드상의 문제는 없기 때문에 node 버전을 업그레이드 했다.

 

우분투 명령어

curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -

sudo apt-get install -y nodejs

 

데비안 명령어

curl -sL https://deb.nodesource.com/setup_12.x | bash -

apt-get install -y nodejs

 

버전 업글하고 하면 잘된다.

반응형
반응형

1. 개요

2. 사용하는 라이브러리, 개념 설명

3. 코드

4. 그 외

 

 

 

1. 개요

이전 게시물에서는 파일을 업로드 하는 경우에 대해서 다뤘는데 이번에는 다운로드하여 저장하는 부분을 살펴보도록 하겠다.

마찬가지로 axios를 통해서 처리한다.

 

2. 사용하는 라이브러리, 개념 설명

사용하는 라이브러리

axios 네트워크 통신

filesystem 파일에 대하여 관리할 수 있도록 도와주는 라이브러리 이미 fs로 쓰고 있을 가능성이 높음.

 

마찬가지로 코드로 넘어가기 전에 간단히 어떤 플로우를 통하여 업로드 하는지 알아보자.

업로드보다 조금 더 쉽다.

 

1. axios에서 원하는 파일 데이터를 stream 형태로 요청

2. 결과값을 writestream으로 저장

 

3. 코드

//1. axios에서 원하는 파일 데이터를 stream 형태로 요청
AxiosNetworkHelper.GetRequest({
    url: 'https://targetsite.com/image.jpg',
    responseType: 'stream',
}).then((res) => {
	//폴더 recursive로 생성 요청
    if (!fileSystem.existsSync(`./images/savefolder`))
        fileSystem.mkdirSync(`./images/savefolder`,{recursive: true})
        
 	//파일 이름 확장자명 따라서 정해주기
    let fileName = `filename.${res.headers['content-type'].split('/')[1]}`

	//2. 결과값을 writestream으로 저장
    const writer = fileSystem.createWriteStream(`./images/savefolder/${fileName }`);
    res.pipe(writer)

    writer.on('error', (error) => {console.log(`이미지 다운로드 스트림 생성 실패 ${error}`)})
    writer.on('close', () => {console.log('이미지 다운로드 성공')})
}).catch(error => {LogManager.getInstance().AddLocalLogData(forkHistoryId, `이미지 다운로드 요청 실패 ${error}`)})

 

마찬가지로 내용이 쉽기 때문에 자세한 설명은 생략하도록 하겠다.

다만 파일 확장자를 따로 정해주는 부분은 이해가 안갈수도 있는데

해당 res.headers 부분을 찍어보면 아래와 같이 나온다.

 

확장자 정보는 url을 통해서 이미 알고 있는데 왜 다시 정보를 이런식을 받아오냐 묻는다면... 가끔 확장자 정보가 없는 경우가 있다. 그런 경우 위와 같이 확장자를 그때그때 받아와서 저장하는 것 이다.

 

4. 그 외

마찬가지로 현재 게시물의 예제는 단일 이미지 다운로드에 대한 예제다.

다음에는 여러개의 이미지를 동시에 다운로드 하는 예제를 올리려고 한다.

 

nodejs axios readstream 파일 업로드

nodejs axios writestream 파일 다운로드

 

반응형
반응형

1. 개요

2. 사용하는 라이브러리, 개념 설명

3. 코드

4. 그 외

 

 

 

1. 개요

노드 서버 백엔드상에서 파일을 주고 받는 경우가 있다.

그런 경우 axios나 request를 통해 주고받는데 생각보다 예제 코드가 많지 않아서 지금 사용중인 코드를 올리도록 하겠다.

reqeust로도 가능하지만 요청 실패시 catch 부분에 대한 설정을 못하는 것 같아서(찾아봐도 안나오더라) axios로 갈아탔다.

 

2. 사용하는 라이브러리, 개념 설명

사용하는 라이브러리

axios 네트워크 통신

form-data axios의 네트워크 요청 시 파일 업로드같이 특수한 형태의 요청을 설정하기 위해서 필요하다.

filesystem 파일에 대하여 관리할 수 있도록 도와주는 라이브러리 이미 fs로 쓰고 있을 가능성이 높음.

 

3가지 라이브러리를 설치하고 나면 파일 업로드에 대한 개발이 가능하다.

코드로 넘어가기 전에 간단히 어떤 플로우를 통하여 업로드 하는지 알아보자

 

1. formdata 변수 생성

2. formdata에 파일 stream 데이터 주입

3. axios에 생성해둔 formdata로 요청

 

3. 코드

//1. formdata 변수 생성
const formData = new FormData();

//2. formdata에 파일 stream 데이터 주입
formData.append("promisedParamName", fileSystem.createReadStream(`target.jpg`));

const requestConfig = {
    url: "https://mytargetsite.com/api/file/photoinfra/uploads",
    headers: {
        'Content-Type':'multipart/form-data; boundary=' + formData.getBoundary()
    },
    data: formData
}

//3. axios에 생성해둔 formdata로 요청
AxiosNetworkHelper.PostRequest(requestConfig).then(res =>{
    if(res.status === 200) console.log("성공")
    else console.log(`요청 실패 ${res.status}`)
}).catch(error => {console.log(`전송 실패 ${error}`)})

 

자세한 설명은 하지않겠다.

다만 저기서 formData.getBoundary()가 어떤 역할을 하는지 알려주겠다.

웹 request가 날아갈때 multipart의 경우 boundary라고 해서 데이터간의 구획을 나누는 역할을 해주는 표지판 역학을 하는 코드가 필요한데 그 정보를 axios의 요청에 넣어주는 것 이다.(이해 안가면 아래 웹 리퀘스트 참고)

Accept: application/json, text/plain, */*
Content-Type: application/x-www-form-urlencoded
User-Agent: axios/0.21.1
Transfer-Encoding: chunked


Content-Type: multipart/form-data; boundary=--------------------------637277032015795830863476
content-length: 67576

 

4. 그 외

현재 게시물의 예제는 단일 이미지 업로드에 대한 예제다.

근데 살다보면 단일 이미지 업로드가 아닌 2개 이상의 이미지 업로드를 해야 할 필요가 있다.

다음은 그런 경우에 대한 예제 코드를 올리도록 하겠다.

 

nodejs axios readstream 파일 업로드

nodejs axios writestream 파일 다운로드

반응형
반응형

오류

Vue에서 Swiper를 사용할 경우 인터넷 익스플로러에서 작동 안하며 콘솔에서 아래와 같이 에러 메시지를 볼 수있다.

"개체가 'isNaN' 속성이나 메서드를 지원하지 않습니다."

 

원인

IE에서는 ES6 문법에 속하는 isNan이나 entries를 사용할 수 없다.

 

 

해결

vue.config.js 파일의 transpileDependencies에 dom7, swiper를 추가해준다.

 

아래 2개 링크는 참고한 사이트다.

github.com/surmon-china/vue-awesome-swiper/issues/665

sjquant.tistory.com/38

 

반응형
반응형

ejs를 설치했는데도 안되는 사람에게 해당되는 글이다.

 

정상적으로 npm install ejs를 했고

app.set('view engine', 'ejs')도 했는데

router단에서 res.render시 아래 오류가 발생한다.

 

Error: Cannot find module 'ejs'
Require stack:
- C:\Users\user\Desktop\workspace\node_modules\express\lib\view.js
- C:\Users\user\Desktop\workspace\node_modules\express\lib\application.js
- C:\Users\user\Desktop\workspace\node_modules\express\lib\express.js
- C:\Users\user\Desktop\workspace\node_modules\express\index.js
- C:\Users\user\Desktop\workspace\ForkCraneServer\app.js
   at Function.Module._resolveFilename (internal/modules/cjs/loader.js:980:15)
   at Function.Module._load (internal/modules/cjs/loader.js:862:27)
   at Module.require (internal/modules/cjs/loader.js:1042:19)
   at require (internal/modules/cjs/helpers.js:77:18)
   at new View (C:\Users\user\Desktop\workspace\node_modules\express\lib\view.js:81:14)
   at Function.render (C:\Users\user\Desktop\workspace\node_modules\express\lib\application.js:570:12)
   at ServerResponse.render (C:\Users\user\Desktop\workspace\node_modules\express\lib\response.js:1012:7)
   at C:\Users\user\Desktop\workspace\ForkCraneServer\app.js:39:9
   at Layer.handle [as handle_request] (C:\Users\user\Desktop\workspace\node_modules\express\lib\router\layer.js:95:5)
   at next (C:\Users\user\Desktop\workspace\node_modules\express\lib\router\route.js:137:13)

 

아래 오류를 자세히 보면 알겠지만 현재 내 프로젝트 경로가 제대로 지정되지 않았다.

따라서 프로젝트 경로를 지정해줘야한다.

 

아래 코드를 통해서 ejs 모듈의 참조 경로를 지정해주자.

app.engine('ejs', require('ejs').__express)

반응형
반응형

1. 개요

2. 메모리 누수란?

3. 메모리 누수 확인

4. 노드 크롬 디버거

5. 누수 발생 확인

 

 

 

 

수많은 글들이 노드의 메모리 누수에 대한 글들을 설명하고 해결방법, 대안을 포스팅했지만 정말 겉만 번지르르하고 도움이 안 됐다.

사실 좀 화가 났다. 왜냐면 나는 개발을 다 해놓고 앱을 돌려보니까 메모리 누수가 발생했는데 어디서 누수가 발생했는지 감도 안 잡히고 찾아보면 대부분의 게시물이 '메모리 누수는 스택과 힙 영역에서 머시기머시기 -> 정말 간단한 테스트 코드로 메모리 할당된 거 보여주면서 요런 식으로 메모리 누수가 발생하고 이렇게 하면 안 됩니다.~~' 이렇게 끝난다. 뭐 어따쓰라고 ㅡㅡ

그래서 직접 부딪혀보면서 메모리 누수를 찾고 해결해내었다. 결론부터 말하자면 개발을 엄청 잘한다고 메모리 누수를 피해 갈 수 있는 것이 아니다. 왜냐면 정말 많은 사람들이 사용하는 라이브러리에서도 엄청난 메모리 누수가 발생하였기 때문이다.

즉 당신은 메모리 누수가 발생하였으면 직접 덤프를 까보며 분석하고 해결하는 능력을 길러야 한다.

앞으로 포스팅할 글들은 실제 개발하다가 메모리 누수가 발생하였고 누수에 대한 확인, 해결방법을 제시하도록 하겠다.

꽤나 가치 있는 게시물이 될 것이라고 장담한다.

 

2. 메모리 누수란?

메모리 누수에 대한 간단한 설명을 하겠다.

 

당신은 온라인 게임을 만들었다. 한 명의 유저가 접속하면 유저에 대한 객체를 생성하는데 1MB가 필요하다.

 

 

축하한다. 당신이 만든 온라인 게임에 유저 100명이 들어왔다. 100MB의 메모리를 할당받았다.

 

 

안타깝게도 당신의 게임이 너무 빨리 질려버린 탓에 50명의 유저가 종료를 해버렸다.

그래서 50개의 유저 데이터를 파괴하고 50MB의 메모리를 다시 돌려받아야 한다.

GC는 이러한 과정 속에서 사용이 완료되고 다시 사용할 수 있도록 메모리를 수거하고 다시 사용 가능하도록 관리한다.

 

여기서 문제가 발생한다. 정체불명의 이유로 인해서 유저 50명이 접속을 종료해도 GC가 판단하기에 50명의 데이터를 담 고 있던 메모리가 아직 사용이 완료됐다고 판단하지 않아서 메모리를 수거하지 않고 계속 남아있는 것이다.

 

 

이것이 계속 반복되면 결국 앱은 무한대로 메모리를 점유하게 될 것이고 언젠가 앱이 강제로 종료될 것이다.

 

메모리 할당 -> 사용 끝 -> GC가 수거 못함 -> 아무도 사용 안 하지만 계속 할당된 채로 남아있음 -> 반복 -> 서버 폭발

 

3. 메모리 누수 확인

그러면 실제 개발하다가 메모리 누수가 발생하면 어떻게 되는지 확인해보자

일단 먼저 본인의 앱에서 메모리 누수가 발생한다는 것을 인지해야 한다.

왜냐하면 당신의 앱에서도 충분히 메모리 누수가 발생하고 있지만 그 정도가 

나는 메모리 누수에 대한 개념이 부족한 때라서 메모리 누수를 인지하는데 상당히 오래 걸렸다.

 

작업 관리자에서 Node가 차지하는 메모리를 보면 2000mb인 것을 볼 수 있다. 말도 안 되는 상황이지 않는가?

 

 

4. 노드 크롬 디버거

먼저 노드의 메모리 누수를 확인하기 위해서는 노드 크롬 디버거를 사용해야 한다.

노드 크롬 디버거를 사용하는 방법은 다음과 같다.

 

node --inspect 시작 파일

 

그러면 위와 같은 메시지가 추가로 나온다. 포트가 9229여야 하는데 꼭 9229일 필요는 없지만 구글 크롬에서 기본값으로 부착되는 포트가 9229라서 편하다

 

구글 크롬으로 들어간다.

 

URL에 chrome://inspect로 접근한다.

 

위와 같이 하단에 구동 중인 서버가 보인다. inspect를 눌러서 디버거를 켠다.

 

Memory > Profiles의 메모리 부분을 보면 현재 애플리케이션이 사용 중인 메모리를 실시간으로 볼 수 있다.

저 상태에서 메모리 부하를 일으키는 작업을 실행해보자 메모리가 하늘 높은 줄 모르고 계속 치솟는다.

 

그러다가 앱이 멈추면서 특정 부분에서 break를 한다. 우측을 보면 Paused before potential out-of-memory crash라고 나와있다. 메모리 부하가 심해서 임종 직전에 멈춘 것이라고 한다.

 

 

5. 누수 발생 확인

메모리 누수가 발생하는 원인을 확인해보는 여러 가지 방법을 설명하도록 하겠다.

 

메모리 스냅샷을 이용하여 부하를 일으키는 메모리는 어떤 종류인지 확인해보겠다.

메모리 부하를 일으킨 다음에 메모리 스냅을 떠서 용량이 많이 차지하는 덩어리를 한번 까보는 것이다.

 

일단 아무것도 하지 않은 위 상태에서 스냅샷을 한번 찍는다.

 

 

이전 단계와 마찬가지로 부하를 일으켜보고 어느 정도 됐다 싶으면 스냅을 한번 더 찍는다.

 

두 개의 스냅이 저장됐다.

 

각 스냅의 Statics를 확인해보자.

 

좌측은 정상적인 어플리케이션 메모리의 분포 상황이고 우측은 딱봐도 이상하리만큼 Strings로 꽉찬 메모리 분포 상황이다. 이정도면 엄청나게 많은 string을 만들어놓고 gc 처리를 못해서 메모리 부하가 생긴다고 킹리적 갓심이 가능한 상황이다. ㅇㅈ?ㅇㅈ

 

 

이번에는 저 많은 string이 어떤 부분에서 발생하는지 확인해보도록 하자.

 

앱을 다시 시작하고 부하를 주지 않은 상태에서 Allocation sampling 을 선택하고 Start를 누르고 어느정도 부하가 되면 Stop을 눌러서 결과를 살펴보자.

Allocation sampling은 메모리 부하를 주는 자바스크립트의 메쏘드를 살펴보는 기능이다.

 

Tree형식으로 확인해보자

 

혼자서 무려 메모리 사이즈의 99.99%를 차지하는 불순한 녀석이 보인다. 저 녀석의 하위 트리를 따라내려가보면서 쭈욱 살펴보도록 하자

 

jsonparse라는 내가 설치한 라이브러리가 해당 문제를 일으키고 있으며 해당 라이브러리의 문제를 일으키는 메쏘드들을 확인할 수 있다.

 

그러면 마지막으로 메모리 누수가 발생하는데 어떤 형태의 데이터가 쌓이는지 그 값들을 직접 확인해보자

 

다시한번더 어플리케이션의 초기상태, 부하상태의 스냅샷을 각각 찍어보자

 

All Obejcts를 두 스냅샷 사이이로 해주고 Summary를 Comparison으로 해주자

 

 

그러면 두 스냅샷 사이에서 생성된 데이터를 볼 수 있다.

 

Alloc.Size로 내림차순 정렬을 하고 상단의 트리를 열어보자

 

그러면 어 저거 내가 생성한 데이터인데 왜 저기있지? 싶은것이 있으면 원인을 찾은 것이다.

분명 저 데이터는 처리되고 말소되어서 GC가 수거해가야하는데 수거하지 못하게끔 어딘가에 계속 맞물려있는 것이다.

이렇게 해서 원인을 찾아봤고 다음은 해결하는 방법에 대해서 작성해보겠다.

 

 

Nodejs - 메모리 JavaScript heap out of memory

Nodejs - 메모리 할당 사이즈 변경

Nodejs - 메모리 누수 확인

반응형