본문 바로가기

공부일지/Project

비전공자의 프로젝트 만들기) Git Hub API를 활용한 유저 검색 서비스

728x90
반응형

Git Hub API를 활용한 유저 검색 서비스

 

오늘은 패스트캠퍼스 인강 중에 Github API를 사용해서 유저 검색 서비스를 작업하였습니다.

 

먼저 완성된 이미지들을 보여드릴께요.

012
유저검색서비스 완성

서치박스에 유저를 검색하고 해당 유저의 정보에 검색하는 내용과 동일한 내용이 있는 유저들을 출력하고 info를 눌렀을 때 해당 유저의 정보와 저장소의 리스트들이 출력되는 프로젝트입니다.

 

코드 리뷰에 앞서 이번 프로젝트에서 사용했던 라이브러리 리스트입니다.

  • mui/material, mui/styled, mui/icons-material
  • axios
  • dayjs
  • zustand
  • react-router-dom

 

그럼 이제 작업했던 코드를 리뷰해 볼게요.

 

indexjs

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { BrowserRouter } from 'react-router-dom';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

 

App.js

import { AppBar, IconButton, Toolbar, Typography } from "@mui/material";
import GitHubIcon from '@mui/icons-material/GitHub';
import { Navigate, Route, Routes } from "react-router-dom";
import Main from "./pages/Main/Main";
import User from "./pages/User/User";

function App() {
    return (
        <>
            <AppBar position="static">
                <Toolbar>
                    <IconButton href="/">
                        <GitHubIcon sx={{color:'white', marginRight:'20px'}} />
                    </IconButton>
                    <Typography variant="h6" component='div'>GitHub Search Engine</Typography>
                </Toolbar>
            </AppBar>
            <div style={{maxWidth:'1000px', margin:'auto', display:'flex', flexDirection:"column", justifyContent:'center'}}>
                <Routes>
                    <Route path="/" element={<Main />} ></Route>
                    <Route path="/user/:username" element={<User />} ></Route>
                    <Route path="*" element={<Navigate to="/" />} ></Route>
                </Routes>
            </div>
        </>
    );
}

export default App;

react-router-dom을 사용하기 위해 App 컴포넌트들 BrowerRouter로 감싸야 됩니다.

 

AppBar는 헤더에 공통으로 사용되는 Nav영역으로 제모고가 로고가 있으며 로고를 클릭하면 메인페이지로 이동됩니다.

 

Route에 각 URL에 맞는 컴포넌트들 불러올 수 있도록 하였으며 각 페이지에 역할은

  • Main : 검색과 유저리스트를 보여주는 페이지
  • User : 유저의 정보와 저장소 리스트를 보여주는 페이지 
  • * : 지정하지 않은 url정보로 접속 시 리다이렉트 될 페이지

이상 위의 3가지의 페이지를 작업하였습니다.

 

Main

import React from 'react'
import SearchInput from '../../components/SearchInput/SearchInput'
import UserGrid from '../../components/UserGrid/UserGrid'

function Main() {
    return (
        <>
            <SearchInput />
            <UserGrid />
        </>
    )
}

export default Main

Main, User 페이지는 컴포넌트들을 출력하는 화면 역할을 한다. 

비유를 하면 레고 조립을 할 때 레고들을 고정하게 하는 밑받침 역할이다. 원하는 레고를 조립하고 해체할 수 있는 것처럼 원하는 컴포넌트들을 불러와서 화면에 출력시켜 주는 페이지이다.

 

Main페이지에서 불러오는 각 컴포넌트를 작업 전에 먼저 Github api 작업을 먼저 하였다.

 

Git Hub API키 발급받는 방법이 적힌 블로입니다. 참고하시면 될 거 같아요.

https://roots2019.tistory.com/417

 

깃허브 API 발급 및 적용

1. 깃허브 API를 사용할 앱 등록 https://github.com/settings/apps/news Github App name 작성 HomepageURL 을 localhost:5500 으로 지정 Webhook URL 은 비활성화 해주십시오. 2. API 키 발급 Generate a new Client secret 버튼을 클릭

roots2019.tistory.com

 

발급받으면 토큰키가 나오는데 바로 복사해야 된다 혹시라도 새로고침을 하게 되면 재발급을 하던지 삭제하고 다시 발급을 받아야 되는 거 같다.

