본 과정에서는 React와 TypeScript 기반으로 낱말퍼즐 게임을 그려 보겠습니다.  첫 번째 포스트에서는 하려는 작업의 Concept와 환경설정에 대한 부분을 다루어 보겠습니다.

 

Concept

이번에 짬짬이 만들어서 가로세로 낱말퍼즐 게임 앱을 하나 출시했는데, 그 중에 Front-end 요소만 뽑아내어서 진행 과정을 설명해 보겠습니다. 어릴 때 스포츠 신문 등에서 많이 보던 가로세로 낱말퍼즐이라 모양은 익숙하실 것 같습니다.

 

실제로 동작하는 부분은 앱을 다운로드 받으셔서도 확인해 볼 수 있습니다. (안드로이드만 있습니다.)

https://play.google.com/store/apps/details?id=com.moa.puzzlekit 

 

퍼즐킷 - 낱말퍼즐 퀴즈 - Google Play 앱

가로세로 낱말퍼즐 게임입니다. 게임도 하고 상식도 쑥쑥! 키우세요

play.google.com

 

구현한 낱말 퍼즐 모양은 아래 그림과 같이 구성됩니다. (아래는 해당 애플리케이션 캡쳐 화면이며, 본 포스트에서는 퍼즐 모양에 대한 구현만 설명합니다.)

낱말퍼즐 게임

 

위 애플리케이션은 문제은행 방식이라 서버를 가지고 개발했지만 이번 포스트에서는 서버 데이터는 샘플 데이터를 만들어서 사용하겠습니다.

 

환경설정

코드 편집기는 VSCode를 사용하고, React TypeScript를 사용합니다. 패키지 관리도구는 yarn을 사용합니다. 개발에 필요한 환경구성은 아래 포스트를 참고하셔도 되고, 이번 과정에서도 동일한 구성으로 진행하겠습니다.

 

https://redballs.tistory.com/entry/React-%EA%B8%B0%EC%B4%88-%EB%AA%A9%EB%A1%9D-TypeScript-02-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%83%9D%EC%84%B1

 

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

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

redballs.tistory.com

 

워크스페이스 생성

먼저 개발에 사용할 워크스페이스 경로를 하나 만들고 패키지를 초기화 합니다. 필요한 파일들의 설치 등은 링크한 포스트를 참고하셔서 설치하시면 됩니다.

D:\workspace> mkdir makePuzzle
D:\workspace> cd makePuzzle
D:\workspace\makePuzzle> yarn init -y

 

연습으로 해보는 프로젝트이므로 패키지 관련 내용은 모두 초기값으로 설정하겠습니다. 위의 과정까지 완료하였으면 VSCode에서 프로젝트를 반입합니다.

 

모듈 설치

VSCode에서 터미널을 하나 열어서 필요한 모듈을 설치합니다. 편의를 위해 위 포스트와 동일한 버전으로 설치하겠습니다.

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

 

런타임에 필요한 모듈은 일반 의존성으로, 컴파일 타임에만 필요한 모듈들은 개발 의존성으로 설치하시면 됩니다.

 

Hello World

이제 실제로 프로그램을 작성해서 브라우저에 "Hello World"를 출력하는 부분까지 진행해 보겠습니다.

먼저 프로젝트 루트에 /src 라는 경로를 생성하고 아래와 같이 app.tsx 파일을 작성합니다.

const App = () => (
    <>Hello World</>
);

export default App;

 

동일한 경로에 해당 Component를 사용하는 main.tsx 파일도 아래와 같이 작성합니다.

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

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

 

위 두 프로그램을 작성했으면 컴파일, 번들링을 진행하여 브라우저에서 한 번 실행해 보겠습니다. 일단은 빨간 줄이 떠도 무시합니다. 먼저 컴파일을 위해 프로젝트 루트에 tsconfig.json 파일을 아래와 같이 작성합니다.

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

 

컴파일 할 대상은 ./src 하위에 있는 파일들로 설정하며 컴파일 완료한 파일은 ./dist 경로 하위에 배포하도록 설정합니다. 여기까지 완료하였으면 tsc 명령으로 컴파일 할 수 있습니다. 컴파일을 정상적으로 수행하고 나면 프로젝트에 ./dist 라는 경로가 생성되고 하위에 app.js, main.js 파일이 생성됩니다. 현재까지 프로젝트 구조는 아래와 같습니다.

프로젝트 구조

 

컴파일 한 js 파일들을 번들링하기 위해 webpack을 설정합니다. 가장 기본적인 설정만 넣어서 아래와 같이 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'
            }
        }]
    },
    devServer: {
        static: './public',
        host: 'localhost',
        port: 8080
    }
};

 

실행할 때 시작점은 컴파일 경로인 ./dist/main.js 로 설정하고 개발 서버의 루트는 ./public 하위 경로로 설정합니다. (아직은 만들지 않아서 해당 경로는 아직 없는게 맞습니다.) 포트는 무난하게 8080으로 합니다. 

output 항목에서는 번들링한 결과를 ./public/js/build.js 로 생성하도록 설정합니다.

 

번들링하고 해당 개발 서버를 start 할 수 있도록 package.json 파일에도 script를 추가합니다.

"scripts": {
    "start": "NODE_ENV=development webpack-dev-server",
    "build": "webpack"
}

 

이제 개발 서버의 루트 경로가 될 ./public 경로를 하나 생성하고 index.html을 작성합니다. index.html 파일에서는 번들링 결과인 build.js 파일을 import 합니다.

<!DOCTYPE html>
<html>
    <head>
        <title>낱말퍼즐</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>

 

Component를 렌더링할 영역으로는 app 라는 이름으로 영역을 하나 만들어 줍니다. 여기까지 완료했으면 이제 위에서 컴파일한 js 파일들을 번들링하여 build.js 로 변환합니다.

D:\workspace\makePuzzle> npm run build

 

명령을 실행하면 번들링 결과로 ./public/js/build.js 파일이 생성됩니다. 현재까지의 모두 진행한 프로젝트 파일 구조는 아래와 같습니다.

프로젝트 구조

 

이제 개발 서버를 시작하고 화면에 Hello World가 출력되는지 확인합니다.

D:\workspace\makePuzzle> yarn start

실행화면

 

이번 포스트에서는 개발 준비 작업으로 프로젝트 구조를 만들고 컴파일, 번들링 및 실행 환경까지 구성해 보았습니다. 다음 포스트에서는 이어서 낱말 퍼즐을 작성해 보겠습니다.

300x250

본 포스트에서는 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

본 포스트에서는 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

본 포스트에서는 리액트와 타입스크립트를 이용하여 네이버 API로 데이터를 검색해서 목록 화면을 구성해 보겠습니다.

 

 

 

