본 포스트에서는 Front-end는 리액트, Back-end는 Express 기반의 Node.js를 사용하여 하나의 프로젝트로 간단한 Application을 구성해 보겠습니다. 구성할 Application은 이전 포스트에서 진행했던 네이버 API 조회 Application을 재사용 하겠습니다.

 

 

React + Express + TypeScript 프로젝트 구성하기

 

이전 포스트에서는 express 서버에서 개발한 API에서 사용하는 데이터와 동일한 형태로 uiModule에도 /src/interface/apidata.interface.ts 파일에 데이터를 선언했습니다. 이번 포스트에서는 해당 데이터 타입을 사용하는 API 인터페이스 부분을 작성해 보겠습니다.

 

/src/component/listview.component.tsx 파일을 생성하고 API 데이터를 저장할 state를 하나 선언합니다. articles 상태에는 API 본문을 저장할 예정이므로 IRespData 형태로 선언합니다.

const ListView = () => {
    const [articles, setArticles] = useState<IRespData | null>(null);
}

export default ListView;

 

목록에 필요한 데이터는 로컬 서버에 Query 해야 하므로 fetch API를 사용해서 서버 API를 Query 하는 apiGet 함수를 하나 정의합니다.

const apiGet = async(type: string, param: string) => {
    const apiUrl: string = '/search/' + type + '/' + param;
    await fetch(apiUrl, {
        method: 'GET',
        headers: {
            'Content-Type': 'application/json'
        }
    })
    .then((resp: Response) => resp.json())
    .then((resp: IHttpResp) => {
        if (resp.code === '00') {
            setArticles(resp.data as IRespData);
            console.log(resp.data);
        } else {
            //handle error
        }
    });
};

 

Naver API는 서버 API에서 호출하므로 UI에서는 /search/{type}/{param} 형식으로 서버 API를 호출합니다.  도서 검색을 React 라는 파라미터로 검색을 하려면 /search/book/React 로 검색할 수 있습니다. apiGet 함수는 useEffect에서 호출하여 화면이 Loading 될 때 조회하도록 합니다.

useEffect(() => {
    apiGet('book', 'React');
}, []);

 

apiGet 함수를 호출할 때 React 관련 도서를 검색할 수 있도록 위와 같이 useEffect Hook을 작성합니다. 이번 포스트에서는 console.log로 resp.data가 올바르게 조회되는지 까지만 확인해 보겠습니다. 그러므로 UI는 별도로 작성하지 않고 <></> 만 return 하도록 합니다. 여기까지 작성한 listview.component.tsx 프로그램의 전체 소스코드는 아래와 같습니다.

import { useEffect, useState } from "react";
import { IHttpResp, IRespData } from "../interface/apidata.interface";

const ListView = () => {
    const [articles, setArticles] = useState<IRespData | null>(null);

    const apiGet = async(type: string, param: string) => {
        const apiUrl: string = '/search/' + type + '/' + param;
        await fetch(apiUrl, {
            method: 'GET',
            headers: {
                'Content-Type': 'application/json'
            }
        })
        .then((resp: Response) => resp.json())
        .then((resp: IHttpResp) => {
            if (resp.code === '00') {
                setArticles(resp.data as IRespData);
                console.log(resp.data);
            } else {
                throw new Error('error');
            }
        });
    };

    useEffect(() => {
        apiGet('book', 'React');
    }, []);

    return (
        <></>
    );
};

export default ListView;

 

다음은 /src/app.tsx 프로그램을 아래와 같이 하나 작성하여 ListView 컴포넌트를 추가합니다.

import ListView from "./component/listview.component";

const App = () => {
    return (
        <ListView />
    )
};

export default App;

 

/src/main.tsx 소스코드에서는 <App /> 컴포넌트를 렌더링 하도록 아래와 같이 변경합니다.

import ReactDOM from 'react-dom';
import App from './app';

ReactDOM.render(<App />, document.getElementById('app'));

 

이제 서버와 인터페이스하여 수신한 데이터를 Logging 하는 부분까지는 준비를 마쳤습니다. 이제 타입스크립트를 컴파일하고 생성한 js 파일을 번들링하여 build.js를 생성합니다.

D:\workspace\expressServer\uiModule> tsc
D:\workspace\expressServer\uiModule> npm run build

 

/public/build.js 가 재생성 되었음을 확인하고 express 서버를 구동합니다.

D:\workspace\expressServer> yarn start

 

이제 브라우저로 http://localhost:8001 에 접속하면 아래와 같은 콘솔 로그를 확인할 수 있습니다.

인터페이스 결과 로그

 

아직 UI는 하나도 만들지 않았지만 이번 시리즈의 목적은 달성했습니다. 이번 시리즈의 목적은 아래 그림과 같았습니다.

서버 to 서버 인터페이스

 

NAVER API를 브라우저에서 직접 호출하지 않고 로컬에 express 서버를 하나 띄운 다음, express 서버에서 Naver API를 호출하여 중계하도록 Front-end, Back-end 프로그램을 작성해 보았습니다.

 

이후 UI를 만들어 보는 과정은 [React 기초 (List - TypeScript)] 시리즈의 "06 - ListView UI 구성" 포스트와 거의 동일한 내용이 될 것 같아서 작성하지 않겠습니다. 계속 진행해 보실 분들은 아래 링크를 참고하시면 됩니다.

React 기초 (목록 - TypeScript) 06 - ListView UI 구성 :: 즐거운인생 (tistory.com)

 

React 기초 (목록 - TypeScript) 06 - ListView UI 구성

본 포스트에서는 리액트와 타입스크립트를 이용하여 네이버 API로 데이터를 검색해서 목록 화면을 구성해 보겠습니다. 저번 포스트에서는 Naver API를 호출하여 응답 데이터를 확인해 보고, 응답

redballs.tistory.com

 

300x250

이 포스트는 JavaScript에서 객체나 배열 요소를 연결하기 위해 사용하는 방법에 대해 알아보겠습니다. jQuery에서 사용하는 extend 함수, ES6+ 에서 제공하는 Object.assign 및 Spread expression(전개 구문)을 확인해 보겠습니다.

 

jQuery.extend

Internet Explorer를 지원해야 하는 애플리케이션을 개발할 때는 Object를 결합해야 할 요건이 있을 경우 대부분 jQuery.extend 함수를 사용했습니다. 가장 기본적인 사용법은 아래와 같습니다.

const obj1 = {
    korean: 97,
    english: 100
};

const obj2 = {
    math: 67
}

$.extend(obj1, obj2);
console.log(obj1);

 

위와 같이 작성하면 obj1 Object에 obj2 Object를 병합하여 아래와 같은 결과가 표시됩니다.

{korean: 97, english: 100, math: 67}

 

아래와 같이 Object 아래 Object가 있을 경우에는 결과가 좀 다릅니다.

const obj1 = {
    korean: {
        step1: 97,
        step2: 100,
    },
    english: 100
};

const obj2 = {
    korean: {
        step1: 60
    },
    math: 67
};

$.extend(obj1, obj2);
console.log(obj1);

 

