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

저번 포스트에서 Naver News API를 호출해서 아래 형태로 결과를 받아보는 부분까지 진행해 보았습니다.

인터페이스 결과 데이터의 형태를 확인했으니, 이제 ListView의 UI를 만들어 보겠습니다.

 

ListView 컴포넌트가 Rendering 할 HTML은 아래와 같이 <ul> 태그만 하나 생성해서 return 한 상태입니다. News API를 호출하여 받은 데이터를 <ul> 안에 <li>로 반복해서 넣어주면 될 것 같지만, <li> 내부에도 여러 태그와 데이터를 바인딩하는 부분이 있을 것이고, 뉴스일 경우와 도서일 경우를 분리해서 <li>를 구성해야 합니다.

 

하여 <li> 부분은 별도의 컴포넌트로 작성하겠습니다. 일단 NewsRow 컴포넌트를 아래와 같이 작성합니다.

const NewsRow = (props) => {
    const title = props.row.title;
    const pubDate = moment(props.row.pubDate).format('YYYY.MM.DD HH:mm');
    const desc = props.row.description;

    return (
        <li>
            <div className="title">
                <a href="#" dangerouslySetInnerHTML={{__html: title}}></a>
            </div>
            <div className="cont">
                <span className="date">{pubDate}</span>
                <span dangerouslySetInnerHTML={{__html: desc}} />
            </div>
        </li>
    );
};

NewsRow 컴포넌트는 행에 표시할 데이터를 props로 전달 받습니다. 뉴스 한 건 당 한 행을 표시할 것이므로 props로 전달하는 데이터의 형식은 아래와 같습니다.

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

props로 넘어오는 데이터가 위와 같은 형태이고 row라는 이름으로 전달하도록 하겠습니다. 우리는 이 데이터 중 title(제목), pubDate(작성일시), description(요약) 3가지 데이터를 사용하겠습니다. 이 중 pubDate는 momentJs 모듈을 사용하여  YYYY.MM.DD HH:mm 형식으로 변환해 줍니다.

 

그리고 NewsRow의 return 에는 위의 내용과 같이 Rendering 할 HTML을 작성합니다. 작성하는 내용 중 특이한 부분이 있습니다. JavaScript에서는 innerHTML로 사용하던 부분을 React에서는 dangerouslySetInnerHTML을 사용하여 표현합니다. 관련된 짧은 설명이 React 공식 페이지에 있으므로 아래 링크를 참고하시면 됩니다.

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

 

DOM 엘리먼트 – React

A JavaScript library for building user interfaces

ko.reactjs.org

이제 ListView 컴포넌트에서 조회한 데이터를 NewsRow 컴포넌트를 사용해서 바인드 합니다. ListView 컴포넌트의 return 부분을 아래와 같이 수정합니다.

return (
    <div className="listArea">
        <ul className="listView">
        {
            articles &&
            articles.map((v, inx) => {
                return <NewsRow key={inx} row={v} />
            })
        }
        </ul>
    </div>
);

데이터를 조회해 온 후 setArticles useHook을 사용해서 articles에 값이 바인드 되면 다시 Rendering 을 하게 됩니다. articles 의 수만큼 NewsRow 컴포넌트를 생성하며 row라는 이름으로 뉴스 데이터를 props로 전달합니다. 

 

 

여기까지 작성 후 다시 프로젝트를 기동하여 화면을 확인합니다. 아직 stylesheet를 작성하지 않아 아래와 같은 모양으로 표시됩니다.

이제 모양을 잡아 줄 stylesheet를 작성합니다. 

/public/ 아래 css 폴더를 생성하고 main.css 파일을 아래와 같이 작성합니다. 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 파일까지 생성한 프로젝트 파일 구조는 아래와 같습니다.

그리고 /public/index.html 파일에 main.css 를 추가합니다.

<!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="main.js"></script>
    </body>
</html>

이제 stylesheet까지 작성했으니 다시 한 번 UI를 확인해 보시면 아래와 같이 표시됩니다.

우리는 뉴스만 조회할 것이 아니라 도서 목록도 조회하기로 했습니다. NewsRow에 이어 BookRow 컴포넌트도 한 번 만들어 보겠습니다. 일단 도서 API는 데이터가 어떻게 넘어오는지 확인해 보아야 하므로 임시로 살짝 수정해서 데이터만 확인해  보겠습니다. 

 

ListView 컴포넌트의 useEffect Hook에서 apiGet 함수를 호출할 때 type 파라미터를 news 대신 book으로 변경합니다. 책은 "코스피"라는 키워드 대신 "React"로 키워드를 변경해 보겠습니다.

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

다시 한 번 새로고침을 한 후 Network 탭을 확인하면 아래와 같이 데이터를 확인할 수 있습니다.

BookRow 컴포넌트에서 사용할 데이터 형식은 아래와 같습니다.

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