이번 과정에서 작성한 전체 소스코드는 중간중간 포스트에서 표시했습니다.

하지만 혹시 잘 안되시는 분들이 있으면 아래 Github에서 다운로드 해서 확인해 보시면 됩니다.

 

https://github.com/lgcjh0s/searchNaverApiTs

 

GitHub - lgcjh0s/searchNaverApiTs

Contribute to lgcjh0s/searchNaverApiTs development by creating an account on GitHub.

github.com

 

Checkout 받는 방법은 아래 포스트를 참고해주세요.

https://redballs.tistory.com/entry/Github-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-VSCode%EB%A1%9C-Checkout-%ED%95%98%EA%B8%B0?category=568554 

 

Github 프로젝트 VSCode로 Checkout 하기

본 포스트는 협업을 위해 Github에 업로드 되어 있는 프로젝트를 비주얼 스튜디오 코드로 체크아웃 하는 방법을 설명합니다. 1. 체크아웃 대상 프로젝트 다른 포스트에서 진행했던 searchNaverApiTs 프

redballs.tistory.com

 

300x250
본 포스트에서는 리액트와 타입스크립트를 이용하여 네이버 API로 데이터를 검색해서 목록 화면을 구성해 보겠습니다.
 
 

React + TypeScript Naver API로 목록 만들기

 

이제 이번 프로젝트의 마지막 과정입니다. 지난 포스팅까지 해서 목록을 구성하고 탭 메뉴로 각자의 탭에 맞는 Naver API를 호출하여 목록을 변경하는 부분까지 해보았습니다. 컴포넌트 간 상태 및 데이터를 공유하기 위해서는 Recoil을 설치하고 사용해 보았습니다. 이제 이번 프로젝트의 마지막으로 키워드 검색 상자를 만들고 키워드에 맞는 검색 결과를 조회해 보겠습니다.

 

먼저 이제까지 진행한 프로젝트의 폴더 및 파일 구조를 한 번 확인하겠습니다.

혹시나 빠진 파일이 있으신 분들은 한 번 체크해 보시면 좋을 것 같습니다. 먼저 목표 UI를 확인해 보겠습니다. 저번 TypeScript를 사용하지 않고 React만 사용해서 개발했을 때 아래와 같이 UI를 확인했었고, 이번에도 동일합니다.

 

 

UI는 위와 같이 구성하고, 스크롤 영역에는 포함시키지 않고 상단에 고정합니다. 키워드를 입력하고 검색 버튼을 터치하면 현재 탭과 검색 키워드에 맞는 검색 결과를 표시합니다.

 

/src/component/ 경로에 searchbar.component.tsx 파일을 생성하고 아래와 같이 UI Rendering 부분을 작성합니다. UI를 구성하는 부분만 있어서 딱히 JavaScript 버전과 다른 내용이 없습니다.

import React from "react";

const SearchBar = () => {
    return (
        <div className="header">
            <input type="text" className="iptSearch" id="keyword" />
            <button type="button" className="search">
                <span>검색</span>
            </button>
        </div>
    )
};

export default SearchBar;

 

생성한 SearchBar 컴포넌트를 사용하기 위해 app.tsx에 SearchBar 컴포넌트를 추가합니다.

import { atom, RecoilRoot, RecoilState } from "recoil";
import ListView from "./component/listview.component";
import SearchBar from "./component/searchbar.component";
import TabList from "./component/tablist.component";

export const selectedTabId: RecoilState<string> = atom({
    key: 'tabId',
    default: 'news'
});

const App = () => (
    <RecoilRoot>
        <div>
            {/* ADD :: Start */}
            <SearchBar />
            {/* ADD :: End */}
            <ListView />
            <TabList />
        </div>
    </RecoilRoot>
)

export default App;

 

상단에 고정할 수 있도록 stylesheet를 아래와 같이 대강 추가해 줍니다.