위 소스코드의 실행결과는 아래와 같습니다.

{korean: {step1: 60}, english: 100, math: 67}

 

Object 내에 Object의 경우에는 병합되지 않고, 나중 Object 기준으로 overwrite 됩니다. 하여 Object 내의 Object까지 병합을 하기 위해서는 아래와 같이 작성합니다.

$.extend(true, obj1, obj2);
console.log(obj1);

 

그러면 결과는 아래와 같이 step1은 overwrite 되지만 step2는 obj1 기준으로 병합되어 있습니다.

{korean: {step1: 60, step2: 100}, english: 100, math: 67}

 

보통 jQuery로 extend 함수는 기존 Object로부터 Object를 하나 복사하여 사용할 때 많이 사용합니다. Object 아래 자기 자신을 복제하는 함수를 하나 생성할 때 아래와 같이 작성합니다.

clone: function() {
    const me = this;
    return $.extend(true, {}, me);
}

 

jQuery.extend 함수에 대한 상세 설명은 아래 링크를 참조하시면 됩니다.

https://api.jquery.com/jquery.extend/

 

jQuery.extend() | jQuery API Documentation

Description: Merge the contents of two or more objects together into the first object. When two or more object arguments are supplied to $.extend(), properties from all of the objects are added to the target object. Arguments that are null or undefined are

api.jquery.com

 

 

Object.assign

ES6부터는 Object.assign 이라는 함수를 제공합니다. Internet Explorer에서는 지원하지 않습니다. Object.assign 함수의 사용 형식은 아래와 같습니다.

Object.assign(target, ...source)

 

target Object에 source Object들의 전개 가능한 모든 속성들을 복사해서 붙여 넣습니다. $.extend에서 사용했던 두 번째 예제로 확인해 보겠습니다.

const obj1 = {
    korean: {
        step1: 97,
        step2: 100,
    },
    english: 100
};

const obj2 = {
    korean: {
        step1: 60
    },
    math: 67
};

const merged = Object.assign(obj1, obj2);
console.log(obj1);
console.log(merged);

 

속성에 대한 복사이므로 결과는 아래와 같습니다. 내부 Object 까지는 복사하지 않으므로 이를 "얕은 복사"라고 합니다.

//obj1
{korean: {step1: 60}, english: 100, math: 67}

//merged
{korean: {step1: 60}, english: 100, math: 67}

 

$.extend에 recursive 옵션을 true로 준 것처럼 간단하게 "깊은 복사"를 수행하는 방법은 자체적으로는 없습니다. 일반적으로 for ~ in Loop을 사용하여 recursive 하게 프로그램을 짜서 복사하거나 lodash 라이브러리를 사용합니다.

 

Object.assign에 대한 상세 설명은 아래 MDN 링크를 참고할 수 있습니다.

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/assign

 

Object.assign() - JavaScript | MDN

Object.assign() 메서드는 출처 객체들의 모든 열거 가능한 자체 속성을 복사해 대상 객체에 붙여넣습니다. 그 후 대상 객체를 반환합니다.

developer.mozilla.org

 

Spread Expression (전개 구문)

전개 구문 역시 ES6부터 제공하며 Internet Explorer에서는 지원하지 않습니다. Spread 구문은 사용이 편리하여 제법 유용하게 사용할 수 있습니다. 다시 jQuery 두 번째 예제로 확인해 보겠습니다.

const obj1 = {
    korean: {
        step1: 97,
        step2: 100,
    },
    english: 100
};

const obj2 = {
    korean: {
        step1: 60
    },
    math: 67
};

const merged = {...obj1, ...obj2};
console.log(obj1);
console.log(merged);

 

Spread 구문은 Object.assign 과는 달리 obj1으로는 데이터를 병합하지 않습니다. obj1과 obj2는 원래 데이터대로 두고 병합한 데이터를 return 합니다. 결과는 아래와 같습니다.

//obj1
{korean: {step1: 97, step2: 100}, english: 100, math: 67}

//merged
{korean: {step1: 60}, english: 100, math: 67}

 

Object.assign과 마찬가지로 "깊은 복사"는 별도의 라이브러리 사용하거나 프로그램을 작성하여 해결해야 합니다. Spread 구문은 배열을 결합하는 경우에 손쉽게 사용할 수 있습니다.

const a = [1, 3, 4];
const b = [1, 3, 6];

const c = [...a, ...b];
console.log(c);

 

실행 결과는 아래와 같습니다.

[1, 3, 4, 1, 3, 6]

 

Spread Expression은 React나 Vue 등에서 상태를 관리할 때 간편한 구문을 제공합니다. Redux나 Context API 등을 사용하여 전역 상태를 관리할 경우 기존 상태에 변경된 상태 값만 갱신하기 위해서 아래와 같이 작성할 수 있습니다.

switch (action.type) {
    case 'SET_IS_VISIBLE':
        return {
            ...state,
            isVisible: action.isVisible
        }
    default:
        //Do something
}

 

위의 소스코드에서 action에 isVisible을 true로 세팅을 수행하게 되면 변경 전과 변경 후의 state 변화는 아래와 같습니다.

//변경 전
state = {
    isVisible: false,
    showPopup: false,
    showMenu: false
}

//변경 후
state = {
    isVisible: true,
    showPopup: false,
    showMenu: false
}

 

Spread Expression에 대한 상세한 설명은 아래 MDN 링크에서 확인할 수 있습니다.

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/Spread_syntax

 

전개 구문 - JavaScript | MDN

전개 구문을 사용하면 배열이나 문자열과 같이 반복 가능한 문자를 0개 이상의 인수 (함수로 호출할 경우) 또는 요소 (배열 리터럴의 경우)로 확장하여, 0개 이상의 키-값의 쌍으로 객체로 확장시

developer.mozilla.org

 

300x250

본 포스트에서는 플러터 애플리케이션을 구성하는 아키텍처에 대한 입문 정도의 이야기와 아이오닉, Cordova, React Native 등의 다른 모바일 하이브리드 개발 방법과 어떤 차이를 가지는지 이야기해 보겠습니다.

 

 

Flutter (플러터)

 

제법 오래 전부터 많은 기업들이 비즈니스를 모바일 애플리케이션으로 서비스를 합니다. 이제 대부분의 사람들이 PC로 뭔가를 처리하기 보다는 모두 손에 들고 다니는 모바일 휴대폰이 일상이 되었기 때문에 기업들도 비즈니스를 하기 위해서는 모바일 애플리케이션은 필수로 개발해야 하는 영역이 된 지 오래입니다.

 

모바일 시장은 Google, 삼성으로 대표되는 Android와 Apple이 대표하는 iOS 진영이 시장을 양분하고 있습니다. 어느 쪽의 사용자도 무시할 수 없기에 기업은 양쪽의 비용을 모두 부담하여 Android와 iOS 앱을 개발해 왔습니다. 하여 전부터 OSMU (One Source Multi Use) 라는 용어가 보여주듯 한 번의 개발로 Android와 iOS 애플리케이션을 만들 수 있는 방법들을 활발하게 고민해 왔습니다.

 

Apache Cordova

