본 과정에서는 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

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

 

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

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

 

https://github.com/lgcjh0s/searchNaverApi.git

 

GitHub - lgcjh0s/searchNaverApi

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

github.com

 

체크아웃 받는 방법은 아래 포스트를 참고합니다.

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

+ Recent posts