BookRow 컴포넌트에서는 위 데이터 중 title(제목), image(도서 이미지), author(저자), description(요약) 4개의 항목을 사용해서 구성해 보겠습니다. BookRow 컴포넌트는 아래와 같이 작성합니다.

const BookRow = (props) => {
    const image = props.row.image;
    const title = props.row.title;
    const author = props.row.author;
    const desc = props.row.description;

    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: desc}} />
                    </div>
                </div>
            </a>
        </li>
    );
};

설명은 NewsRow 컴포넌트의 동일한 내용이므로 다시 설명하지 않겠습니다. 이렇게 변경한 다음 ListView 컴포넌트에서 Rendering 하는 부분을 BookRow로 변경합니다.

return (
    <div className="listArea">
        <ul className="listView">
        {
            articles &&
            articles.map((v, inx) => {
                return <BookRow key={inx} row={v} />
            })
        }
        </ul>
    </div>
);

여기까지만 변경을 하면 위에서 작성한 NewsRow stylesheet가 반영되어 아래와 같이 UI가 만들어 집니다. (이..이미지가 깨지는 게시물도 있네요)

BookRow에 사용할 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;}

좌측에 도서 이미지를 배치하고 오른쪽에 제목, 저자, 요약 내용을 배치 했습니다.

이번 포스트는 여기까지 작성해 보겠습니다. NewsRow와 BookRow 컴포넌트를 포함하고 있는 listview.component.jsx 프로그램의 현재까지의 소스는 아래와 같습니다.

import React from "react";
import { useEffect, useState } from "react"
import moment from "moment";

const NewsRow = (props) => {
    const title = props.row.title;
    const pubDate = moment(props.row.pubDate).format('YYYY.MM.DD HH:mm');
    const desc = props.row.description;

    return (
        <li>
            <div className="title">
                <a href="#" dangerouslySetInnerHTML={{__html: title}}></a>
            </div>
            <div className="cont">
                <span className="date">{pubDate}</span>
                <span dangerouslySetInnerHTML={{__html: desc}} />
            </div>
        </li>
    );
};

const BookRow = (props) => {
    const image = props.row.image;
    const title = props.row.title;
    const author = props.row.author;
    const desc = props.row.description;

    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: desc}} />
                    </div>
                </div>
            </a>
        </li>
    );
};

const ListView = () => {

    const [articles, setArticles] = useState(null);

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

    };

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

    return (
        <div className="listArea">
            <ul className="listView">
            {
                articles &&
                articles.map((v, inx) => {
                    return <BookRow key={inx} row={v} />
                })
            }
            </ul>
        </div>
    );
}

export default ListView;

 

 

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

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

 

js, jsx 파일은 /src 아래 경로에 작성하면 됩니다.

 

먼저 /src 하위에 /component 라는 폴더를 만들고 listview.component.jsx 파일을 하나 생성하고 아래 설명을 참고하여 목록 컴포넌트를 작성해 봅니다. 중간중간 코드에 추가한 부분은 아래와 같이 블럭으로 표시하겠습니다.

//ADD :: START
추가한 코드
//ADD :: END

 

먼저 컴포넌트를 생성하고, 현재 컴포넌트는 목록 컴포넌트이므로 데이터를 조회할 때마다 상태를 변경하고 목록을 갱신하기 위해 useState Hook을 사용하여 상태를 선언하고 초기 값은 null로 설정합니다.

import React from "react";
import { useEffect, useState } from "react"

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

export default ListView;

목록에 필요한 데이터는 Naver API를 호출하여 받아와야 합니다. fetch API를 사용하여 Naver API를 GET 방식으로 호출하는 apiGet이라는 이름의 함수를 하나 정의합니다.

import React from "react";
import { useEffect, useState } from "react"

const ListView = () => {
    const [articles, setArticles] = useState(null);
	
    //ADD :: START
    const apiGet = async (type, param) => {
        const apiUrl = 'https://openapi.naver.com/v1/search/' + type + '?query=' + param;
        const resp = await fetch(apiUrl, {
            method: 'GET',
            headers: {
                'Content-Type': 'application/json',
                'X-Naver-Client-Id': CLIENT_ID,
                'X-Naver-Client-Secret': CLIENT_SECRET
            }
        });
        resp.json().then(data => {
            setArticles(data.items);
        });
    };
    //ADD :: END
}

export default ListView;

Naver Open API를 사용하기 위해서는 API 사용 신청을 하고 인증에 필요한 정보를 발급 받아야 합니다. 여기서는 검색 API를 사용하므로 아래 URL에서  오픈 API 이용신청해서 먼저 발급 받습니다.

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

 

검색 - SERVICE-API

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

developers.naver.com

apiGet 함수는 type과 param을 전달 받도록 작성했습니다. type은 뉴스(news)인지 도서(book)인지를 결정하는 인자이고, param은 검색어입니다. 두 인자를 받아서 Naver API URL을 조립하고 GET 방식으로 호출합니다. 이 때 Naver Open API 규격에 따라 headers에 인증 정보를 전달합니다.

 