그 과정에서 많은 방법과 Platform, Framework들이 스쳐갔지만 가장 보편적으로 많이 사용되고 있는 방법은 Open Source인 Apache Cordova 기반의 Framework 들이었습니다. Cordova 애플리케이션의 아키텍처는 아래 링크에서 확인할 수 있습니다.

https://cordova.apache.org/docs/en/11.x/guide/overview/index.html

 

Architectural overview of Cordova platform - Apache Cordova

Overview Apache Cordova is an open-source mobile development framework. It allows you to use standard web technologies - HTML5, CSS3, and JavaScript for cross-platform development. Applications execute within wrappers targeted to each platform, and rely on

cordova.apache.org

 

아키텍처를 확인해보면 아래와 같습니다. (출처는 위 링크입니다.)

Cordova Application Architecture

 

Android, iOS, Windows 애플리케이션에서 사용할 수 있는 WebView를 생성하고, 해당 WebView에서 Native 기능을 사용할 수 있는 Plug-in 인터페이스를 제공합니다. 하여 실제 비즈니스 UI의 대부분은 WebView 위에서 동작하는 웹 애플리케이션으로 만들어 사용하고 Device의 기능을 사용하고자 할 때만 Plug-in을 사용해서 개발합니다.

 

Cordova는 Open Source 형태로도 많이 사용하지만, Cordova를 Wrapping 한 솔루션들도 제법 많아 많은 기업들이 현재까지도 활발하게 사용하고 있습니다.

 

 

React Native

Front-end 개발 언어 중에 가장 인기가 높은 (물론 아직 점유율 1위는 jQuery 지만) React의 인기에 힘입어 요즘은 React Native도 계속 성장하는 추세입니다. React Native는 React와 마찬가지로 Facebook에서 개발한 모바일 Open Source Framework 입니다.  

 

인스타그램, 페이스북, 핀터레스트, 스카이프 등 쟁쟁한 앱들에서 사용하고 무엇보다 React에 익숙하다면 Learning Curve를 줄일 수 있다는 장점이 있어 성장세를 이어가고 있습니다. React Native의 아키텍처는 아래 링크에서 확인할 수 있습니다.https://reactnative.dev/architecture/xplat-implementation

 

Cross Platform Implementation · React Native

The React Native renderer utilizes a core render implementation to be shared across platforms

reactnative.dev

 

아키텍처를 보면 아래와 같이 간략하게 설명되어 있습니다. (출처는 위 링크입니다.)

React Native Architecture

 

비즈니스를 JavaScript로 작성하면 패브릭이라는 렌더링 시스템이 처리하여 사용자에게 UI를 제공합니다. Cordova와 전혀 다르지만 UI를 웹 애플리케이션으로 작성한다는 점은 유사합니다.

 

Google에서도 이런 흐름에 호응하여 크로스 플랫폼을 지원하기 위한 Front-end 개발도구를 하나 내놓았습니다.

 

Flutter

Flutter는 Cordova나 React Native와는 다른 방향의 아키텍처를 제시합니다. Flutter에 대한 소개는 아래 링크에서 확인할 수 있습니다.

https://flutter.dev/

 

Flutter - Build apps for any screen

Flutter transforms the entire app development process. Build, test, and deploy beautiful mobile, web, desktop, and embedded apps from a single codebase.

flutter.dev

 

위 링크를 보시면 아래와 같은 표현이 있습니다.

Flutter 소개

 

Flutter는 Front-end UI을 웹 애플리케이션으로 개발하지 않습니다. 다른 개발 도구들은 자체의 렌더링 엔진으로 JavaScript 기반의 웹 애플리케이션을 UI로 사용하는 반면 Flutter는 위와 같이 "natively compiled" 입니다. Java와 JavaScript의 장점을 결합한 듯한 Dart 라는 언어로 UI를 개발한 후 Android, iOS, Windows, Web 애플리케이션을 각자 네이티브 애플리케이션으로 컴파일 해줍니다.

 

하여 JavaScript를 기반으로 하는 다른 기술들에 비해 Native와 조금 더 가까운 퍼포먼스를 보여줍니다. 대신 처음보는 언어인 Dart를 사용하여 개발하므로 Learning Curve가 조금 있습니다. 웹 애플리케이션만 개발하던 분이라면 UI를 개발하는 사상 자체가 Android와 가까운 Dart에 초기 적응이 쉽지 않을 수 있을 것 같습니다.

 

Flutter에서 사용하는 Dart 언어에 대한 소개는 아래 링크에서 확인할 수 있습니다.

https://dart.dev/

 

Dart programming language

Dart is a client-optimized language for fast apps on any platform

dart.dev

 

Google에서 지속적으로 신규 버전을 제공하고 있지만, 지속성을 가질 지 아닐 지 잘 감이 안오는 Flutter 및 Dart에 대해서 다음 포스트에서는 개발 환경을 구성하는 방법을 알아보겠습니다.

 

 

300x250

웹 페이지의 성능을 측정하고 진단하기 위한 도구로 Google Chrome의 Lighthouse의 권고사항 중 하나인 WebP, AVIF와 같은 차세대 이미지 형식을 사용하는 방법을 알아 보겠습니다.

 

 

이번 포스트에서는 기존의 이미지를 WebP 형식으로 변환하여 웹 페이지에서 사용할 수 있는 방법을 설명하겠습니다.

 

로컬 페이지로 성능 테스트

먼저 로컬에 JPEG 형식의 이미지를 하나 표시하는 페이지를 만든 후 Chrome Lighthouse로  성능을 테스트합니다. 이미지는 그냥 제 이전 포스트를 캡쳐한 이미지를 사용하겠습니다.

Chrome Lighthouse 사용하기

 

위 그림과 같이 크롬 개발자 도구를 열고 Lighthouse 탭으로 이동하여 [Analyze page load] 버튼을 선택하면 아래와 같이 성능을 분석한 결과가 표시됩니다. 물론 로컬에 있는 페이지를 불러온 것이고 달랑 이미지만 하나 넣었으므로 성능 지수는 부족함이 없이 나오지만 권장 사항 중에 아래와 같은 내용이 표시됩니다. (실제 웹 페이지를 분석해 보면 많은 내용들이 나옵니다.)

차세대 이미지 형식 권장

 

현재 사용 중인 JPEG 포맷 대신 WebP나 AVIF 포맷을 사용ㅇ하면 126.8 KiB가 발생하는 트래픽을 73.0KiB 까지 줄일 수 있다는 내용입니다. 이번 포스트에서는 이 중 WebP 포맷으로 변환하여 사용해 보겠습니다.

 

WebP 변환 Command-Line 도구 다운로드

Photoshop 플러그인 형태로도 사용할 수 있지만 우리는 Command-Line 도구를 다운로드 하여 기존 이미지를 변환하는 방법으로 진행해 보겠습니다. 아래 URL로 가서 각자가 사용하는 환경에 맞는 버전을 다운로드 합니다.

https://developers.google.com/speed/webp/docs/precompiled

 

Precompiled Utilities  |  WebP  |  Google Developers