발급을 받으면 Root 폴더에. env 폴더를 만들어 토큰키를 저장한다.

REACT_APP_GITHUB_TOKEN="토큰키"

 

 

유저 리스트를 출력하기 위해 검색 api를 사용하였다.

https://docs.github.com/ko/rest/search?apiVersion=2022-11-28 

 

검색 - GitHub Docs

Status: 200 { "total_count": 1, "incomplete_results": false, "items": [ { "url": "https://api.github.com/repos/octocat/Spoon-Knife/commits/bb4cc8d3b2e14b3af5df699876dd4ff3acd00b7f", "sha": "bb4cc8d3b2e14b3af5df699876dd4ff3acd00b7f", "html_url": "https://gi

docs.github.com

 

해당 페이지에 관련 코드와 설명이 있어서 쉽게 작업하였다.

 

store/githubUsers.js

import axios from 'axios';
import {create} from 'zustand'


export const useGithubUsersStore = create(set => ({
    users:[],
    totalCount:0,
    loading:false,
    searchUsers: async (q, page=1)=>{
        set({loading:true})
        const res = await axios.get(`https://api.github.com/search/users?q=${q}&per_page=20&page=${page}`, {
                Authorization:`Bearer ${process.env.REACT_APP_GITHUB_TOKEN}`
        })
        set({
            loading:false,
            users:res.data.items,
            totalCount:res.data.total_count
        })
    }
}));

zustand에서 create를 해줘야 상태 관리를 할 수 있다.

 

상태관리툴은 zustand 말고도 다양한 게 많은데 이번에는 zustand를 사용하였다.

useGithubUsersStore라는 상태를 만들어서 users와 totalCount, loading의 데이터를 받고 searchUsers는 searchApi로 유저를 검색해서 저장하였다.

유저를 검색할 때는 loading을 true로 해서 로딩화면을 출력하고 데이터를 다 받아오면 false로 변경되고 user의 데이터를 users에 저장하고 총개수를 totalCount에 저장하였다.

 

이번에 zustand를 사용면서 상태를 저장하는 방법은 set을 사용해서 상태를 변경하는 것 같다.

 

GitHub api는 Authorization에 토큰을 작성해서 해당 토큰이 맞는지 여부를 판단하고 데이터를 출력해 주는데 저부분을 인강이랑 동일하게 작성하였는데 계속 401 오류가 나오면서 화면이 출력이 되지 않았다. 그 부분 이야기는 검색 기능을 다 작업하고 다시 이야기해보겠다.

 

SearchInput.jsx

import { IconButton, TextField } from '@mui/material'
import React, { useCallback, useEffect, useState } from 'react'
import SearchIcon from '@mui/icons-material/Search';
import { useSearchParams } from 'react-router-dom';

function SearchInput() {

    const [text, setText] = useState('')
    const [searchParams, setSearchParams] = useSearchParams({})

    const onSubmit = useCallback(() => {
        if(text === "") return;
        setSearchParams({q:text})

    }, [text, setSearchParams]);

    const onChange = useCallback(e => {
        setText(e.target.value)
    }, []);

    const onKeyUp = useCallback(e=>{
        if(e.key !== "Enter") return;
        onSubmit()
    }, [onSubmit])

    useEffect(()=>{
        const query = searchParams.get("q")
        if(!query) return;
        setText(query)
    }, [searchParams])

    return (
        <TextField 
            label="Github User 입력"
            variant='outlined'
            sx={{margin:"50px auto", width:'80%'}}
            value={text}
            onChange={onChange}
            onKeyUp={onKeyUp}
            InputProps={{endAdornment: <IconButton type="button" onClick={onSubmit} ><SearchIcon /></IconButton>}}
        />
    )
}

export default SearchInput

 

코드가 별로 길진 않아서 보면 바로 이해가 되실 것 같다.

 

하지만 나는 아직도 useCallback이라는 게 잘 이해가 되지 않는다. 다른 hook들은 어느 정도 이해가 되는데 callback은 내가 이해한 거는 왼쪽의 함수가 아무리 변경이 되어도 오른쪽의 배열이 변경되지 않으면 데이터가 바뀌지 않는다는 건데 이게 맞는지 모르겠다.

 

코드의 기능을 보자면 input의 내용을 바꾸고 submit 되면 params에서 q의 값을 변경하고 만약 input에 아무 내용도 없이 submit을 하면 기능이 작동이 안 되게 바로 return을 한다.

 

searchParams는 raect-router-dom의 내장 함수로써 주소에 파라미터들을 object로 저장하고 거기서 데이터를 들고 오고 파라미터를 저장하고 하는 기능을 제공한다.

 

그리고 만약 디렉트로 주소를 입력하고 접속하는 사람들이 있을 수도 있으니 url에 q의 파라미터가 있으면 자동으로 해당 유저 리스트가 출력되며 input에도 q의 값이 적히게 하였다.

 

이제 검색어를 입력했으니 유저를 검색하는 컴포넌트의 코드들을 보자

 

UserGrid.jsx

import { CircularProgress, Grid, Pagination } from '@mui/material'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useSearchParams } from 'react-router-dom'
import { useGithubUsersStore } from '../../store/githubUsers'
import UserItem from '../UserItem/UserItem'

function UserGrid() {

    const [searchParams, setSearchParams] = useSearchParams({})
    const {users, totalCount, loading, searchUsers} = useGithubUsersStore();
    const [currentPage, setCurrentPage] = useState(1)

    useEffect(()=>{
        const query = searchParams.get("q")
        const page = searchParams.get('page');
        if(!query) return;
        searchUsers(query, page )
    }, [searchParams, searchUsers])

    const totalPageCount = useMemo(()=> {
        const pageCount = Math.ceil(totalCount / 20)
        return pageCount > 50 ? 50 : pageCount
    }, [totalCount]);

    const handleChangePage = useCallback((e, number) => {
        setSearchParams({q:searchParams.get('q'), page:number })
    }, [searchParams, setSearchParams])

    useEffect(()=>{
        const page = searchParams.get('page') ?? 1;
        setCurrentPage(parseInt(page))

    }, [searchParams])

    if(loading){
        return <CircularProgress sx={{margin:'200px auto 0',}} size={100} />
    }else{
        return (
            <>
                <Grid
                    container
                    spacing={{xs:2, sm:2, md:2}}
                    columns={{xs:2, sm:3, md:4}}
                    sx={{padding:"10px"}}                
                >
                    {users.map((user, idx)=>(
                        <Grid item xs={1} sm={1} md={1} key={idx} >
                            <UserItem user={user} />
                        </Grid>
                    ))}
                </Grid>
                {totalCount !== 0 ? 
                <Pagination 
                    sx={{margin:'auto'}} 
                    page={currentPage} 
                    count={totalPageCount}
                    color='primary'
                    onChange={handleChangePage}
                />
                : null}
            </>
        )
    }
}

export default UserGrid

해당 파일에서 위에서 만든 store을 사용해서 데이터를 출력하는 코드이다.

 

파라미터를 들고 오고, 기본 페이지넘버를 1로 지정하고 store에서 만든 키들을 바로(갑자기 용어가 기억이 안 나네요...) 들고 왔다.

 

useEffect로 검색어나 페이지가 변경될 때 setUser()에 검색어와 page를 넣어서 실행한다.

 

page는 GitHub API는 최대 1000개의 정보를 뿌려준다고 해서 최댓값 1000를 기준으로 한번 출력에 20개의 리스트를 보여주기 위해 20을 기준으로 최대 50이라는 최대값 수치를 적용하였다.

 

이번에도 혹시나 다이렉트로 주소를 입력하고 접속할 때 page의 값이 없을 수 있으니 page의 파라미터 값이 없으면 1번 페이지를 로드하게 하였다.

 

페이징과 로딩은 전부 MUI 프레임워크를 사용해서 디자인해주었다.

 

하지만 여기서 위에서 말만 문제가 계속되었다.

 

0123
401 Error

토큰 쪽에서 계속 에러가 나왔다.

구글링을 해보니 나만 그런 게 아닌 거 같았다

https://github.com/the-road-to-graphql/react-graphql-github-apollo/issues/10

 

process.env.REACT_APP_GITHUB_PERSONAL_ACCESS_TOKEN produces undefined · Issue #10 · the-road-to-graphql/react-graphql-github-a

const httpLink = new HttpLink({ uri: 'https://api.github.com/graphql', headers: { authorization: Bearer ${ process.env.REACT_APP_GITHUB_PERSONAL_ACCESS_TOKEN }, }, }); The process.env code is unrel...

github.com

해당 글을 읽어보니 npm start 할 때 토큰을 못 불러와서 그렇다고 하는데 처음에는 그런가 싶어서 실행 중인 서버도 내렸다가 다시 올려도 잠시 됐다가 또 계속 오류가 나왔다.

 

그래서 env에 토큰을 작성하는 방식 말고 다이렉트로 토큰을 입력하니 잘 작동하였다.

 

하지만 나는 env에 작성하고 해당 토큰을 불러오고 싶었다. 여기서 포기하고 다이렉트로 적으면 뭔가 아무것도 얻을 게 없고 이런 오류도 수정 못하는데 다른 건 어떻게 하나 생각이 들었다.

 

그래서 몇 시간을 구글링 해보았다. 아마 회사에서 저런 문제로 몇 시간을 구글링 하게 되면 안 되겠지만 혼자서 하는 프로젝트의 장점이자 단점인 거 같다.

 

그래도 우연히 구글링 중에 알게 된 방법으로 해결하였다.

 

기존코드는 

const res = await axios.get(`https://api.github.com/search/users?q=${q}&per_page=20&page=${page}`, {
        header{
            Authorization:`Bearer ${process.env.REACT_APP_GITHUB_TOKEN}`
        }
})

이렇게 header로 토큰을 넘겨주었는데

const res = await axios.get(`https://api.github.com/search/users?q=${q}&per_page=20&page=${page}`, {
        Authorization:`Bearer ${process.env.REACT_APP_GITHUB_TOKEN}`
})

이렇게 header을 적지 않고 사용하니 잘 작동되었다. 

 

아마 인강이 작년에 만든 거라 그 사이에 github 쪽에서 수정이 된 거 같다. 

 

이번 프로젝트에서 제일 오래 걸렸던 오류를 해결해서 이번 프로젝트에서도 하나 배운 게 있고 기억에 남는 수정을 해서 기분이 좋았다.

 

프로젝트를 할 때마다 느끼는 건데 처음 뼈대 잡는데 시간이 제일 오래 걸리는 거 같다.

이번에도 유저리스트 부분 작업하는데 시간이 제일 오래 걸렸고 그 후에는 해당 코드를 기반으로 응용만 하는 부분이라 오래 걸리지 않았다.

 

이걸로 Main페이지는 완성이 되었다.

검색결과 화면

 

이제 리스트 중에 선택한 유저의 정보를 보는 기능을 만들면서 먼저 api작업을 해주어야겠죠?

 

store/githubuser.js

import axios from "axios";
import { create } from "zustand";

export const useGithubUserStore = create (set=>({
    user:{},
    loading:false,
    getUser: async (username) => {
        set({loading:true})
        const res = await axios.get(`https://api.github.com/users/${username}`, {
                Authorization:`Bearer ${process.env.REACT_APP_GITHUB_TOKEN}`
        })
        set({
            loading:false,
            user:res.data
        })
    }
}))

유저 리스트를 출력하는 코드와 크게 다를 게 없는 코드이다. 

api의 주소가 다르고 user를 배열이 아닌 객체로 받는다는 것이다.

 

UserInfo

import { Avatar, Button, Card, CardContent, CircularProgress, Link, Typography } from '@mui/material'
import React, { useCallback, useEffect } from 'react'
import { useGithubUserStore } from './../../store/githubUser';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
import dayjs from 'dayjs';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';

function UserInfo() {

    const {
        user: {
            avatar_url,
            name,
            html_url,
            company, 
            blog, 
            location:locationInfo,
            email, 
            hireable, 
            bio, 
            public_repos, 
            public_gists, 
            followers, 
            following, 
            created_at, 
            updated_at 
        }, 
        loading, 
        getUser 
    } = useGithubUserStore()

    const {username} = useParams()
    useEffect(() => {
        getUser(username)
    }, [username, getUser])

    const location = useLocation();
    const navigate = useNavigate();
    const onClickNavigateToList = useCallback(() => {
        if(!location.state) navigate('/');
        else{
            navigate({
                pathname:'/',
                search : !!location.state?.previous 
                    ? `?q=${location.state?.q}&page=${location.state?.previous}` 
                    : `?q=${location.state?.q}`
            })
        }
    }, [location.state, navigate])

    if(loading){
        return <CircularProgress sx={{margin:'200px auto 0'}} />
    }else{
        return (
            <>
                <Button style={{margin:'10px'}} onClick={onClickNavigateToList} startIcon={<ArrowBackIcon />}>목록</Button>
                <Card variant='outlined' sx={{ margin: '10px' }}>
                    <CardContent sx={{textAlign:'center'}}>
                        <Avatar alt={username} src={avatar_url ? avatar_url : ''} sx={{width:'200px', height:'200px', margin:'auto'}} />
                        <Typography variant='h4'sx={{marginBottom:'50px'}} >
                            {name}
                        </Typography>
                        <Button variant='contained' href={html_url} target='_blank' sx={{marginBottom:'30px'}}>GitHub Page</Button>
                        {bio ? <Typography variant='subtitle1'>자기소개 : </Typography> : null}
                        {company ? <Typography variant='subtitle1'>회사정보 : </Typography> : null}
                        {blog ? <Typography variant='subtitle1'>블로그 : <Link href={blog}>{blog}</Link></Typography> : null}
                        {locationInfo ? <Typography variant='subtitle1'>위치 : {locationInfo}</Typography> : null}
                        {email ? <Typography variant='subtitle1'>메일 : {email}</Typography> : null}
                        <Typography variant='subtitle1'>고용가능 여부 : {hireable ? "예" : "아니요"}</Typography>
                        <Typography variant='subtitle1'>public repository 개수 : {public_repos}</Typography>
                        <Typography variant='subtitle1'>public gists 개수 : {public_gists}</Typography>
                        <Typography variant='subtitle1'>팔로워 : {followers}</Typography>
                        <Typography variant='subtitle1'>팔로잉 : {following}</Typography>
                        <Typography variant='subtitle1'>GitHup 생성일 : {dayjs(created_at).format('YYYY.MM.DD h:mm A')}</Typography>
                        <Typography variant='subtitle1'>최근 GitHup 업데이트 : {dayjs(updated_at).format('YYYY.MM.DD h:mm A')}</Typography>
                    </CardContent>
                </Card>
            </>
        )
    }
}

export default UserInfo

코드가 길어 보이지만 결국은 user에서 가져오는 데이터가 많아서 그 부분을 출력하는 코드들이 많을 뿐인 거 같다.

 

이번에도 Main 컴포넌트를 작업할 때처럼 다이렉트로 주소를 입력하고 들어올 수 있도록 Params 작업을 하였다.

 

목록으로 이동하기 위해 Location에 previos를 저장해 이전 페이지 정보를 얻어 왔다. 다이렉트로 접속 시 메인페이지로 이동되게 하였다.

유저 정보 화면

 

유저 정보 밑에 저장소를 출력하는 부분의 코드를 한번 볼까요?

 

저장소의 데이터를 처리하는 store부터 볼게요.

 

store/githubRepos.js

import axios from "axios";
import { create } from "zustand";

export const useGithubReposStore = create(set => ({
    repos:[],
    loading:false,
    isEnd:false,
    getRepos:async (username, page)=>{
        set({loading:true})
        const res = await axios.get(`https://api.github.com/users/${username}/repos?pre_page=30&page=${page}`,{
            Authorization:`Bearer ${process.env.REACT_APP_GITHUB_TOKEN}`
        })
        set(state => ({
            loading:false,
            repos:[...state.repos, ...res.data],
            isEnd:res.data.length === 0
        }))
    },
    resetRepos: () => {
        set({
            loading:false,
            repos:[],
            isEnd:false,
        })
    }
}))

 

users에서 username의 repos의 데이터를 가져오는데 30개씩 가져오도록 했어요.

 

이번에는 다른 store와 다르게 repos를 배열로 정의하고 데이터를 가져올 때마다 기존 state는 그대로 저장하고 요청으로 받은 데이터를 추가하는 방식으로 기존 데이터에서 추가 데이터를 계속 추가하는 방식입니다.

 

데이터가 없으면 isEnd가 true가 되고 데이터가 없다는 것을 알려줍니다.

 

RepoList.jsx

import { Button, CircularProgress, Typography } from '@mui/material'
import React, { useCallback, useEffect, useState } from 'react'
import { useGithubReposStore } from '../../store/githubRepos'
import { useParams } from 'react-router-dom'
import RepoItem from '../RepoItem/RepoItem'

function RepoList() {

    const {username} = useParams()
    const [page, setPage] = useState(1)
    const {repos, loading, isEnd, getRepos, resetRepos} = useGithubReposStore()

    useEffect(()=>{
        getRepos(username, page)
    }, [username, page, getRepos])

    useEffect(()=>{
        return() => {
            resetRepos();
        }
    },[resetRepos])

    const loadMore = useCallback(() => setPage((prePage) => prePage +1), [])

    return (
        <>
            {repos.length > 0 ?
                <Typography variant='h4' textAlign="center">
                    Github Repository List
                </Typography> : null
            }
            {
                repos.map((repo, idx) => <RepoItem repo={repo} key={idx} />)
            }
            {
                loading 
                ? <CircularProgress sx={{margin:'auto'}} size={50} /> 
                : isEnd 
                    ? null 
                    : <Button style={{margin:'10px'}} onClick={loadMore}>Load More</Button>
            }
        </>
    )
}

export default RepoList

코드는 검색했을 때 유저 리스트를 뿌려주는 것과 비슷하게 페이지와 유저 이름을 받습니다.

 

받은 유저정보를 map으로 RepoItem을 순차적으로 만들어서 출력해 줍니다.

 

RepoItem은 받은 데이터를 디자인해서 출력시켜줍니다.

 

import React from 'react'
import { Card, CardContent, Link, Typography } from '@mui/material'
import dayjs from 'dayjs'
import VisibilityIcon from '@mui/icons-material/Visibility';
import StarIcon from '@mui/icons-material/Star';

function RepoItem({repo:{html_url, name, update_at, language, watchers_count, stargazers_count, forks}}) {
    return (
        <Card variant='outlined' sx={{margin:'10px'}}>
            <CardContent>
                <Link href={html_url} target='_blank' underline='none' sx={{fontSize:'50px'}} >{name}</Link>
                <div style={{marginTop:'10px'}}>
                    <Typography variant='subtitle2' display='inline'>
                        최근 업데이트 날짜 : {dayjs(update_at).format('YYYY.MM.DD h:mm A') }
                    </Typography>
                    {
                        language 
                            ? <Typography variant='subtitle2' display='inline' sx={{paddingLeft:'20px'}}>
                                Language : {language}
                            </Typography> 
                            : null
                    }
                </div>
                <div style={{marginTop:'10px', display:'flex', alignItems:'center', gap:'20px'}}>
                    <Typography style={{display:'flex', alignItems:'center', gap:'5px'}} >
                        <VisibilityIcon sx={{fontSize:'15px'}} /> {watchers_count}
                    </Typography>
                    <Typography style={{display:'flex', alignItems:'center', gap:'5px'}} >
                        <StarIcon sx={{fontSize:'15px'}} />{stargazers_count}
                    </Typography>
                    <Typography style={{display:'flex', alignItems:'center', gap:'5px'}} >
                        forks : {forks}
                    </Typography>
                </div>
            </CardContent>
        </Card>
    )
}

export default RepoItem

 

이렇게 하면 이제 유저 데이터 쪽도 완전히 출력이 됩니다.

 

초반에 api에서 데이터를 받는 과정에서 토큰 오류로 인해 시간이 오래 걸렸지만 그래도 한 가지 배우는 계기가 되었고 한 가지를 습득할 수 있어서 좋았습니다.

 

다음시간에는 인강에서 슬랙 클론코딩과 쇼핑몰 만들기가 있던데 두 가지를 만들도 또 개인적으로 이번에는 Next를 사용해서 프로젝트를 만들어 보려고 합니다.

 

마지막으로 깃헙 주소에요.

https://github.com/piwe2004/github-search-engine/tree/main

 

GitHub - piwe2004/github-search-engine

Contribute to piwe2004/github-search-engine development by creating an account on GitHub.

github.com

 

그럼 다음시간에 또 올게요 ~

 

 

728x90
반응형