Node.js에서 request를 통해 가져온 값을 화면에 뿌릴 때 문제가 생기는 경우 (async, await... 동기/비동기에 대한 간단한 이해)

Posted by , November 11, 2021
NodeJSTroubleshooting
Series ofNodeJS

Node.js에서 request를 사용하여...

현재 베타테스트 중인 내 프로젝트의 일부 기능을 Node.js로 변환하는 중이었다.
아직 Node를 학습하면서 붙이는 거라서 좀 익숙하지 않았다.

일단 진행하면서 겪은 문제의 포인트는 아래와 같다.

  1. 특정 사이트의 크롤링을 진행한 다음 해당 내역중에 필요한 데이터를 html 파싱 처리.
  2. 테스트 환경이 Node.js로 구축한 서버의 특정 url로 접근 시 1번 기능이 호출된다.
  3. 문제는 2번에 나온 url 호출을 하면 1번의 작업이 끝나고 그 결과를 json으로 뱉어야 하는데 {} 반환.

난 거의 Java로만 개발을 많이 했던 사람이라 (물론 python도 많이 썼지만 그건 좀 과거라 패스하고...) 순차적인 흐름에 익숙한 사람이다.
일단 코드로 보자.

//route에서 크롤링 처리 결과를 가져와서 json으로 응답 처리해주는 함수
router.get('',(req, res, next) => {
    res.json(crawling4Test());
});

function crawling4Test() {
  .....
  return someValue;
}

내 개념으로는 crawling4Test() 함수의 처리 결과가 끝나고,
그 반환 값을 res.json() 에 전달하여 Json 응답이 처리되는 것으로 이해하고 있었다.
근데 아니었다.
먼저 crawling4Test() 함수의 로직도 함께 보자.