Getting cwebp, dwebp, and the WebP Libraries cwebp encodes images in either JPEG, PNG or TIFF format into WebP, while dwebp decodes them back into PNG. For a quick and easy way to get started converting your images, the following archives are available on

developers.google.com

 

다운로드를 완료하면 각자 사용을 원하는 경로에 압축을 해제합니다. 환경변수 PATH에 등록하면 어느 경로에서든지 사용할 수 있습니다. 저 같은 경우는 C:\tools\libwebp-1.2.3-windows-x64 경로 아래 압축을 해제 했으며, 필요한 명령어들은 C:\tools\libwebp-1.2.3-windows-x64\bin 아래 있습니다.

 

이미지 변환해보기

이제 로컬 페이지에서 사용한 JPEG 형식의 이미지를 WebP로 변환해 보겠습니다. 연습 과정이니 PATH를 등록하지 않았다면 그냥 bin 디렉토리 아래 해당 이미지를 복사합니다. 변환 전 이미지의 크기를 확인해 보겠습니다.

변환 전 이미지 크기 확인

 

변환 전 blog_capture.jpg 이미지 파일의 크기는 129,872 Bytes  입니다. 아래 명령어를 입력하여 이미지를 변환해 봅니다.

C:\tools\libwebp-1.2.3-windows-x64\bin>cwebp -q 50 blog_capture.jpg -o blog_capture.webp

 

위 명령은 이미지 품질을 50%으로 하여 WebP 형식으로 이미지를 변환하겠다는 의미입니다. cwebp 명령어로 WebP 인코딩을 위한 명령에 대한 상세 설명은 아래 페이지를 참고하시면 됩니다.

https://developers.google.com/speed/webp/docs/cwebp

 

cwebp  |  WebP  |  Google Developers

cwebp Name cwebp -- Compress an image file to a WebP file Synopsis cwebp [options] input_file -o output_file.webp Description cwebp compresses an image using the WebP format. Input format can be either PNG, JPEG, TIFF, WebP or raw Y'CbCr samples. Note: Ani

developers.google.com

 

변환을 완료한 후 이미지 크기를 확인해 보면 아래와 같습니다.

변환 후 이미지 크기 확인

40,124 Bytes로 이미지 크기가 감소했습니다. 압축률이 상당히 좋아 보입니다.

 

 

변환한 이미지 사용하기

WebP 이미지 사용을 위해서는 WebP를 지원하지 않는 브라우저 지원을 위해 <picture> 태그를 사용해야 합니다. 변환 전에는 아래와 같은 마크업을 사용해서 이미지를 표시했습니다.

<img src="img/blog_capture.jpg">

 

WebP 이미지를 사용하기 위해서는 아래와 같이 마크업을 변경합니다.

<picture>
    <source type="image/webp" srcset="img/blog_capture.webp">
    <source type="image/jpeg" srcset="img/blog_capture.jpg">
    <img src="img/blog_capture.jpg">
</picture>

 

위와 같이 마크업을 구성하면 WebP를 지원하지 않는 브라우저에서는 jpeg 형식의 이미지를 사용하고, WebP를 지원하는 브라우저에서는 webp 형식의 이미지를 사용합니다.

 

변경 확인

Chrome의 [Network] 탭에서 사용한 이미지의 종류를 확인해보면 아래와 같이 WebP 이미지를 로딩했음을 확인할 수 있습니다.

WebP 이미지 사용 확인

 

Lighthouse에서 다시 Analyze 해보면 차세대 이미지를 사용하라는 권장 사항이 사라진 것도 확인할 수 있습니다.

Lighthouse 확인

 

300x250

본 포스트에서는 Front-end는 리액트, Back-end는 Express 기반의 Node.js를 사용하여 하나의 프로젝트로 간단한 Application을 구성해 보겠습니다. 구성할 Application은 이전 포스트에서 진행했던 네이버 API 조회 Application을 재사용 하겠습니다.

 

 

React + Express + TypeScript 프로젝트 구성하기

 

이전 포스트에서는 expressServer 프로젝트 아래 uiModule 이라는 경로를 생성하고 해당 경로를 React 프로젝트로 구성한 후 소스코드를 컴파일 및 번들링해서 Node.js 서버에서 실행해보는 부분까지 진행해 보았습니다. 이번 포스트에서는 이전 포스트들에서 만든 서버 API에서 응답하는 데이터를 확인해 보고, Front-end에서 사용할 데이터 타입을 선언해 보겠습니다.

 

먼저 서버 API의 데이터 구조를 정의한 포스트는 아래 주소를 참고합니다.

React + Express + Typescript - 02. 데이터 타입 선언 :: 즐거운인생 (tistory.com)

 

React + Express + Typescript - 02. 데이터 타입 선언

본 포스트에서는 Front-end는 리액트, Back-end는 Express 기반의 Node.js를 사용하여 하나의 프로젝트로 간단한 Application을 구성해 보겠습니다. 구성할 Application은 이전 포스트에서 진행했던 네이버 API

redballs.tistory.com

 

먼저 로컬 서버를 구동하고 http://localhost:8001/search/news/React 로 접근해서 데이터를 확인해 보겠습니다.

 

HTTP 응답

HTTP 응답은 아래와 같은 형태로 응답코드, 응답메시지, 응답데이터가 return 됩니다.

HTTP 응답

 

DATA 영역

HTTP 응답 중 데이터 영역은 아래와 같이 출력 건수(display), 시작건수(start), 전체건수(total), 검색유형(type), 검색 결과 데이터(items) 가 return 됩니다.

데이터부 응답

 

검색 결과 데이터 영역

검색 결과 데이터 영역은 두 가지 형태로 return 됩니다. type이 news일 경우에는 뉴스 데이터의 배열 형태로 return 되고, type이 book일 경우에는 도서 데이터의 배열 형태로 return 됩니다.

 

[type이 new일 경우]

뉴스 유형의 검색 결과

[type이 book일 경우]

도서 유형의 검색 결과

 

데이터 타입 선언

위 데이터를 분석해서 데이터 타입을 정의하면 되는데, 기본적으로는 서버 애플리케이션에서 정의한 데이터와 동일하게 정의하면 됩니다. 서버 애플리케이션의 정의를 참조하여 아래 경로에 프로그램을 작성합니다.

- ./uiModule/src/interface/apidata.interface.ts

 

먼저 HTTP 응답은 아래와 같이 작성합니다.

interface IHttpResp {
    code: string;
    message: string;
    data: IRespData | null
}

 

서버 요청에 대한 응답은 기본적으로 응답코드와 메시지, 조회한 데이터를 응답하도록 하고 조회한 데이터는 IRespData 형태로 아래와 같이 정의합니다. 오류일 경우 데이터가 없을 수도 있으니 null도 허용해줍니다.

interface IRespData {
    lastBuildDate: string;
    total: number;
    start: number;
    display: number;
    type: string;
    items: INewsData[] | IBookData[];
}

 

Naver API에서 주는 응답의 형태에 현재 데이터의 유형을 구분할 수 있는 type 속성을 추가합니다. 실제 조회한 데이터는 뉴스 데이터일 경우 INewsData의 배열 형태, 도서 데이터일 경우 IBookData의 배열 형태로 구성합니다. INewsData와 IBookData의 정의는 아래와 같습니다.