.header{padding:10px;border-bottom:1px solid #DDD;position:fixed;top:0;display:inline-block;z-index:100;width:100%;background-color:#FFF;}
.iptSearch{width:calc(100% - 110px);border:1px solid #EEE;line-height:27px;padding:0px 10px;margin-right:10px;}
button.search{padding:5px 10px;}

 

Header 영역에 검색 상자를 추가하면서 목록 영역은 상단 부분의 자리를 좀 비켜줘야 합니다. 목록 영역의 class인 listArea class에 상단 여백을 추가합니다.

.listArea{width:100%;margin-bottom:50px;margin-top:52px;}

 

 

여기까지 진행을 하면 검색 상자에 대한 UI는 구성을 마쳤습니다. 컴파일과 번들링을 진행해서 UI를 확인하고 넘어가겠습니다.

D:\workspace\searchNaverApiTs> tsc
D:\workspace\searchNaverApiTs> npm run build

 

 

기능 구현은 아직 하지 않아, 검색이 되지는 않습니다. UI를 완성했으니 이제 검색 기능을 구현해 보겠습니다. 먼저 app.tsx 에 검색 키워드를 관리할 atom을 하나 추가로 생성합니다. 기본 검색어는 "코로나"로 지정했습니다.

import { atom, RecoilRoot, RecoilState } from "recoil";
import ListView from "./component/listview.component";
import SearchBar from "./component/searchbar.component";
import TabList from "./component/tablist.component";

export const selectedTabId: RecoilState<string> = atom({
    key: 'tabId',
    default: 'news'
});

//ADD :: Start
export const searchKeyword: RecoilState<string> = atom({
    key: 'keyword',
    default: '코로나'
});
//ADD :: End

const App = () => (
    <RecoilRoot>
        <div>
            <SearchBar />
            <ListView />
            <TabList />
        </div>
    </RecoilRoot>
)

export default App;

 

다음은 SearchBar 컴포넌트에서 검색어를 입력하고 [검색] 버튼을 터치하면 입력한 검색어를 상태 값에 저장하는 기능을 구현합니다. 아래와 같이 먼저 RecoilState를 선언한 후 [검색] 버튼을 터치하면 setter를 호출하여 상태 값에 저장합니다.

import { useRecoilState } from "recoil";
import { searchKeyword } from "../app";

const SearchBar = () => {
	//ADD :: Start
    const [keyword, setKeyword] = useRecoilState<string>(searchKeyword);

    const search = () => {
        const searchKeyword: string = (document.querySelector('#keyword') as HTMLInputElement).value;
        setKeyword(searchKeyword);
    };
    //ADD :: End

    return (
        <div className="header">
            <input type="text" className="iptSearch" id="keyword" />
            {/* onClick 추가 */}
            <button type="button" className="search" onClick={search}>
                <span>검색</span>
            </button>
        </div>
    )
};

export default SearchBar;

 

SearchBar 컴포넌트에서 searchKeyword atom에 값을 저장하는 부분을 구현하였으니 이제 ListView 컴포넌트에서 searchKeyword atom에서 검색 키워드를 추출해서 해당 검색어로 검색하도록 수정합니다.

 

먼저 SearchBar 컴포넌트와 동일하게 useRecoilState를 선언합니다.

const [keyword, setKeyword] = useRecoilState<string>(searchKeyword);

 

useEffect를 구현한 부분에 keyword를 하드코딩 값이 아닌 keyword 값으로 변경해주고, 탭 아이디나 키워드가 변경될 때마다 인터페이스를 수행하도록 변경합니다.

useEffect(() => {
    apiGet(selTabId, keyword);
}, [selTabId, keyword]);

 

ListView 컴포넌트의 전체 소스코드는 아래와 같습니다.

import moment from "moment";
import { useEffect, useState } from "react";
import { useRecoilState } from "recoil";
import { searchKeyword, selectedTabId } from "../app";
import { IBookData, IHttpResp, INewsData } from "../interface/apidata.interface";

const NewsRow = ({ title, pubDate, description }: INewsData) => {  
    return (
        <li>
            <div className="title">
                <a href="#" dangerouslySetInnerHTML={{__html: title}} />
            </div>
            <div className="cont">
                <span className="date">{moment(pubDate).format('YYYY.MM.DD HH:mm')}</span>
                <span dangerouslySetInnerHTML={{__html: description}} />
            </div>
        </li>
    )
};

const BookRow = ({ image, title, author, description }: IBookData) => {
    return (
        <li>
            <a href="#" className="bookRow">
                <div className="bookImg">
                    <img src={image} />
                </div>
                <div className="bookDesc">
                    <div className="title" dangerouslySetInnerHTML={{__html: title}} />
                    <div className="cont">
                        <span className="author">{author}</span>
                        <span dangerouslySetInnerHTML={{__html: description}} />
                    </div>
                </div>
            </a>
        </li>
    );
};

const ListView = () => {
    const [articles, setArticles] = useState<IHttpResp | null>(null);
    const [selTabId, setSelTabId] = useRecoilState<string>(selectedTabId);
    const [keyword, setKeyword] = useRecoilState<string>(searchKeyword);

    const search = () => {
        const searchKeyword: string = (document.querySelector('#keyword') as HTMLInputElement).value;
        setKeyword(searchKeyword);
    };

    const apiGet = async(type: string, param: string) => {
        const apiUrl: string = 'https://openapi.naver.com/v1/search/' + type + '?query=' + param;
        await fetch(apiUrl, {
            method: 'GET',
            headers: {
                'Content-Type': 'application/json',
                'X-Naver-Client-Id': CLIENT_ID,
                'X-Naver-Client-Secret': CLIENT_SECRET
            }
        })
        .then((resp: Response) => resp.json())
        .then((resp: IHttpResp) => {
            const typedResp: IHttpResp = {
                ...resp,
                type: type
            }
            setArticles(typedResp);
        });
    };

    useEffect(() => {
        apiGet(selTabId, keyword);
    }, [selTabId, keyword]);

    return (
        <div className="listArea">
            <ul className="listView">
            {
                articles &&
                articles.items &&
                (
                    (articles.type === 'news') ?
                        (articles.items as INewsData[]).map((v: INewsData, inx: number) => {
                            return <NewsRow key={inx} {...v} />
                        })
                    : (articles.type === 'book') ?
                        (articles.items as IBookData[]).map((v: IBookData, inx: number) => {
                            return <BookRow key={inx} {...v} />
                        })
                    : ''
                )
            }
            </ul>
        </div>
    );
};

export default ListView;

 

여기까지가 마무리입니다. 컴파일과 번들링을 수행하고 실행하면 결과를 확인할 수 있습니다. 검색과 탭 기능 모두 정상적으로 동작하네요.

 

[검색] 기능 확인

 

[탭 이동] 확인

300x250
본 포스트에서는 리액트와 타입스크립트를 이용하여 네이버 API로 데이터를 검색해서 목록 화면을 구성해 보겠습니다.
 
 

React + TypeScript Naver API로 목록 만들기

 

저번 포스트에서 뉴스와 도서 목록을 각각 구성해서 목록 UI를 완성해 보았습니다. 뉴스와 도서를 전환하기 위해서는 소스코드를 수정해가면서 조회해야 하는 부분까지 작성해 보았고, 이번 포스트에서는 [뉴스]와 [도서] 탭을 선택하면 원하는 데이터 목록이 조회되도록 탭 기능을 추가해 보겠습니다.

 

탭은 이전 React 버전과 동일하게 아래와 같은 모양으로 만들어 보겠습니다.

먼저 탭에서 사용할 데이터의 타입을 정의해 보겠습니다. /src/interface/ 아래에 tabinfo.interface. ts 파일을 생성하고 아래와 같이 작성합니다.

interface ITabInfo {
    tabId: string;
    tabName: string;
    on: boolean;
}

export {
    ITabInfo
}

 

탭 아이디와 탭 이름은 string으로 선언하고 탭의 on/off를 나타내는 "on" 항목은 boolean으로 선언합니다.

 

다음에는 /src/component/ 하위에 tablist.component.tsx 파일을 생성합니다. 먼저 탭 한 칸에 대한 컴포넌트를 작성해 보겠습니다. 아래 소스코드를 참고하여 작성합니다.

const Tab = ({ tabId, tabName, on }: ITabInfo) => {
    const changeTab = (id: string) => {
        (document.querySelector('.tabList li a.on') as HTMLAnchorElement).classList.remove('on');
        (document.querySelector('.tabList li a#' + id) as HTMLAnchorElement).classList.add('on');
    };

    return (
        <li>
            <a href="#"
                id={tabId}
                className={on ? 'on' : ''}
                onClick={() => changeTab(tabId)}>
                <span>{tabName}</span>
            </a>
        </li>
    )
};

Tab 컴포넌트는 ITabInfo 타입의 props를 전달 받습니다. 일단 changeTab 함수는 사용자가 탭을 선택했을 때 토글하는 코드만 작성합니다. TypeScript를 사용하지 않고 JavaScript만 사용할 때에는 아래와 같이 작성 했었지만 TypeScript를 사용하면 오류가 발생합니다.

컴파일 타이밍에는 해당 Selector의 결과가 있는지 없는지 알 수가 없어, 위와 같은 오류를 발생 시킵니다. 하여 TypeScript를 사용할 경우 아래 세 가지 방법 중 하나를 선택하여 소스코드를 작성합니다.

 

1. 해당 Selector의 결과에 대한 타입을 명확하게 선언합니다.

(document.querySelector('.tabList li a.on') as HTMLAnchorElement).classList.remove('on');
(document.querySelector('.tabList li a#' + id) as HTMLAnchorElement).classList.add('on');

 

2. ? (Optional) 연산자를 사용해 해당 Selector의 결과가 필수가 아님으로 선언합니다. 이 방법은 컴파일 오류는 피할 수 있지만 의미가 맞지는 않아 보입니다.

document.querySelector('.tabList li a.on')?.classList.remove('on');
document.querySelector('.tabList li a#' + id)?.classList.add('on');

 

3. ! (null forgiving) 연산자를 사용해 해당 Selector는 null이나 undefined가 할당되지 않을 것이라는 것을 표현합니다.

document.querySelector('.tabList li a.on')!.classList.remove('on');
document.querySelector('.tabList li a#' + id)!.classList.add('on');

 

Runtime에 영향을 주는 코드는 아니지만 각 방법 중 가장 의미에 맞는 방법을 사용하시면 될 것 같습니다.

 

 

다음엔 Tab 컴포넌트를 사용해 전체 탭 (뉴스, 도서)을 표시하는 TabList 컴포넌트를 아래와 같이 작성합니다.

const TabList = () => {
    const tabList: ITabInfo[] = [
        {tabName: '뉴스', tabId: 'news', on: true},
        {tabName: '도서', tabId: 'book', on: false}
    ];

    return (
        <div className="tabBox">
            <ul className="tabList">
            {
                tabList.map((v: ITabInfo, inx: number) => {
                    return <Tab key={inx} {...v} />
                })
            }
            </ul>
        </div>
    )
};

export default TabList;

ITabInfo 형식의 배열로 탭 메뉴를 선언한 후 해당 탭의 개수만큼 <Tab /> 컴포넌트를 Rendering 합니다. 이 때 뉴스는 초기에 활성화해야 하므로 "on" 항목은 true로 선언합니다. Tab 컴포넌트에 props를 전달할 때는 스프레드 구문을 사용하여 ITabInfo 형태의 Object를 모두 전달합니다. 

 

tablist.component.tsx 프로그램의 현재까지의 소스코드는 아래와 같습니다.

import { ITabInfo } from "../interface/tabinto.interface";

const Tab = ({ tabId, tabName, on }: ITabInfo) => {
    const changeTab = (id: string) => {
        (document.querySelector('.tabList li a.on') as HTMLAnchorElement).classList.remove('on');
        (document.querySelector('.tabList li a#' + id) as HTMLAnchorElement).classList.add('on');
    };

    return (
        <li>
            <a href="#"
                id={tabId}
                className={on ? 'on' : ''}
                onClick={() => changeTab(tabId)}>
                <span>{tabName}</span>
            </a>
        </li>
    )
};

const TabList = () => {
    const tabList: ITabInfo[] = [
        {tabName: '뉴스', tabId: 'news', on: true},
        {tabName: '도서', tabId: 'book', on: false}
    ];

    return (
        <div className="tabBox">
            <ul className="tabList">
            {
                tabList.map((v: ITabInfo, inx: number) => {
                    return <Tab key={inx} {...v} />
                })
            }
            </ul>
        </div>
    )
};

export default TabList;

 

App 컴포넌트에 TabList 컴포넌트를 표시하기 위해 app.tsx 프로그램에 <TabList /> 컴포넌트를 추가합니다.

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

const App = () => (
    <div>
        <ListView />
        <TabList />
    </div>
)

export default App;

 

마지막으로 TabList UI를 위해 아래 css를 main.css 파일에 추가합니다.

.tabBox{position:fixed;bottom:0px;width:100%;height:50px;border-top:1px solid #DDD;background-color:#FFF;}
.tabList{display:table;width:100%;table-layout:fixed;}
.tabList li{display:inline-block;text-align:center;width:49%;}
.tabList li:first-child{border-right:1px solid #DDD;}
.tabList li a{display:block;color:#000;font-size:1.6rem;line-height:48px;}
.tabList li a span{display:inline-block;padding:0px 8px;width:100%;}
.tabList li a.on span{background-color:#999;color:#FFF;font-weight:700;}

 

이제 UI 확인을 위해 컴파일, 번들링 한 후 확인합니다.

D:\workspace\searchNaverApiTs> tsc
D:\workspace\searchNaverApiTs> npm run build

 

확인을 해보면 탭 UI가 잘 표현되고, 토글도 정상적으로 동작합니다.

[도서] 탭 선택 시 토글 확인
[뉴스] 탭 선택 시 토글 확인

 

하지만 아직 선택한 탭에 맞는 API를 호출하여 데이터를 바인드 하는 부분은 작성해 보지 않았습니다. 이번 글이 좀 길어진 관계로 해당 내용들은 다음 포스트에서 진행 해보겠습니다.

300x250
본 포스트에서는 리액트와 타입스크립트를 이용하여 네이버 API로 데이터를 검색해서 목록 화면을 구성해 보겠습니다.
 
 

React + TypeScript Naver API로 목록 만들기

 

저번 포스트에서는 Naver API를 호출하여 응답 데이터를 확인해 보고, 응답 데이터에 맞는 interface를 구성하고 데이터 타입을 정의해서 사용해 보았습니다. TypeScript의 장단점은 분명한 것 같습니다. 어느 정도 러닝 커브가 필요하고 소스코드가 길어지는 단점이 존재하지만, JavaScript의 유연한 성격과 인터프리터 언어라는 이유로 런타임에서 발생하는 수많은 오류를 컴파일 시점에 잡아내지 못하는 단점을 상당 부분 보완해 줍니다.

 

이번 포스트에서는 Naver API의 응답 데이터로 ListView 컴포넌트를 완성하고 UI를 표시하는 부분을 작성해 보겠습니다. 현재 UI 부분은 <ul> 태그만 구성해 놓았습니다. 목록 데이터로 <li>를 반복적으로 채워 넣으면 되나, <li> 내부에도 여러 태그와 데이터를 바인딩 하는 부분들이 들어가고, 뉴스와 도서의 목록 형태가 상이합니다. 이렇게 반복적으로 사용하는 부분은 되도록 별도의 컴포넌트로 분리해서 사용합니다.

 

먼저 뉴스 목록 UI를 표시할 NewsRow 컴포넌트를 아래와 같이 작성합니다.

const NewsRow = ({ title, pubDate, description }: INewsData) => {  
    return (
        <li>
            <div className="title">
                <a href="#" dangerouslySetInnerHTML={{__html: title}} />
            </div>
            <div className="cont">
                <span className="date">{moment(pubDate).format('YYYY.MM.DD HH:mm')}</span>
                <span dangerouslySetInnerHTML={{__html: description}} />
            </div>
        </li>
    )
};

NewsRow의 역할은 props로 뉴스 정보를 전달 받아 <li> 태그를 응답하는 역할을 합니다. NewsRow에서 전달 받는 props의 형태는 INewsData 타입으로 전달 받습니다. 지난 포스트에서 정의한 INewsData 타입은 아래와 같습니다.

 

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

제목, 원문 링크, 링크, 요약, 발행일시 정보로 구성되지만 NewsRow 컴포넌트에서는 제목, 발행일시, 요약 데이터만 사용하므로 props는 위와 같은 형태로 { title, pubDate, description }: INewsData와 같이 정의 했습니다. 만약 다른 정보들을 더 표시하고 싶은 경우 추가적으로 전달 받을 수 있습니다.

 

제목과 요약에는 HTML 데이터가 포함되어 있으므로 dangerouslySetInnerHTML 속성을 사용하여 정의하며, 발생일시는 일시 데이터를 보기 좋게 표시하기 위해 moment 모듈을 이용해서 formatting 합니다. dangerouslySetInnerHTML 속성은 innerHTML로 사용하던 부분인데 React에서는 dangerouslySetInnerHTML를 사용합니다.

 

관련 설명은 React 공식 문서 중 아래 링크를 참고하시면 됩니다.

https://ko.reactjs.org/docs/dom-elements.html

 

DOM 엘리먼트 – React

A JavaScript library for building user interfaces

ko.reactjs.org

NewsRow 컴포넌트를 완성했으면 ListView 컴포넌트에서 조회한 데이터를 NewsRow 컴포넌트에 바인드 합니다. ListView 컴포넌트의 return 부분을 아래와 같이 수정합니다.

return (
    <div className="listArea">
        <ul className="listView">
        {
            articles &&
            articles.items &&
            (
                (articles.type === 'news') ?
                    (articles.items as INewsData[]).map((v: INewsData, inx: number) => {
                        return <NewsRow key={inx} {...v} />
                    })
                : ''
            )
        }
        </ul>
    </div>
);

articles의 타입은 IHttpResp 입니다. articles와 articles.item 속성이 모두 있을 경우 NewsRow를 생성하라는 의미의 소스코드 입니다. 그리고 IHttpResp 정의를 보면 아래와 같이 items의 속성은 INewsData[] | IBookData[] 입니다.

 

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

하여 map으로 반복문 작성 시 articles.items.map((v: INewsData, inx: number) => ); 같은 형식으로 작성하면 TypeScript 컴파일러가 타입 추론 시 v에 대한 타입과 속성이 맞지 않아 아래와 같은 오류가 발생합니다.

 

그래서 위와 같이 as를 사용하여 articles.items as INewsData[] 로 데이터 타입을 명시하여 타입 추론 시 오류가 발생하지 않도록 작성합니다. NewsRow 컴포넌트는 props를 INewsData 형태로 전달받도록 선언하였습니다. 컴포넌트 사용 시에도 동일한 형태의 데이터를 전달할 수 있도록 스프레드 구문으로 아래와 같이 작성합니다.

<NewsRow key={inx} {...v} />

 

여기까지 작성한 후 ListView 컴포넌트를 App 컴포넌트에서 사용하도록 app.tsx 파일을 아래와 같이 수정합니다.

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

const App = () => (
    <div>
        <ListView />
    </div>
)

export default App;

 

잊지말고 다시 컴파일, 번들링을 하고 브라우저로 확인합니다.

D:\workspace\searchNaverApiTs> tsc
D:\workspace\searchNaverApiTs> npm run build

 

아직 stylesheet를 작성하지 않았으므로 아래와 같은 모양으로 표시됩니다.

모양을 잡아 줄 stylesheet는 React 때와 동일하게 복사해서 사용하겠습니다. 아래 내용을 Copy&Paste 해서 /public/css/main.css 로 작성합니다.

html{font-size:10px;}
html, body, div, span, ul, li, h1, h2, h3, table{margin:0;padding:0;}
body, button, input {font-family:'나눔고딕', 'NanumGothic', 'Dotum', '돋움', 'gulim', '굴림', Helvetica, sans-serif;color:#000;font-size:1.4rem;font-weight:400;}
li {list-style:none;}
a:link, a:hover{text-decoration:none;}

.content{position:absolute;top:52px;bottom:0px;overflow-x:hidden;}

.listArea{width:100%;margin-bottom:50px;}
.listView li{border-bottom:1px solid #DDD;padding:10px 15px;display:inline-block;}
.listView li .title {padding:10px 0px;color:#000;font-size:1.6rem;font-weight:400;}
.listView li .title a{color:#000;}
.listView li .cont span{display:inline-block;color:#555;}
.listView li .cont span.date{margin-bottom:5px;color:#999;font-size:1.2rem}

 

css 파일까지 생성한 프로젝트의 현재 구조는 아래와 같습니다.

작성한 main.css를 /public/index.html 파일에 추가합니다.

<!DOCTYPE html>
<html>
    <head>
        <title>Search Naver API</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">
        <link rel="stylesheet" href="./css/main.css" />
    </head>
    <body>
        <div id="app"></div>
        <script type="text/javascript" src="./js/build.js"></script>
    </body>
</html>

 

stylesheet까지 반영 후 UI를 다시 확인해 보면 아래와 같이 잘 표시됩니다.

 

뉴스 목록까지 확인을 했으니 이번에는 도서 목록을 표시하는 BookRow 컴포넌트를 아래와 같이 작성합니다.

const BookRow = ({ image, title, author, description }: IBookData) => {
    return (
        <li>
            <a href="#" className="bookRow">
                <div className="bookImg">
                    <img src={image} />
                </div>
                <div className="bookDesc">
                    <div className="title" dangerouslySetInnerHTML={{__html: title}} />
                    <div className="cont">
                        <span className="author">{author}</span>
                        <span dangerouslySetInnerHTML={{__html: description}} />
                    </div>
                </div>
            </a>
        </li>
    )
};

 

NewsRow와 비교해서 별 다른 특이한 사항은 없으므로 별도 설명은 생략하겠습니다. BookRow 컴포넌트도 정상적으로 동작하는지 확인하기 위해 ListView의 Rendering 부분에 articles.type이 book일 경우 BookRow를 사용하도록 조건을 추가합니다.

return (
    <div className="listArea">
        <ul className="listView">
        {
            articles &&
            articles.items &&
            (
                (articles.type === 'news') ?
                    (articles.items as INewsData[]).map((v: INewsData, inx: number) => {
                        return <NewsRow key={inx} {...v} />
                    })
                : (articles.type === 'book') ?
                    (articles.items as IBookData[]).map((v: IBookData, inx: number) => {
                        return <BookRow key={inx} {...v} />
                    })
                : ''
            )
        }
        </ul>
    </div>
);

 

apiGet을 호출하는 useEffect 부분의 조회 조건을 아래와 같이 변경합니다.

useEffect(() => {
    apiGet('book', '타입스크립트');
}, []);

 

도서 목록을 위한 stylesheet를 main.css 파일에 추가합니다.

.listView li .bookImg{position:relative;float:left;}
.listView li .bookDesc{float:left;width:100%;}
.listView li a.bookRow{padding:0px 10px 0px 100px;display:block;height:auto;}
.listView li a.bookRow .bookImg{margin-left:-100px;margin-top:10px;}
.listView li a.bookRow .cont span.author{margin-bottom:8px;color:darkblue;}

 

다시 컴파일, 번들링을 수행한 후 화면을 확인할 수 있습니다.

D:\workspace\searchNaverApiTs> tsc
D:\workspace\searchNaverApiTs> npm run build

이번 포스트에서는 뉴스와 도서의 목록을 위한 컴포넌트를 작성해 보았습니다. 타입에 따라 목록에서 분기하여 데이터에 맞는 목록을 표시하도록 작성해 보았고, 각 UI에 맞는 stylesheet를 작성해 보았습니다.

 

다음 포스트에서는 뉴스/도서를 선택할 수 있는 탭 버튼을 작성해서 탭 전환에 따라 목록을 표시하는 부분을 작성해 보겠습니다.

 

300x250
본 포스트에서는 리액트와 타입스크립트를 이용하여 네이버 API로 데이터를 검색해서 목록 화면을 구성해 보겠습니다.
 
 

React + TypeScript Naver API로 목록 만들기

 

저번 포스트까지 TypeScript로 Hello World를 출력하는 프로그램을 작성해보고 컴파일, 번들링을 진행해서 브라우저에서 실행하는 부분까지 실습을 해보았습니다. 이번 포스트에서는 Naver 검색 API를 호출하는 부분의 소스코드를 변경하여 TypeScript 기반으로 작성해 보겠습니다.

 

이번에도 동일하게 Naver Open API를 사용해 보겠습니다. Naver API는 아래 URL에서 신청하시면 됩니다.

https://developers.naver.com/products/service-api/search/search.md

 

검색 - SERVICE-API

검색 NAVER Developers - 검색 API 소개 웹, 뉴스, 블로그 등 분야별 네이버 검색 결과를 웹 서비스 또는 모바일 앱에서 바로 보여 줄 수 있습니다. 또한 ’OO역맛집’과 같은 지역 검색을 할 수도 있으

developers.naver.com

Naver 검색 API는 아래와 같은 형식으로 데이터를 응답합니다. (아래는 뉴스 API 입니다.)

 

정리하면 뉴스 검색 API, 도서 검색 API는 아래와 같은 형식이며 items 항목에 각자의 레이아웃으로 데이터를 응답합니다.

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

 

items 항목에는 뉴스, 도서 구분에 따라 아래와 같은 데이터를 응답니다.

 

[뉴스 API 데이터]

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

 

[도서 API 데이터]

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

TypeScript는 각 변수의 데이터와 목적에 맞는 데이터 타입을 선언해야 합니다. 데이터 타입은 interface를 사용해서 선언하겠습니다. /src/interface 라는 경로를 생성하고 apidata.interface.ts 파일을 생성합니다.

 

먼저 뉴스 API에 대한 데이터 타입을 선언합니다. 뉴스 API의 응답 데이터는 모두 문자 형태이므로 string 으로 선언합니다.

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

 

다음 도서 API에 대한 데이터 타입을 선언합니다. 뉴스 API 마찬가지로 모두 문자 형태이므로 string 으로 선언합니다.

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

마지막으로 뉴스 API와 도서 API가 공통으로 사용하는 응답을 IHttpResp 라는 이름으로 선언합니다. 건수를 나타내는 항목의 데이터 유형은 number로 선언하고, items 항목은 뉴스나 도서 데이터를 목록 형태로 응답하므로 각 데이터의 배열을 or(|) 기호로 연결하여 작성합니다.

 

그리고 Naver API 응답부에는 없었던 type이라는 변수를 하나 추가합니다. 우리는 뉴스 데이터와 도서 데이터를 모두 IHttpResp 라는 형태로 수신하는데, 현재 인터페이스의 응답이 어떤 API에 대한 응답인지를 식별할 필요가 있습니다. interface가 아니라 class로 생성해서 사용한다면 resp instanceof IHttpResp 같은 형태로 식별이 가능하겠지만 우리는 단순히 데이터의 타입만 정의했으므로 instanceof 로는 현재 데이터가 뉴스 데이터인지, 도서 데이터인지 식별이 불가능합니다.

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

 

배열 요소는 []로 선언해도 되고, 아래와 같이 Array로 선언할 수도 있습니다.

items: Array<INewsData> | Array<IBookData>;

 

마지막으로 3가지 유형의 interface를 export 합니다.

export {
    IHttpResp,
    INewsData,
    IBookData
}

 

apidata.interface.ts 파일의 전체 소스코드는 아래와 같습니다.

interface IHttpResp {
    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 {
    IHttpResp,
    INewsData,
    IBookData
}

 

다음은 React와 마찬가지로 /src/component 경로를 생성하고 listview.component.tsx 파일을 생성합니다. 먼저 ListView 컴포넌트를 생성하고 useState Hook으로 상태를 선언하고 초기 값은 null로 설정합니다. React만 사용해서 개발할 때는 articles, setArticles 튜플에 대한 데이터 타입은 선언하지 않았으나 TypeScript 기반으로 개발하므로 데이터 타입을 선언합니다. 전체 데이터를 의미하는 IHttpResp를 사용하며 초기에는 빈 값이므로 null로 허용하고, 초기 값은 null로 선언합니다.

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

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

export default ListView;

 

목록에 필요한 데이터는 Naver API를 호출해서 받아와야 하므로 React로 개발할 때와 동일하게 fetch API를 사용하여 Naver API를 GET 방식으로 호출하는 apiGet이라는 이름의 함수를 하나 정의합니다.

const apiGet = async(type: string, param: string) => {
    const apiUrl: string = 'https://openapi.naver.com/v1/search/' + type + '?query=' + param;
    await fetch(apiUrl, {
        method: 'GET',
        headers: {
            'Content-Type': 'application/json',
            'X-Naver-Client-Id': CLIENT_ID,
            'X-Naver-Client-Secret': CLIENT_SECRET
        }
    })
    .then((resp: Response) => resp.json())
    .then((resp: IHttpResp) => {
        const typedResp: IHttpResp = {
            ...resp,
            type: type
        }
        setArticles(typedResp);
    });
};

각 변수의 데이터 타입을 선언합니다. fetch API의 결과는 then()을 통해 Response 타입으로 받을 수 있으며, Response의 json() API를 통해 Response body를 추출할 수 있습니다. 이렇게 추출한 응답부는 위에서 선언한 IHttpResp 형태가 됩니다.

받아온 IHttpResp 데이터를 setArticles로 상태를 저장할 때 type 속성을 추가해서 상태를 저장합니다. ...resp 같은 형태를 spread 구문 (전개 구문)이라고 하며, 아래 MDN 링크를 참고하시면 됩니다.

 

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

 

전개 구문 - JavaScript | MDN

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

developer.mozilla.org

 

이제 useEffect를 추가하여 apiGet 함수로 뉴스 검색을 호출합니다. 코로나가 다시 2만명 대가 되어서 걱정이네요. "코로나" 키워드로 검색해 보겠습니다.

useEffect(() => {
    apiGet('news', '코로나');
}, []);

 

이번 포스트에서는 UI 부분까지는 진행하지 않을 예정이므로 Rendering 하는 부분은 그냥 <ul> 태그를 하나 추가만 해놓고, 다음 포스트에서 데이터를 바인드 해보겠습니다.

return (
    <div className="listArea">
        <ul className="listView">

        </ul>
    </div>
);

 

현재까지 진행한 listview.component.tsx 파일의 전체 소스코드는 아래와 같습니다.

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

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

    const apiGet = async(type: string, param: string): Promise<void> => {
        const apiUrl: string = 'https://openapi.naver.com/v1/search/' + type + '?query=' + param;
        await fetch(apiUrl, {
            method: 'GET',
            headers: {
                'Content-Type': 'application/json',
                'X-Naver-Client-Id': CLIENT_ID,
                'X-Naver-Client-Secret': CLIENT_SECRET
            }
        })
        .then((resp: Response) => resp.json())
        .then((resp: IHttpResp) => {
            const typedResp: IHttpResp = {
                ...resp,
                type: type
            }
            setArticles(typedResp);
        });
    };

    useEffect(() => {
        apiGet('news', '코로나');
    }, []);

    return (
        <div className="listArea">
            <ul className="listView">

            </ul>
        </div>
    );
};

export default ListView;

 

소스코드 작성을 마치셨으면 컴파일과 번들링을 수행합니다.

D:\workspace\searchNaverApiTs>tsc
D:\workspace\searchNaverApiTs>npm run build

 

프로젝트를 기동합니다.

D:\workspace\searchNaverApiTs>yarn start

 

 

실행을 위한 환경 설정은 지난 포스트에서 모두 마쳤으니 한 번 실행해 보겠습니다. Local 데몬에서 Naver API를 호출하는 방식이니 Chrome 브라우저의 CORS 차단을 해제하고 실행해야 합니다. 관련된 내용은 이전 아래 포스트를 참고해주세요.

 

https://redballs.tistory.com/entry/React-%EA%B8%B0%EC%B4%88-%EB%AA%A9%EB%A1%9D-%EB%A7%8C%EB%93%A4%EA%B8%B0-04-ListView-%EB%A7%8C%EB%93%A4%EA%B8%B0?category=564726 

 

React 기초 (목록 만들기) 04 - API 호출

저번 포스트에서는 간단하게 "Hello World"를 출력하는 프로그램을 작성하고 실행시켜서 확인해 보았습니다. 이번 포스트에서는 현재 과정의 주 목적인 ListView를 구성하기 위해 Naver News API를 호출

redballs.tistory.com

 

잘 따라하셨다면 아래와 같이 Network 탭에 인터페이스 이력을 확인할 수 있습니다.

저번 포스트와 동일한 내용을 TypeScript 기반으로 변경하여 API 인터페이스까지 진행해 보았습니다. 다음 포스트에서는 인터페이스한 데이터를 목록에 바인드해서 목록 UI 구성을 마무리 해보겠습니다.

300x250
본 포스트에서는 리액트와 타입스크립트를 이용하여 네이버 API로 데이터를 검색해서 목록 화면을 구성해 보겠습니다.
 
 

React + TypeScript Naver API로 목록 만들기

 

저번 포스트에서는 프로젝트 경로를 생성하고 개발에 필요한 모듈들을 설치해 보았습니다. React 외에 TypeScript를 사용하기 위한 준비도 마쳤습니다. 이제 VSCode에 프로젝트를 반입해서 간단하게 "Hello World"를 출력하는 프로그램을 작성해 보겠습니다.

 

VSCode로 프로젝트 열기

먼저 빈 VSCode 창을 하나 열고 [폴더 열기] 버튼을 클릭합니다.

만들어 둔 searchNaverApiTs 폴더를 선택하여 엽니다.

해당 폴더를 선택하면 VSCode에 프로젝트가 아래와 같이 반입됩니다.

 

프로젝트 구조 생성

프로젝트 하위에 폴더를 두 개 만들어 보겠습니다.

 

[./public]

public 이라는 폴더에는 html, css, image 등 정적인 Resources를 관리합니다.

 

[./src]

src 라는 폴더에는 React 기반으로 개발하는 JavaScript 소스 코드를 관리합니다.

 

일단 public 경로는 비워두고 이번에는 src 경로부터 작성해 보겠습니다. 먼저 애플리케이션을 초기화 할 main.tsx 파일을 ./src 경로 하위에 생성하고 아래와 같이 작성합니다. 확장자를 .js 가 아닌 .tsx 로 작성합니다.

import ReactDOM from 'react-dom';

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

<App /> 컴포넌트가 있는 app.tsx 파일도 아래와 같이 작성합니다.

const App = () => (
    <div>Hello World</div>
)

export default App;

VSCode에서 TypeScript로 작성하게 되면 위와 같이 작성했을 때 main.tsx에는 App 컴포넌트를 import 하지 않았으므로 아래와 같은 Syntax Error가 표시됩니다.

오류가 발생한 곳에 커서를 가져가 보면 아래와 같이 오류에 대한 설명이 나오고 VSCode가 바로 수정할 수 있는 방법이 있는 상태라면 [빠른 수정] 이라는 링크가 표시됩니다.

[빠른 수정] 링크를 클릭해보면 App 컴포넌트를 ./app 파일에서 참조하겠다는 처리 방안이 표시되고, 선택할 경우 모듈을 import 시킵니다.

현재까지 작성한 프로젝트의 폴더 및 파일 구조는 아래와 같습니다.

 

컴파일 옵션 설정

TypeScript는 브라우저에서 바로 해석할 수 있는 형태가 아니므로 컴파일 과정을 거쳐 JavaScript로 변환이 필요합니다. 컴파일은 tsc 명령을 통해서 수행할 수 있습니다. 아래 명령어를 실행시키면 tsc 명령 형식 및 옵션을 조회할 수 있습니다.

tsc -help

컴파일을 할 때마다 tsc 명령에 옵션을 주고 컴파일 할 수도 있겠지만 일관성 있는 컴파일 정책을 위해서 tsconfig.json 파일을 생성하고 프로젝트에서 사용할 컴파일 옵션을 정의합니다. tsconfig 파일에 설정할 수 있는 옵션은 정말 다양하게 있지만 우리는 아래 정도의 옵션만으로 컴파일을 진행하겠습니다.

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

전체 옵션에 대한 내용은 아래 링크의 공식 문서를 참고하시면 됩니다.

https://www.typescriptlang.org/tsconfig

 

TSConfig Reference - Docs on every TSConfig option

From allowJs to useDefineForClassFields the TSConfig reference includes information about all of the active compiler flags setting up a TypeScript project.

www.typescriptlang.org

사용한 옵션들에 대해서는 간략하게 짚어 보겠습니다.

옵션 설명
jsx  JSX 코드를 컴파일 하는 방법을 설정합니다.
target  컴파일 할 ECMAScript 버전을 설정합니다.
module  사용할 모듈 시스템을 설정합니다.
outDir  컴파일 결과물인 .js 파일을 생성할 위치를 설정합니다.
rootDir  컴파일 Source의 루트 경로를 설정합니다.
esModuleInterop  CommonJS와 ES Modules 간의 상호 운용성이 생기게 할 지 여부를 설정합니다.
forceConsistentCasingInFileNames  사용할 파일명을 정확히 작성할 지 여부를 설정합니다.
strict  엄격한 타입 검사를 수행할지 설정합니다. (TypeScript를 사용하는 이유이니 true를 권장합니다.)
skipLibCheck  선언 파일 (.d.ts)의 타입 확인 여부를 설정합니다.

 

여기까지 진행한 다음 tsc 명령을 한 번 수행해 봅니다.

D:\workspace\searchNaverApiTs> tsc

컴파일 옵션에서 rootDir을 ./src, outDir을 ./public/dist로 설정했으므로 /src 아래 생성한 app.tsx 파일과 main.tsx 파일이 컴파일 되어 ./public/dist 경로 하위에 js 파일을 생성한 것을 확인할 수 있습니다.

TypeScript 컴파일러가 tsconfig.json에 선언한 옵션에 따라 TypeScript를 es5 기준의 JavaScript로 변환해서 브라우저에서 실행할 수 있는 상태로 변경했습니다. 단순히 tsc 명령을 수행하면 실행하는 경로 하위에 있는 모든 *.tsx 및 *.ts 파일을 컴파일 합니다. 

 

원하는 파일만 컴파일하기 위해서는 tsc [파일명] 형식으로 실행하는 방법도 있고, tsconfig.json 파일에 compilerOptions와 동일한 레벨에 files를 선언해서 관리할 수도 있습니다.

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "target": "es5",
    "module": "commonjs",
    "outDir": "./public/dist",
    "rootDir": "./src",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  },
  "files": [
    "main.tsx",
    "app.tsx"
  ]
}

여기까지 진행했다면 한 번 실행해 보겠습니다. ./public 하위 경로에 index.html 파일을 생성하고 /dist/main.js 파일을 참조합니다.

<!DOCTYPE html>
<html>
    <head>
        <title>Search Naver API</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="./dist/main.js"></script>
    </body>
</html>

그리고 전과 동일하게 webpack.config.js 파일을 현재 환경에 맞춰서 생성합니다.

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

module.exports = {
    entry: {
        main: ['./public/dist/main.js']
    },
    module: {
        rules: [{
            test: /\.jsx?$/,
            use: {
                loader: 'babel-loader'
            }
        }]
    },
    devServer: {
        static: './public',
        host: 'localhost',
        port: 8080
    }
};

package.json 파일에도 start script를 추가합니다.

"scripts": {
  "start": "NODE_ENV=development webpack-dev-server"
}

위 내용까지 모두 마쳤다면 yarn start로 프로젝트를 구동합니다.

D:\workspace\searchNaverApiTs> yarn start
yarn run v1.22.17
$ NODE_ENV=development webpack-dev-server
<i> [webpack-dev-server] Project is running at:
<i> [webpack-dev-server] Loopback: http://localhost:8080/, http://127.0.0.1:8080/
<i> [webpack-dev-server] Content not from webpack is served from './public' directory
[BABEL] Note: The code generator has deoptimised the styling of /Users/a20201022/Documents/searchNaverApiTs/node_modules/react-dom/cjs/react-dom.development.js as it exceeds the max of 500KB.
asset main.js 1.16 MiB [emitted] (name: main)
runtime modules 27 KiB 12 modules
modules by path ./node_modules/ 1.06 MiB
  modules by path ./node_modules/webpack-dev-server/client/ 62.1 KiB 12 modules
  modules by path ./node_modules/webpack/hot/*.js 4.4 KiB 4 modules
  modules by path ./node_modules/react/ 120 KiB 4 modules
  modules by path ./node_modules/html-entities/lib/*.js 115 KiB 4 modules
  modules by path ./node_modules/scheduler/ 29.4 KiB 4 modules
  modules by path ./node_modules/react-dom/ 732 KiB 2 modules
  ./node_modules/ansi-html-community/index.js 4.26 KiB [built] [code generated]
  ./node_modules/events/events.js 14 KiB [built] [code generated]
  ./node_modules/object-assign/index.js 2.17 KiB [built] [code generated]
modules by path ./public/dist/*.js 753 bytes
  ./public/dist/main.js 488 bytes [built] [code generated]
  ./public/dist/app.js 265 bytes [built] [code generated]
webpack 5.66.0 compiled successfully in 2314 ms

오류 없이 잘 구동되었다면 http://localhost:8080 으로 접속하여 확인을 합니다. "Hello World"가 출력되면 좋겠지만 아래와 같은 오류가 발생하고 화면에는 아무것도 출력되지 않습니다.

 

이어지는 내용부터는 다음 포스트에서 다루겠습니다. 추가 설정을 마치고 화면에 "Hello World"를 출력하는 부분까지 다음 포스트에서 진행해 보겠습니다.

300x250

+ Recent posts