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

저번 포스트에서 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

+ Recent posts