interface INewsData {
    title: string;
    originallink: string;
    link: string;
    description: string;
    pubDate: string;
}

interface IBookData {
    title: string;
    link: string;
    image: string;
    author: string;
    price: string;
    discount: string;
    publisher: string;
    pubdate: string;
    isbn: string;
    description: string;
}

 

다른 모듈에서 사용할 수 있도록 export를 추가합니다.

export {
    IHttpResp,
    IRespData,
    INewsData,
    IBookData
}

 

JSON 형태로 보면 아래와 같은 형태입니다.

{
    "code": "",
    "message": "",
    "data": {
        "start": 0,
        "total": 0,
        "display": 0,
        "lastBuildDate": "",
        "type": "",
        "items": [{
            "title": "",
            "description": "",
            "link": "",
            "originallink": "",
            "pubDate": ""
        }]
    }
}

 

데이터 타입 선언 부분은 서버와 클라이언트가 동일한 형태로 데이터를 참조해야 하므로 서버 개발자와 클라이언트 개발자가 별개로 있다면 협의해서 인터페이스 하는데 문제 없이 맞추어 가야합니다.

 

다음 포스트에서는 서버에서 제공한 API와 인터페이스하여 선언한 데이터 타입으로 바인드해서 확인해 보겠습니다.

300x250

본 포스트에서는 Front-end는 리액트, Back-end는 Express 기반의 Node.js를 사용하여 하나의 프로젝트로 간단한 Application을 구성해 보겠습니다. 구성할 Application은 이전 포스트에서 진행했던 네이버 API 조회 Application을 재사용 하겠습니다.

 

 

React + Express + TypeScript 프로젝트 구성하기

 

저번 포스트까지 Node.js 서버를 express 기반으로 구성한 후 간단하게나마 서버 프로그램을 작성해서 Naver API를 호출하는 API를 작성하고 테스트 해보았습니다. 이번 포스트에서는 해당 API를 사용하여 UI를 구성할 React 프로젝트의 환경을 구성해 보겠습니다.

 

서버와 클라이언트 프로젝트는 별도의 Repository로 구성해도 되고, 하나의 통합 Repository로 구성해도 됩니다. 여기서는 별도의 프로젝트로 구성하지 않고 express 프로젝트 안에 uiModule이라는 경로를 생성하여 React 프로젝트를 구성해 보도록 하겠습니다.

 

먼저 expressServer 프로젝트의 루트 경로에 uiModule이라는 경로를 생성합니다.

D:\workspace\expressServer> mkdir uiModule
D:\workspace\expressServer> cd uiModule
D:\workspace\expressServer\uiModule>

 

yarn init 명령으로 package.json 파일을 생성합니다.

D:\workspace\expressServer\uiModule> yarn init -y

 

다음은 React 프로젝트에서 사용할 모듈을 설치합니다.  프로젝트에 사용할 모듈에 대한 설명은 아래 포스트를 참고합니다.

React 기초 (목록 - TypeScript) 02 - 프로젝트 생성 :: 즐거운인생 (tistory.com)

 

React 기초 (목록 - TypeScript) 02 - 프로젝트 생성

본 포스트에서는 리액트와 타입스크립트를 이용하여 네이버 API로 데이터를 검색해서 목록 화면을 구성해 보겠습니다. 이전 포스트에서 개발할 내용을 확인해 보았으니, 이제 개발에 필요한 환

redballs.tistory.com

 

설치할 모듈은 아래와 같습니다.

D:\workspace\expressServer\uiModule> yarn add react@17.0.2
D:\workspace\expressServer\uiModule> yarn add react-dom@17.0.2
D:\workspace\expressServer\uiModule> yarn add --dev babel-core@6.26.3
D:\workspace\expressServer\uiModule> yarn add --dev babel-loader@8.2.3
D:\workspace\expressServer\uiModule> yarn add --dev babel-preset-react-app@10.0.1
D:\workspace\expressServer\uiModule> yarn add --dev webpack@5.66.0
D:\workspace\expressServer\uiModule> yarn add --dev webpack-cli@4.10.0
D:\workspace\expressServer\uiModule> yarn add --dev typescript
D:\workspace\expressServer\uiModule> yarn add --dev @types/react @types/react-dom
D:\workspace\expressServer\uiModule> yarn add moment@2.29.1

 

위 포스트에서 사용했던 webpack-dev-server는 이미 express로 구성한 서버가 있으므로 설치하지 않습니다.

 

/uiModule/src 경로를 생성하고, 아래와 같이 main.tsx 파일을 작성합니다

import ReactDOM from 'react-dom';

ReactDOM.render(<div>Hello World</div>, document.getElementById('app'));

 

타입스크립트를 사용하도록 tsx 파일로 생성했으므로 uiProject 하위에 tsconfig.json 파일을 아래와 같이 생성합니다.

{ 
    "compilerOptions": {
        "jsx": "react-jsx",
        "target": "es6",
        "module": "commonjs",
        "outDir": "./dist",
        "rootDir": "./src",
        "esModuleInterop": true,
        "forceConsistentCasingInFileNames": true,
        "strict": true,
        "skipLibCheck": true
    }
}

 

rootDir 이 ./src 이므로 uiModule/src 하위 파일들에 대해 컴파일을 수행하고 outDir 이 ./dist 이므로 uiModule/dist 하위에 컴파일 결과 파일을 생성합니다.

 

 

uiModule 의 package.json 파일에 build 스크립트를 아래와 같이 추가합니다.

{
  "name": "uiModule",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "moment": "2.29.1",
    "react": "17.0.2",
    "react-dom": "17.0.2"
  },
  "devDependencies": {
    "@types/react": "^18.0.15",
    "@types/react-dom": "^18.0.6",
    "babel-core": "6.26.3",
    "babel-loader": "8.2.3",
    "babel-preset-react-app": "10.0.1",
    "typescript": "^4.7.4",
    "webpack": "5.66.0",
    "webpack-cli": "4.10.0",
    "webpack-dev-server": "4.7.3"
  },
  "scripts": {
    "build": "webpack"
  }
}

 

여기까지 작업을 하면 uiModule 하위의 프로젝트 구조는 아래와 같습니다.

uiModule 프로젝트 구조

 

이제 tsc 명령으로 컴파일을 하면 아래 그림과 같이 dist 폴더가 생성되고 그 하위에 컴파일 결과가 생성됩니다.

D:\workspace\expressServer\uiModule> tsc

컴파일 결과 생성 확인

 

여기까지 잘 되었다면 이제 Node.js 서버에서 uiModule을 사용하기 위해 expressServer 프로젝트의 /public/js 하위로 컴파일 결과 파일들을 번들링하기 위해 아래와 같이 uiModule/webpack.config.js 파일을 생성합니다.

'use strict'
const path = require('path');

module.exports = {
    entry: {
        main: ['./dist/main.js']
    },
    output: {
        path: path.resolve(__dirname, '../public/js'),
        filename: 'build.js'
    },
    module: {
        rules: [{
            test: /\.jsx?$/,
            use: {
                loader: 'babel-loader'
            }
        }]
    }
};

 