Ajax는 비동기지만 aync/await를 사용했으므로 동기처럼 사용할 수 있습니다. 조회를 하고 나면 컴포넌트의 상태를 변경해 줍니다. 위에서 선언한 setArticles 함수를 사용해서 조회한 결과로 상태를 변경합니다.

 

다음은 useEffect Hook을 사용하여 컴포넌트가 마운트 될 때 한 번만 실행할 수 있도록 두 번째 인자로 빈 배열([])을 넘겨 아래와 같이 작성합니다. "코스피"라는 단어가 들어가는 뉴스를 검색하기 위해 아래와 같이 작성해 봅니다.

import React from "react";
import { useEffect, useState } from "react"

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

    const apiGet = async (type, param) => {
        const apiUrl = 'https://openapi.naver.com/v1/search/' + type + '?query=' + param;
        const resp = await fetch(apiUrl, {
            method: 'GET',
            headers: {
                'Content-Type': 'application/json',
                'X-Naver-Client-Id': CLIENT_ID,
                'X-Naver-Client-Secret': CLIENT_SECRET
            }
        });
        resp.json().then(data => {
            setArticles(data.items);
        });
    };
	
    //ADD :: START
    useEffect(() => {
        apiGet('news', '코스피');
    }, []);
    //ADD :: END
}

export default ListView;

이제 마지막으로 컴포넌트에서 Rendering 하는 부분을 작성합니다. 일단은 데이터가 어떻게 넘어오는지 아직 모르기 때문에 그냥 <ul> 태그만 하나 출력해 두겠습니다.

import React from "react";
import { useEffect, useState } from "react"

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

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

    useEffect(() => {
        apiGet('news', '코스피');
    }, []);
	
    //ADD :: START
    return (
    	<div className="listArea">
            <ul className="listView">

            </ul>
        </div>
    );
    //ADD :: END
}

export default ListView;

여기까지 작성했다면 일단 ListView 컴포넌트는 대강 작성했습니다. 이제 main.js 에서 ListView 컴포넌트를 사용하도록 아래와 같이 변경합니다.

import React from 'react';
import ReactDOM from 'react-dom';
import ListView from './component/listview.component.jsx';

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

이제 실행을 한 번 시켜봐야 하는데, <ListView /> 등의 JSX 요소는 React의 컴포넌트를 표현하는 확장된 형태일 뿐, 정규 HTML 문법이 아니기 때문에 브라우저가 해석을 할 수 없습니다. 브라우저가  이해할 수 있는 순수 JavaScript로 변환하는 과정인 transpile 이라는 과정을 거쳐야 하며, 이 과정은 babel을 사용해서 진행합니다.

 

webpack.config.js 파일을 열어서 babel 설정을 추가합니다.

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

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

확장자가 jsx인 파일은 babel-loader를 사용해서 transpile 하겠다는 의미입니다. babel 에서 transpile에 사용할 규칙은 preset을 사용하도록 하겠습니다. 프로젝트 루트에 .babelrc 라는 파일을 생성하고 아래와 같이 작성합니다.

{
    "presets": [
        "@babel/preset-env",
        "@babel/preset-react"
    ]
}

여기까지 작성하셨으면 프로젝트의 파일 구조는 아래와 같습니다.

 

이제 yarn start 명령으로 프로젝트를 기동합니다. 잘 따라하셨다면 특별한 오류 없이 기동될 것 같습니다. 정상 기동되었다면 터미널에 아래와 같은 형태로 표시됩니다.

D:\workspace\searchNaverApi>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 D:\workspace\searchNaverApi\node_modules\react-dom\cjs\react-dom.development.js as it exceeds the max of 500KB.
asset main.js 1.13 MiB [emitted] (name: main)
runtime modules 27 KiB 12 modules
modules by path ./node_modules/ 1.02 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/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/ 78.1 KiB 2 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 ./src/ 15.8 KiB
  ./src/main.js 219 bytes [built] [code generated]
  ./src/component/listview.component.jsx 15.6 KiB [built] [code generated]
webpack 5.66.0 compiled successfully in 1866 ms

프로젝트가 기동되었다면 이제 데이터를 확인하기 위해 http://localhost:8080으로 접근하여 Network 탭을 확인합니다. 하지만  인터페이스가 정상 수행되지는 않았고 Chrome Console 에 아래와 같은 에러 메시지가 남아 있습니다.

Chrome 브라우저의 CORS 정잭을 예외하기 위해서 Chrome의 바로가기에 아래처럼 예외 옵션을 적용합니다.

"C:\Program Files\Google\Chrome\Application\chrome.exe" --disable-web-security --user-data-dir="C:\Chrome"

브라우저를 모두 종료 후에 새로 시작해서 다시 http://localhost:8080 으로 접근해서 Network 탭을 확인하면 아래와 같이 인터페이스 결과를 확인할 수 있습니다.

다음 포스트에서는 인터페이스한 내용으로 ListView UI를 완성해 보도록 하겠습니다.

300x250

+ Recent posts