본 포스트에서는 리액트와 타입스크립트를 이용하여 네이버 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

+ Recent posts