애플리케이션의 시작점은 ./dist/main.js 로 컴파일 되므로 entry는 main.js 로 설정합니다. 번들링한 결과는 uiProject 외부에 있는 프로젝트 루트의 ./public/js로 전송해야 하므로 output은 위와 같이 설정하고 번들링한 결과는 build.js로 생성합니다.

D:\workspace\expressServer\uiProject> npm run build

 

npm run build 명령으로 번들링하면 아래 그림과 같이 결과가 생성됩니다.

번들링 결과 확인

 

번들링한 결과를 사용할 수 있도록 아래와 같이 index.html 파일을 수정합니다.

<!DOCTYPE html>
<html>
    <head>
        <title>Express Sample</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
    </head>
    <body>
        <div id="app"></div>
        <script type="text/javascript" src="js/build.js"></script>
    </body>
</html>

 

이제 프로젝트 루트로 이동해서 yarn start 명령으로 서버를 실행시키고 화면에 Hello World가 표시되는지 확인합니다.

D:\workspace\expressServer> yarn start

 

 

실행 결과 확인

300x250

본 포스트는 내부망에서 별도 Proxy를 거치는 환경에서 Node.js 서버 구성 시 발생하는 SELF_SIGNED_CERT_IN_CHAIN 오류에 대한 원인 및 해결에 대한 글입니다.

 

 

Node.js 서버에서 Http를 생성하여 다른 서버와 통신하려 할 때 아래와 같은 오류가 발생하는 경우가 있습니다.

Error: self signed certificate in certificate chain
    at TLSSocket.onConnectSecure (node:_tls_wrap:1530:34)
    at TLSSocket.emit (node:events:390:28)
    at TLSSocket._finishInit (node:_tls_wrap:944:8)
    at TLSWrap.ssl.onhandshakedone (node:_tls_wrap:725:12) {
  code: 'SELF_SIGNED_CERT_IN_CHAIN'
}

 

위와 같은 경우는 대부분 내부망에서 외부망으로 통신할 때 별도의 Proxy 서버를 두고, 해당 Proxy 서버를 거칠 때 자체 사설 인증서로 통신할 경우에 발생합니다. 클라이언트가 서버와 통신하는 과정에서 신뢰할 수 있는 인증기관에서 발급된 인증서인지를 확인하는 과정을 거칠 때, 자체 서명된 사설 인증서가 인증 체인의 중간에 위치해서 발생하는 오류입니다.

 

테스트 용도로 사설 인증서를 사용하는 것이라면 추후 공인된 인증기관의 SSL로 변경하면 되겠지만 망분리로 인해서 발생하는 오류라면 테스트 환경일 경우에 아래 로직을 한 줄 추가해서 개발합니다.

process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';

 

위 소스코드는 허가되지 않은 인증서를 거부하지 않겠다는 의미입니다.

300x250

본 포스트에서는 Front-end는 리액트, Back-end는 Express 기반의 Node.js를 사용하여 하나의 프로젝트로 간단한 Application을 구성해 보겠습니다. 구성할 Application은 이전 포스트에서 진행했던 네이버 API 조회 Application을 재사용 하겠습니다.

 

 

React + Express + TypeScript 프로젝트 구성하기

 

이전 포스트에서는 서버와 Naver API 서버 간, 클라이언트와 서버 간 인터페이스에 사용할 데이터 타입을 선언해 보았습니다. 이번 포스트에서는 Controller와 Service를 만들어서 실제로 인터페이스를 수행해 보겠습니다.

 

먼저 서버에서 Naver API와 통신을 위해서 request 모듈을 설치하겠습니다. 이전 포스트에서 React로 개발할 때는 Fetch API를 사용했지만 Node.js에는 Fetch API가 구현되어 있지 않아, node-fetch 모듈을 설치하면 유사하게 사용할 수 있지만 좀 더 많이 사용하는 request 모듈을 설치해서 사용해 보겠습니다.

D:\workspace\expressServer> yarn add request

 

request 모듈의 기본적인 사용법은 아래와 같습니다. 우리가 사용할 Naver API는 Get 방식으로 호출할 것이므로 request.get 메소드로 알아 보겠습니다.

const request = require('request');

request.get({
    headers: {},
    url: '',
    qs: {},
    json: true
}, function(error, response, body) {
});

 

request.get에 전달하는 파라미터 중 headers에는 HTTP Header 정보를 설정합니다. Content-Type과 Naver의 인증을 위한 인증 정보를 전달합니다. 아래 코드를 참고하여 각자가 발급 받은 인증 키를 입력합니다.

headers: {
    'Content-Type': 'application/json',
    'X-Naver-Client-Id': CLIENT_ID,
    'X-Naver-Client-Secret': CLIENT_SECRET
}

 

url은 파라미터를 제외한 부분을 입력합니다. 일단 뉴스 API로 간주하고 작성합니다.

uri: 'https://openapi.naver.com/v1/search/news

 

qs에는 해당 API를 Get 방식으로 호출할 때 전달할 QueryString 파라미터를 세팅합니다. 일단 검색에 정도만 세팅합니다.

qs: {
    query: "코로나"
}

 

우리는 API 요청에 대한 응답을 json으로 parsing하여 전달 받으면 되므로 json: true 옵션도 추가합니다.

 

Callback은 3개의 파라미터를 전달 받습니다. error, response, body를 전달 받는데 일단 우리는 오류 처리에 대한 부분은 body가 있는지 없는지를 보고 판단하겠습니다. body가 있을 경우에는 body에서 데이터를 추출하여 응답할 것이고, 없으면 오류 코드를 세팅하여 응답하도록 합니다.

const httpResp: HttpResp = new HttpResp();
const respData: RespData = new RespData(type);

if (body) {
    respData.setStart(body.start);
    respData.setDisplay(body.display);
    respData.setTotal(body.total);
    respData.setItems(body.items);
        
    httpResp.setCode('00');
    httpResp.setMessage('Success');
    httpResp.setData(respData);
} else {
    httpResp.setCode('99');
    httpResp.setMessage('Failed');
    httpResp.setData(null);
}

 

해당 조회하는 메소드는 비동기 처리를 위해 async 처리하여 Promise를 응답하도록 구성합니다. Service 프로그램은 ./src/naverapi/naverapi.service.ts 로 생성하고 아래와 같이 작성합니다.

import { HttpResp, RespData } from "../entity/httpresp.entity";
import { INaverApiResp } from "../interface/apidata.interface";