function crawling4Test() {
    let targetUrlReq = {
        url:'https://target_url.com',
        method: 'GET',
        timeout: 5000,
    }

    let resultJson = {};

    request(targetUrlReq, function(err,res,body) {
            if (!err && res.statusCode === 200) {
                const enc = charset(res.headers, body)
                const i_result = iconv.decode(body, enc)
                const resultHtml = cheerio.load(i_result);

                const regex = /[^0-9]/g;
                let targetA = resultHtml('div.target_a_tag).text().replace(regex, '');
                let targetB = resultHtml('div.target_b_tag).text().replace(regex, '');

                //체크용
                console.log('targetA = ' + targetA)
                console.log('targetB = ' + targetB)

                resultJson['targetA'] = targetA;
                resultJson['targetB'] = targetB;

                resolve(resultJson);
            }
            else {
                console.log(`error${res.statusCode}`);
                resultJson['error'] = 'Some error';
                reject(resultJson);
            }
        });

    return resultJson;
}

위에서도 언급했지만 저것의 처리 결과는 {} 비어있는 딕셔너리 객체였다.
(뭐 Map, Key-value 다양한 이름이지만 본문에서는 딕셔너리라 표현)

근데 또 신기한 것은 crawling4Test() 함수 안에서 console.log 를 통해 로그를 띄우면 값은 잘 들어있다.
내가 원한건 router.get() 함수에서 crawling4Test() 함수의 처리를 기다리고 응답이 완료되면 res.json() 로 결과를 내보내는 동기적 처리 방식을 기대했던 것.

그럼 원인은?

그냥 함수들이 비 동기로 실행되었다.

이 표현이 맞는지 모르겠지만 무튼 문맥상으로는 저렇게 표현했다.
내가 원하는 결과물은 동기처럼 보이는 순차적 비동기 뭐 좀 편한 표현으로 하나의 흐름으로 동작하는 비동기 였다.

그래서 찾아보니 node에서는 비동기를 하나의 흐름으로 처리하기 위해서는 promise 그리고 await, async 라는게 필요했다.


그래서 promise, await, async 이것들은 대충 감이 오는데...

Node를 공부하는 사람이라면 주요 특징점이나 문제점(?) 같은걸 많이 들었을 것이다.
그 중 하나가 콜백지옥 이다.

이거는 하도 설명이 많이 나와서 난 패스하겠다.
궁금하신 분은 검색하면 다른 블로그나 유튜브에 아주 자세히 설명되어 있으니 그곳을 참고해주시길...

무튼 promiseawait, async 이거 3가지만 잘 조합하면 깔끔하게 처리할 수 있다.
(초창기 버전에서는 엄청 복잡했다는데..시간이 지나니 점점 편해지는건 덤...)

promise에 대해 설명글 쓰면 또 엄청 길어지니까...간단하게 말하자면..

“A promise is an object that may produce a single value some time in the future”
하나의 요청 처리가 끝날때까지 기다리지 않고 다른 요청을 동시에 처리할수 있는 방식 ()

promise 소개 문구는 저렇다. 즉 비동기를 하겠단 이야기인데 콜백의 지옥을 파훼하기 위한 해법이라 하는데 얘도 사실 보면 장황하지 않을 뿐 비슷하다.
그래서 await, async 이거 두 개랑 같이 쓰면 그 때는 어느정도 파훼법이 완성된다.

이번 문제도 같다.
그럼 이걸 어떻게 해결했는지 한번 알아보도록 하자


처리한 방법

먼저 변경된 crawling4Test() 함수를 보자.

function crawling4Test() {
    let targetUrlReq = {
        url:'https://target_url.com',
        method: 'GET',
        timeout: 5000,
    }

    return new Promise((resolve, reject) => {
        let resultJson = {};

        request(targetUrlReq, function(err,res,body) {
            if (!err && res.statusCode === 200) {
                const enc = charset(res.headers, body)
                const i_result = iconv.decode(body, enc)
                const resultHtml = cheerio.load(i_result);

                const regex = /[^0-9]/g;
                let targetA = resultHtml('div.target_a_tag).text().replace(regex, '');
                let targetB = resultHtml('div.target_b_tag).text().replace(regex, '');

                resultJson['targetA'] = targetA;
                resultJson['targetB'] = targetB;

                resolve(resultJson);
            }
            else {
                resultJson['error'] = 'Some error';
                reject(resultJson);
            }
        });
    });
}

일단 반환 값이 json 이 아닌 Promise로 바뀌었다.
그거 외에는 로직은 같지만 반환의 경우 resolve 와 reject 인데 이거는 어렵지 않다.

해당 값을 다시 또 가공하여 처리하는 함수를 호출해야 하는 경우 then() 으로 체이닝 해서 처리하면 되고, 예외가 생길 경우에도 마찬가지 이다.
이 글에서는 Promise에 대해 설명하는 공간은 아니기에 간략하게 여기까지만 알아보자.
(사실 너무 많은 자료가 많아서...다른 분들꺼 참고하자
<하단에 참고로 주렁주렁 달아 놓을 예정>)

이제는 그럼 저 함수를 호출하는 곳으로 가보자

router.get("", async (req, res, next) => {
  res.json(await crawling4Test())
})

그냥 단어 몇 개가 사이에 박힌거 빼곤 없다.
await 키워드는 promise를 반환하는 함수 앞에 써주고, async 키워드는 await 선언이 된 곳에 써주면 된다.
이것도 여기서는 이렇게만 설명한다.
좀 더 자세한 글은...아시죠? ㅎㅎ

그래서 결국 저렇게 처리한 상태로 실행해보면 드디어 원하는 결과가 잘 나오게 된다.


결론 및 여담

Node는 뭔가 자바에서 온 사람을 당황하게(나만 그럴수 있다) 만드는 게 많은듯...
promise, async, await 에 대해 자세하게 알고 있음 좋을 것 같다.

엄청 딥하게 알면 더 좋겠지만...
사실 구현하는데 바쁘니... 어떤 식으로 동작하는지...그리고 어떻게 사용하면 ㅈ 되는지, 어떻게 사용해야 하는지만 알면 될 것 같다.

원리까지 설명하세요 이러는 건 면접에서나 하는거겠지?
근데 사실 이런거 면접에서 딥하게 묻는 것도 문제가 있는듯..

위에 언급한 대로 어떻게 사용하는지 정확한 이해도 체크만 하면 되는데 이거 동작 방법을 화이트보드에 설명해보세요 한다?
그건 면접자에게 질문을 잘못 던진 케이스 같다.

그런 질문 보다는 이걸 어떻게 썼을 때 잘못된 경우 있었냐? 이걸 어케 대처해야 하냐?
이게 옳바른 질문 아닐까? ㅎㅎ

내가 예전에 3년전인가... 면접다닐때...특히 스프링 자바는 진짜 되도 않는 면접 질문 많이 받았다.
네이버랑 카카오 전화면접에서는 GC에 대해 다 설명해보고 java8의 GC가 뭐가 틀린지 설명해보랜다...
아니 트러블슈팅 성향이나 프로젝트에서 겪은 장애 처리나 어떻게 구현했냐 질문보단 저런걸로...

아마 떨어트리려고 한거 같다는 생각이 많이 든 면접...(아님 내 정신승리일 수도...)

그냥..다시 이런거 찾아보고 공부하다가 옛날 생각나서 주절거려봤다.

참고