const NaverApiService = () => {
    const request = require('request');

    const search = async (type: string, param: string): Promise<HttpResp> => {

        return new Promise<HttpResp>((resolve, reject) => {
            const CLIENT_ID: string = '';
            const CLIENT_SECRET: string = '';
    
            const apiUrl: string = 'https://openapi.naver.com/v1/search/' + type;
            
            request.get({
                headers: {
                    'Content-Type': 'application/json',
                    'X-Naver-Client-Id': CLIENT_ID,
                    'X-Naver-Client-Secret': CLIENT_SECRET
                },
                uri: apiUrl,
                qs: {
                    query: param
                },
                json: true
            }, (error: object, response: object, body: INaverApiResp) => {
                const httpResp: HttpResp = new HttpResp();
                const respData: RespData = new RespData(type);

                if (body) {
                    respData.setStart(body.start);
                    respData.setDisplay(body.display);
                    respData.setTotal(body.total);
                    respData.setItems(body.items);
        
                    httpResp.setCode('00');
                    httpResp.setMessage('Success');
                    httpResp.setData(respData);
                } else {
                    httpResp.setCode('99');
                    httpResp.setMessage('Failed');
                    httpResp.setData(null);
                }

                resolve(httpResp);
            });
        });
    }

    return {
        search
    }
};

export default NaverApiService;

 

위에서 설명한 내용들로 구성되어 있습니다. apiUrl을 구성할 때는 뉴스와 도서를 하나의 메소드로 사용하기 위해 /search/ 하위에 타입을 추가하는 구조로 작성합니다. 다음은 Controller를 작성해 보겠습니다.

 

 

Controller 프로그램은 ./src/naverapi/naverapi.controller.ts 로 생성하고 아래와 같이 작성합니다.

import express, { Request, Response } from "express";
import { HttpResp } from "../entity/httpresp.entity";
import NaverApiService from "./naverapi.service";

const service = NaverApiService();
const NaverApiController = express.Router();

NaverApiController.get('/:type/:keyword', async (req: Request, res: Response) => {
    const httpResp: HttpResp = await service.search(req.params.type, req.params.keyword);
    res.send(httpResp);
});

export default NaverApiController;

 

express.Router()로 Router를 선언한 후 get 리스너를 추가합니다. URL은 아래와 같이 구성합니다.

http://localhost:8001/search/news/코로나

라는 URL로 접근할 경우 해당 요청에서 "news"와 "코로나"를 사용해서 Naver API에는 아래와 같이 요청하게 됩니다.

https://openapi.naver.com/v1/search/news?query=코로나

 

도서 API를 사용할 경우에도 위와 같은 규칙으로 동일하게 변환합니다.

http://localhost:8001/search/book/코로나

라는 URL로 접근할 경우 해당 요청에서 "book"과 "코로나"를 사용해서 Naver API에는 아래와 같이 요청합니다.

https://openapi.naver.com/v1/search/book?query=코로나

 

그리고 아까 생성한 Service class의 search 메소드를 호출한 결과를 응답합니다. search 메소드는 응답을 Promise<HttpResp> 타입으로 응답하므로 await 키워드를 사용하여 HttpResp 형태로 응답 받습니다.

 

이제 app.ts 프로그램에 app.use 를 사용해서 해당 Router를 앱에 추가합니다. 해당 Router는 /search 경로 하위에 매핑하겠습니다.

app.use('/search', NaverApiController);

 

해당 라인을 추가한 app.ts 의 전체 소스코드는 아래와 같습니다.

import express, { Request, Response } from "express";
import path from "path";
import NaverApiController from "./naverapi/naverapi.controller";

const app = express();

app.use(express.static(path.join(__dirname, '../public')));
app.get('/', (req: Request, res: Response) => {
    res.sendFile(path.join(__dirname, '../public/index.html'));
});
app.use('/search', NaverApiController);

app.listen('8001', () => {
    console.log('Server started');
});

  

이제 컴파일한 후 애플리케이션을 재시작 합니다.

D:\workspace\expressServer> tsc
D:\workspace\expressServer> yarn start

 

시작한 후 설정한 경로로 검색해서 결과를 한 번 확인해 보겠습니다.

먼저 news API를 "코로나"라는 키워드로 검색해서 결과를 확인해 보겠습니다.

 

[http://localhost:8001/search/new/코로나]

뉴스 API 확인

 

그 다음은 book API를 "코로나"라는 키워드로 검색해서 결과를 확인해 보겠습니다.

 

[http://localhost:8001/search/book/코로나]

도서 API 확인

 

클라이언트 프로그램에서 직접 Naver API를 호출할 때와 달리  CORS 오류가 발생하지 않고 잘 조회됨을 확인할 수 있습니다.

300x250

본 포스트에서는 Front-end는 리액트, Back-end는 Express 기반의 Node.js를 사용하여 하나의 프로젝트로 간단한 Application을 구성해 보겠습니다. 구성할 Application은 이전 포스트에서 진행했던 네이버 API 조회 Application을 재사용 하겠습니다.

 

 

React + Express + TypeScript 프로젝트 구성하기

 

이전 포스트에서는 TypeScript 컴파일 환경 및 프로젝트 실행 환경을 구성하고 컴파일한 ./dist/app.js 프로그램으로 Node.js 서버를 구동하는 부분까지 작업해 보았습니다. 이번 포스트에서는 Controller와 Serivce를 생성해서 브라우저에서 받은 요청을 Naver API로 전달하여 응답을 받은 후, 사용자의 브라우저로 전달하는 부분을 작성해 보겠습니다.

 

데이터 타입 선언

먼저 각 데이터의 타입을 선언해 보겠습니다. 데이터의 타입은 interface로 선언하며 ./src/interface/ 하위에 apidata.interface.ts 파일을 생성하여 작성합니다. 이전 포스트에서 사용했던 Naver API의 데이터 종류는 아래와 같습니다.

 

인터페이스 응답

Naver API의 전체적인 응답 Layout은 아래와 같습니다.

{
    lastBuildDate: '',
    total: 0,
    start: 0,
    display: 0,
    items: []
}

 

뉴스 API 데이터

뉴스 API 요청에 대한 응답 Layout은 아래와 같습니다.

{
    title: '',
    originallink: '',
    link: '',
    description: '',
    pubDate: ''
}

 

도서 API 데이터

도서 API 요청에 대한 응답 Layout은 아래와 같습니다.

{
    title: '',
    link: '',
    image: '',
    author: '',
    price: '',
    discount: '',
    publisher: '',
    pubdate: '',
    isbn: '',
    description: ''
}

 

위에서 확인한 데이터를 interface로 ./src/interface/apidata.interface.ts 프로그램에 아래와 같이 선언합니다.

interface INaverApiResp {
    lastBuildDate: string;
    total: number;
    start: number;
    display: number;
    type: string;
    items: INewsData[] | IBookData[];
}

interface INewsData {
    title: string;
    originallink: string;
    link: string;
    description: string;
    pubDate: string;
}

interface IBookData {
    title: string;
    link: string;
    image: string;
    author: string;
    price: string;
    discount: string;
    publisher: string;
    pubdate: string;
    isbn: string;
    description: string;
}

export {
    INaverApiResp,
    INewsData,
    IBookData
}

 

해당 Layout은 Service class에서 참조해야 하므로 export하여 다른 class에서 참조할 수 있도록 합니다. Naver API 데이터에 대한 부분은 아래 포스트에서 예제를 확인할 수 있습니다.

React 기초 (목록 만들기) 05 - ListView UI 구성 :: 즐거운인생 (tistory.com)

 

React 기초 (목록 만들기) 05 - ListView UI 구성

본 포스트에서는 리액트를 이용하여 네이버 API로 데이터를 검색해서 목록 화면을 구성해 보겠습니다. 저번 포스트에서 Naver News API를 호출해서 아래 형태로 결과를 받아보는 부분까지 진행해 보

redballs.tistory.com

 

Class 선언

추가로 두 가지 유형을 더 선언해 보겠습니다.  현재 작성하는 것은 서버 프로그램이므로 클라이언트의 HTTP 요청에 응답할 표준 Layout이 필요합니다. 간단하게 응답코드, 메시지, 데이터 본문으로만 구성해서 Class를 생성해 보겠습니다. 실제 사용 시에는 new 키워드로 인스턴스를 생성하여 사용하겠습니다.

 

interface와의 차이점은 interface는 Naver API에서 받아온 데이터의 응답에 대한 유형을 선언하고 타입 확인만 필요한 형태에 사용하지만 직접 데이터를 생성하는 부분은 class를 선언하고 인스턴스를 생성해서 사용합니다.

 

./src/entity/httpresp.entity.ts 파일을 생성하고 먼저 HTTP 요청에 대한 응답 Layout으로 사용할 HttpResp class를 아래와 같이 생성합니다.

class HttpResp {
    private code: string;
    private message: string;
    private data: RespData | null = null;

    constructor (code: string = '00', message: string = 'Success') {
        this.code = code;
        this.message = message;
    }

    public setCode = (code: string): void => {
        this.code = code;
    }

    public setMessage = (message: string): void => {
        this.message = message;
    }

    public setData = (data: RespData | null): void => {
        this.data = data;
    }
}

 

속성은 3개를 정의했습니다. 응답코드(code), 응답메시지(message), 응답데이터(data) 크게 3가지 정보를 사용하여 응답합니다. constructor는 new 키워드로 인스턴스를 생성할 때 사용하는 생성자입니다. code와 message를 파라미터로 받으며  입력하지 않을 경우 '00', 'Success'로 각각 세팅합니다.

 

응답데이터인 data는 RespData라는 타입으로 선언합니다. RespData라는 타입은 아직 생성하지 않았으니 아직은 오류가 발생합니다. 그리고 응답데이터는 초기에는 값이 없으므로 null도 허용해 주고, 초기값은 null로 세팅합니다. 아래와 같이 | (pipeline)으로 연결하면 여러 유형을 허용할 수 있습니다.

const data: RespData | null = null;

 

그리고 class에서 사용하는 속성들에 대해서는 setter만 선언 했습니다. 나중에 getter가 필요하게 되면 추가적으로 선언해도 되고, 서버에서 데이터를 수신 받아 응답만 할 목적이므로 아직까지는 getter는 필요가 없어 보입니다.

 

그럼 다음으로 응답데이터에 사용하는 RespData class를 아래와 같이 작성합니다. 별도의 파일로 생성하지 않고 동일한 파일에 작성합니다.

class RespData {
    private total: number = 0;
    private start: number = 0;
    private display: number = 0;
    private type: string = '';
    private items: INewsData[] | IBookData[] | null = null;

    constructor (type: string) {
        this.type = type;
    }

    public setTotal = (total: number): void => {
        this.total = total;
    }

    public setStart = (start: number): void => {
        this.start = start;
    }

    public setDisplay = (display: number): void => {
        this.display = display;
    }

    public setType = (type: string): void => {
        this.type = type;
    }

    public setItems = (items: INewsData[] | IBookData[]): void => {
        this.items = items;
    }
}

 

Naver API에서 넘겨주는 total (전체건수), start (시작 인덱스), display (표시 건수)는 숫자 형태로, 초기값을 0으로 선언합니다. 그리고 items는 Naver API에서 넘겨주는 뉴스나 도서 목록 데이터입니다. INewsData, IBookData의 배열이나 초기값을 위해 null 타입을 허용합니다.

 

그리고 type이라는 속성을 string 타입으로 생성합니다. 현재 조회한 목록이 뉴스 데이터인지, 목록 데이터인지를 구분하기 위한 값입니다. RespData class 역시 HttpResp와 동일한 사유로 getter는 작성하지 않았습니다. 다음 포스트 등에서 필요하게 되면 추가하겠습니다.

 

이제 필요한 데이터 타입은 모두 작성해 보았습니다. 다음 포스트에서는 실제 사용자의 요청을 받는 역할을 하는 Controller와 Naver API와 인터페이스를 수행하고 데이터를 만드는 Service를 작성하여 인터페이스를 진행해 보겠습니다.

 

300x250

본 포스트는 Node.js 에서 모듈 시스템 방식을 ES 방식으로 사용할 경우 __dirname을 찾을 수 없는 오류에 대한 설명입니다.

 

Node.js와 express로 서버를 구성할 때 html 등 Static resources에 대한 경로 설정을 아래와 같이 합니다.

const express = require('express');
const path = require('path');

const app = express();

app.use(express.static(path.join(__dirname, '../public')));
app.get('/', (req,res) => {
    res.sendFile(path.join(__dirname, '../public/index.html'));
});

 

위 소스코드는 static resources는 /public 하위 경로에 두고 초기 화면을 index.html로 설정한 경우이며, 서버를 구동하고 localhost 로 접속하면 /public/index.html을 응답합니다.

 

모듈 시스템을 CommonJS가 아닌 ES 방식을 사용해서 구성하면 require 되신 import ~ from을 사용하여 아래와 같이 작성할 수 있습니다.

import express from "express";
import path from "path";

const app = express();

app.use(express.static(path.join(__dirname, '../public')));
app.get('/', (req, res) => {
    res.sendFile(path.join(__dirname, '../public/index.html'));
});

 

 

위와 같이 작성한 후 서버를 구동해 보면 아래와 같은 오류가 표시됩니다.

ReferenceError: __dirname is not defined in ES module scope
This file is being treated as an ES module because it has a '.js' file extension and '/Users/a20201022/Documents/expressServer/package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.
    at file:///Users/a20201022/Documents/expressServer/src/app.js:7:34
    at ModuleJob.run (node:internal/modules/esm/module_job:185:25)
    at async Promise.all (index 0)
    at async ESMLoader.import (node:internal/modules/esm/loader:281:24)
    at async loadESM (node:internal/process/esm_loader:88:5)
    at async handleMainPromise (node:internal/modules/run_main:65:12)
error Command failed with exit code 1.

 

오류 메시지 그대로 __dirname 이라는 전역 변수는 ES 모듈에서는 선언되지 않아 사용할 수 없다는 의미입니다. 해결을 위해서는 해당 변수를 아래와 같이 선언합니다.

import express from "express";
import path from "path";

const app = express();
//__dirname 선언
const __dirname = path.resolve();

app.use(express.static(path.join(__dirname, '../public')));
app.get('/', (req, res) => {
    res.sendFile(path.join(__dirname, '../public/index.html'));
});

 

위와 같이 작성한 후 서버를 구동해 보면 정상 구동됨을 확인할 수 있습니다.

300x250

+ Recent posts