본문 바로가기
React

2021.08.07 React 외부 API 호출

by 해맑은 코린이 2021. 8. 7.

2021.08.07 React 외부 API 호출_정리노트

 

 

드디어 ~!!!!!!!! 아기다리고기다리던 API 요청 받아오기 파트..꼭..내가 마스터해서...장고에서도 불러오리라...

 

axios

현재 가장 많이 사용되고 있는 자바스크립트 HTTP 클라이언트 .

HTTP 요청을 Promise 기반으로 처리한다.

 

 

 

이제 프로젝트 만들고 시작!

 

$ yarn create react-app news-viewer

$ cd news-viewer

$ yarn add axios

 

프로젝트 만들고 axios 까지 설치해줍씨단.

 

 

 

내가 이번 프로젝트에서 사용될 코드 포맷팅을 .prettierrc 파일을 만들고,

 

 

이렇게 설정!

 

요게 뭔지 모른다면 해당 포스팅 참고 ㅎㅅㅎ!

 

https://korinkorin.tistory.com/68

 

2021.07.28 React To-do List

2021.07.28 React To-do List_정리노트 오늘은 드디어 이론 공부만 줄줄하다가 프로젝트 한다니까 넘나 행복쓰 ㅎㅅㅎ 프로젝트 전체코드를 다 다루진 않고, 흐름 정리하며, 놓치기 쉬운 것들 위주로 블

korinkorin.tistory.com

 

 

먼저 간단하게 API 요청을 하는 코드 App 에 작성!

 

src/App

import React, { useState } from 'react';
import axios from 'axios';

function App() {
  const [data, setData] = useState(null);
  const onClick = () => {
    axios
      .get('https://jsonplaceholder.typicode.com/todos/1')
      .then((response) => {
        setData(response.data);
      });
  };
  return (
    <div>
      <div>
        <button onClick={onClick}>불러오기</button>
      </div>

      {data && (
        <textarea
          rows={7}
          value={JSON.stringify(data, null, 2)}
          readOnly={true}
        />
      )}
    </div>
  );
}

export default App;

 

 

App 에다가  axios.get 이라는 함수를 사용했는데, 이는 파라미터로 전달된 주소에 GET 요청을 해준다. 이에 대한 결과는 비동기적으로 확인하기 위해서 .then 을 사용!

 

 

이제는 async 를 써서 가져와보자!

import React, { useState } from 'react';
import axios from 'axios';

function App() {
  const [data, setData] = useState(null);
  const onClick = async () => {
    try {
      const response = await axios.get(
        'https://jsonplaceholder.typicode.com/todos/1',
      );
      setData(response.data);
    } catch (e) {
      console.log(e);
    }
  };
  return (
    <div>
      <div>
        <button onClick={onClick}>불러오기</button>
      </div>

      {data && (
        <textarea
          rows={7}
          value={JSON.stringify(data, null, 2)}
          readOnly={true}
        />
      )}
    </div>
  );
}

export default App;

 

arrow function 에 async/await 를 적용할 때는 async () =>{} 와 같은 형식으로 적용한다.

 

실행화면

 

그러면 두 형식 모두다 불러오기 버튼을 눌렀을 때 해당 데이터가 잘 받아와진다! 

 

그러면 newsapi 에서 제공하는 API 를 사용하여 최신 뉴스를 불러온 후에 보여주자.

 

https://newsapi.org/register

 

에 가입해서 

 

 

발급받아서 사용하면 된다!!

 

이 API key를 추후 API 를 요청할 때 API 주소의 쿼리 파라미터로 넣어서 사용하면 된다.

 

 

https://newsapi.org/s/south-korea-news-api

 

South Korea News API - Live top headlines from South Korea

Get live top and breaking news headlines from South Korea with our JSON API. Live example This example demonstrates the HTTP request to make, and the JSON response you will receive, when you use the News API to get live headlines from South Korea. Top head

newsapi.org

 

여기로 들어가면 한국 뉴스를 가져오는 API 에 대한 설명서가 적혀있음.

 

여기서 사용할 주소는 총 2가지!

 

 

 

 

전체 뉴스를 불러올 주소와,

 

 

 

카테고리별로 뉴스를 불러올 수 있다. 

 

여기서 apiKey= 부분에는 우리가 발급받은 API 키를 넣어주면 된다. 

 

 

src/App

 

아까 넣어줬던 JSONPlaceholder 예제 API 부분에 우리가 발급받은 API 키와 함께 넣어주고!!!! 

 

 

 

짠짠 이제 불러왔으면 얘를 화면에 예쁘게 띄울 UI 만들러 ㄱ ㄱ 

 

$ yarn styled-components

 

styled-components 를 설치하고, 투두리스트랑 똑같이 components 라는 디렉터리를 생성한 뒤에 작업하겠움!

 

 

 

 

여기서 데이터들을 살펴보면서 우리가 띄울 것들을 적어보자!!!

 

title 제목

description 내용

url  링크

urlToImage 뉴스 이미지 

 

이렇게 4가지를 띄워보자!

 

 

 

 

NewsItem 컴포넌트를 article 이라는 객체를 props 로 통째로 받아서 사용한다.  흐름은 밑에 부분에서 같이 정리!

 

작성 ㄱ

 

src/components/NewsItem.js ( 새로 생성 )

 

import React from 'react';
import styled from 'styled-components';

const NewsItemblock = styled.div`
  display: flex;

  .thumnail {
    margin-right: 1rem;
    img {
      display: block;
      width: 160px;
      height: 100px;
      object-fit: cover;
    }
  }

  .contents {
    h2 {
      margin: 0;
      a {
        color: black;
      }
    }
    p {
      margin: 0;
      line-height: 1.5;
      margin-top: 0%.5rem;
      white-space: normal;
    }
  }

  & + & {
    margin-top: 3rem;
  }
`;

const NewsItem = ({ article }) => {
  const { title, description, url, urlToImage } = article;
  return (
    <NewsItemblock>
      {urlToImage && (
        <div className="thumnail">
          <a href={url} target="_blank" rel="noopener noreferrer">
            <img src={urlToImage} alt="thumnnail" />
          </a>
          
        </div>
      )}

      <div className="contents">
        <h2>
            {/* black option 해당 링크를 새 탭에서 열게 함. 기본값은 self. 현재 프레임에 열기 */}
          <a href={url} target="_blank" rel="noopener noreferrer">
            {title}
          </a>
        </h2>
        <p>{description}</p>
      </div>
    </NewsItemblock>
  );
};

export default NewsItem;

 

스타일 컴포넌트를 이용해서 각 뉴스 아이템마다 들어갈 스타일을 정하고, 여기서 쓴 article props 는 이 아이템을 감싸는 NewsList 컴포넌트에서 API 를 요청해서 넘겨줄 것이다 . 여기서는 넘겨 받은 props 들의 위치들을 정해준 모습!

 

 

이제 그럼 NewsList 컴포넌트를 생성해줄텐데, 컴포넌트가 화면에 보이는 시점 에서 API를 요청할거기 때문에 useEffect 를 사용해서 요청해봅시단.

 

 

src/components/NewsList.js ( 새로 생성 )

import axios from 'axios';
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import NewsItem from './NewsItem';

const NewsListBlock = styled.div`
  box-sizing: border-box;
  padding-bottom: 3rem;
  width: 768px;
  margin: 0 auto;
  margin-top: 2rem;
  @media screen and (max-width: 768px) {
    width: 100%auto;
    padding-left: 1rem;
    padding-right: 1rem;
  }
`;

const NewsList = () => {
  const [articles, setArticles] = useState(null);
  //   요청이 대기 중일 때는 loading 이 ture, 요청이 끝나면 false .
  const [loading, setLoading] = useState(false);
  // 처음 렌더링 된 후에 API 요청할거니까 두번째 인자로 빈 배열 적어주기.
  // 주의 : useEffect 자체에 async 사용 불가. 왜 ? 컴포넌트가 언마운트 되기 전이나 업데이트 되기 직전에는 어떠한 작업을 수행하고 싶다면, 반환해야하는 값이 뒷정리 함수이기 때문. 따라서 내부에서 async 함수를 따로 만들어 주어야 함.
  useEffect(() => {
    const fetchData = async () => {
        // API 요청 완료전 loading true.
      setLoading(true);
      // API 요청 
      try {
        const response = await axios.get(
          'https://newsapi.org/v2/top-headlines?country=kr&apiKey=8bd8cfdce1834bdab3cdcc3cf1a00faf',
        );
        // articles 의 값을 API 요청으로 받아온 배열로 설정해줌.
        setArticles(response.data.articles);
      } catch (e) {
        console.log(e);
      }
     //  요청이 끝나면 loading false
      setLoading(false);
    };

    // 내부에 만들어준 fetchData 함수 실행
    fetchData();
  }, []);

  //  요청이 완료전, 즉 loading 값이 true 이면 해당 UI 띄우기
  if (loading) {
    return <NewsListBlock>로딩중....</NewsListBlock>;
  }
  // 아직 article 값이 설정되지 않았을 때. 왜 ? null 값을 조회하지 않으면, 아직 데이터가 없을 때 즉, null 일때는 map 함수가 없기 때문에 렌더링 과정에서 오류가 발생해 제대로 렌더링이 이루어지지 않는다. 꼭!! article 값이 비어있는지 없는지 검사를 해주자
  if (!articles) {
    return null;
  }
  return (
    //   article 값이 유효할 때
    <NewsListBlock>
    // 배열을 map 을 돌려서 하나하나 담기
      {articles.map((article) => (
        <NewsItem key={article.url} article={article} />
      ))}
    </NewsListBlock>
  );
};

export default NewsList;

 

 

주석으로 책의 내용을 적었지만, 뒷정리 함수를 반환해야 한다는 말이 아직 익숙하지 않아서 

https://ko.reactjs.org/docs/hooks-effect.html

 

Using the Effect Hook – React

A JavaScript library for building user interfaces

ko.reactjs.org

공식문서와 책의 HOOKS 부분을 참고했을 때, effect 함수를 실행시킬 때 뒷정리가 필요한 경우와 필요없는 경우가 있는데, 

우리는 지금 해당 작업이 effect 내에서 어떠한 함수를 실행시켜 작업해야 하기 때문에, 뒷정리 함수가 필요한 경우다. 이 때는 뒷정리 함수를 반환해야하는데, async 를 쓰지 못하게 됨. 그래서  useEffect 함수 내부 안에 async 함수를 따로 만들어서 실행시킨다.

 

아직 뒷정리가 필요한 경우와, 필요하지 않는 경우가 딱 나눠서 구분을 잘 하지는 못하겠..지만 외부에서 데이터를 요청할 때, 하나의 작업이 필요한 경우라서 뒷정리 함수를 반환해야한다라고 생각하고 넘어가겠음!

 

또 null 값을 검사해서 렌더링 오류가 나지 않게 해주었다..!

 

 

또또 response.data.articles 가 뭘까하고 console.log로 해당 데이터 요청 response 를 찍어보면,

 

 

 

이렇게 뜸 그러니까 우리는 data 에 있는 articles 배열을 통째로 props 로 설정값으로 NewsItem에 넘겨준 것

 

 

그리고 NewsItem 에서는 얘를 객체로 받아서 ( 리액트에서 props 는 설정값을 객체형태로 가져가니까 )

 

 

 

 

여기서 우리가 필요한 4개의 값을 객체 비구조화 할당해주고, urlImage 가 true, 즉 이미지가 있다면 thumnail 을 띄워주는 코드를 논리 연산자를 통해서 간편하게 조건부 렌더링을 해준 것!!!! 

 

 

그러면 NewsItem 의 각각에는 article 의 값이 담길거고, 그 아이템들을 NewsList 가 크게 감싼다 . 후!!!! 정말 개념이 쌓이고 쌓이는 만큼 보이고 모르는 것도 많아지는군. (ㅎ 제대로 쌓이고 있는지는 모르겠지만 )

 

 

이제 App 에서는 그럼 크게 감싸준 List 만 띄우면 되겠지! List 안에 각각 map 으로 돌린 Item 이 있으니까 ㅎㅎ! 

 

src/App

 

 

 

 

 

실행화면

 

 

굿.....!!!!!! 

 

 


 

 

전체뉴스를 보여줬으니 이제 카테고리별로 뉴스를 보여주는 기능을 작성해볼까!

 

https://newsapi.org/s/south-korea-news-api

 

아까 API 설명서에서 봤던 대로 이 카테고리를 사용할건데 우리는 한국인 . 한국말로 띄워줘보자.

 

 

 

src/component/Categories.js ( 새로 생성 )

import React from 'react';
import styled from 'styled-components';

const categories = [
  { name: 'all', text: '전체 뉴스' },
  { name: 'business', text: '비지니스' },
  { name: 'entertainment', text: '엔터테이먼트' },
  { name: 'health', text: '헬스' },
  { name: 'science', text: '과학' },
  { name: 'sports', text: '스포츠' },
  { name: 'technology', text: '기술' },
];

const CategoriesBlock = styled.div`
  display: flex;
  padding: 1rem;
  width: 768px;
  margin: 0 auto;
  @media screen and (max-width: 768px) {
    width: 100%;
    overflow-x: auto;
  }
`;

const Category = styled.div`
  font-size: 1.125rem;
  cursor: pointer;
  white-space: pre;
  text-decoration: none;
  color: inherit;
  padding-bottom: 0.25rem;

  &:hover {
    color: #495057;
  }

  & + & {
    margin-left: 1rem;
  }
`;

const Categories = () => {
  return (
    <CategoriesBlock>
      {categories.map((c) => (
        <Category key={c.name}>{c.text}</Category>
      ))}
    </CategoriesBlock>
  );
};

export default Categories;

 

배열에다가 name 과 text 값을 넣어주고 map 으로 key 값에다 실제 카테고리를 넣어주고, text는 렌더링할 때 사용할 UI 한글 카테고리로 각각 매칭 시켜주었다. 

 

 

 

이제 App 에서 컴포넌트를 불러와서 띄우는 것!!!! 이것만은 익숙해졌다 *^^*

 

 

 

그러면 상단에 카테고리가 잘 뜬당. API 요청이 덜끝나니까 로딩중도 잘 뜨넴.

 

 

src/App

 

이제 카테고리 값을 App 에서 state 로 관리해보게쑴. category 와 category 값을 업데이트 하는 함수를 Categories 컴포넌트에 props로 설정해준다. 일단 List 는 보지말고 카테고리부터 ㄱ ㄱ

 

 

src/components/Categories

import React from 'react';
// css import 
import styled, { css } from 'styled-components';

const categories = [
  { name: 'all', text: '전체 뉴스' },
  { name: 'business', text: '비지니스' },
  { name: 'entertainment', text: '엔터테이먼트' },
  { name: 'health', text: '헬스' },
  { name: 'science', text: '과학' },
  { name: 'sports', text: '스포츠' },
  { name: 'technology', text: '기술' },
];

const CategoriesBlock = styled.div`
  display: flex;
  padding: 1rem;
  width: 768px;
  margin: 0 auto;
  @media screen and (max-width: 768px) {
    width: 100%;
    overflow-x: auto;
  }
`;

const Category = styled.div`
  font-size: 1.125rem;
  cursor: pointer;
  white-space: pre;
  text-decoration: none;
  color: inherit;
  padding-bottom: 0.25rem;

  &:hover {
    color: #495057;
  }
  // active 값이 true 일 때 해당 스타일 실행 .
  ${(props) =>
    props.active &&
    css`
      font-weight: 600;
      border-bottom: 2px solid #ccccff;
      color: #ccccff;
      &:hover {
        color:#6495ED ;
        border-bottom: 2px solid #6495ED;
      }
    `}

  & + & {
    margin-left: 1rem;
  }
`;

const Categories = ({ onSelect, category }) => {
  return (
    <CategoriesBlock>
      {categories.map((c) => (
        <Category
          key={c.name}
        // 현재 category 값이 해당 name 과 같으면 true, 아니면 false 반환
          active={category === c.name}
        //   해당 카테고리를 클릭했을 때 해당 카테고리의 name 을 onSelcet 의 파라미터로 가져가서 카테고리 값을 업데이트 시킴.
          onClick={() => onSelect(c.name)}
        >
          {c.text}
        </Category>
      ))}
    </CategoriesBlock>
  );
};

export default Categories;

 

 

주석으로 적은 부분이 바뀌었는데, 여기서 props 의 자리를 짜란 하고 설정을 해주고 클릭했을 때 카테고리 값을 파라미터로 가져가고, 현재 카테고리 상태값과 내가 선택한 카테고리의 이름이 같으면 active 가 true 가 되는데, styled-component 에서는 해당 props 의 값을 바로 전달해줄 수 있었음! 그러니 논리연산자로 해당 값이 true 일 때의 스타일 바로 지정가능 ..!! 굿구수숙굿! 그리고 마우스를 가져다 댔을때, 즉 호버가 일어날 때는 다른 색깔로 지정해주었다! 

 

 

 

 

이렇게 카테고리에 따른 스타일 적용이 잘된다면 성공!

 

이제 NewsList 로 갈건데, 여기서는 API 요청을 하고 있으니까, 여기서는 카테고리별 API 요청을 하도록 만들어주면 되시겠다.

 

 

그러니 카테고리 값을 props 로 넘겨주자!

 

 

 

 

아까의 API 설명서에서 보면 카테고리를 받아올 때는 이렇게 해당 키워드가 추가가 되니까 우리는 해당 cateogry 값을 넣어서 API 요청을 해주시면 되겠군!

 

 

 

읏챠 받아와서 얘가 들어갈 자리를 마련해줘야지.

 

 

 

근데 우리는 이제 카테고리 값이 바뀔 때마다 렌더링을 해야하기 때문에 빈 배열을 넣으면 처음 한번만 렌더링 되므로 얘도 이제 배열에 추가해주자.

 

 

 

그리고 category 값을 요청하기 전에 전체면 카테고리 요청할 필요가 없으니 빈값으로 두고, 각각 카테고리를 누를 때마다 c.name 이 카테고리 값이 되니까 걔가 영어 카테고리였지 그러니 쨋든 이렇게 요청할 때 &category=카테고리값 으로 넣어줍씨다.

 

주의할 점

요청할 때 따옴표를 썼었는데, 우리는 중간에 문자열을 넣어야 하니 요청할 때 백틱으로 바꿔주자 ( 나는 이걸 못찾아서 한참 헤멤 ㅎ )

그리고 ? 뒤에 '' 으로 꼭 따옴표 붙여서 써줘야함. 중간에 스페이스 하나라도 들어가면 그거 그대로 공백 들어가서 제대로 된 요청을 못받아옴!!!!

 

 

 

실행화면

 

 

 

react developer tools 로 찍어보면 이렇게 active true  값으로 잘 들어가서 스타일도 잘 바뀌고,

 

 

console.log(response);

 

카테고리도 콘솔로그로 찍어보면 잘 받아와서 찍히는걸 보셨다면!!!! 성공!!!! 

 


 

음음 근데 우리는 링크로 갈 때 state 값 말고 다른걸 배웠지.. 라우터로 해보자!!!!!!!!!!

 

$ yarn add react-router-dom

 

src/index.js

 

인덱스에서 라우터 적용을 해주고 App 에서 state 적용대신에 새로운 컴포넌트에서 라우터를 적용시켜서 해볼까!

 

src/App

 

pages 라는 디렉토리 새로 생성하고, NewsPage 컴포넌트를 생성해서 여기다가 URL 파라미터를 받아와서 해줄 거임! 일단은 파일만 만들어주고 import 해주자.

 

 

다 필요없다 Route 컴포넌트에 :key 값으로 category 만 path 에 넣어주자.  그리고 ? 를 뒤에 붙였는데, 이는 카테고리의 값이 선택적이라는 의미이다. 있을 수도 있고 없을수도 있는 값이라고 정의! 없으면 전체 카테고리로 가도록 할 것임

 

댑악이네,,,, 근데 먼가 익숙하지 않는가 앞으로가 예상되지 않는가!!!!!!! 

 

예상되지 않는다면,

 

https://korinkorin.tistory.com/70?category=974678 

 

2021.08.03 리액트 라우터로 SPA 개발하기

2021.08.03 리액트 라우터로 SPA 개발하기_정리노트 드디어....SPA 까지 왔다... 힘들어따...... SPA(Single Page Application) 말 그대로 한 개의 페이지로 이루어진 애플리케이션 기존에는 사용자가 다른 페이

korinkorin.tistory.com

 

역시나 이 글을 읽기를 추천 *^^*

 

 

 

src/pages/NewsPage.js ( 새로 생성 )

 

import React from 'react';
import Categories from '../components/Categories';
import NewsList from '../components/NewsList';


const NewsPage = ({match}) => {
    // match.params.category가 false 이면 all 로
    const category = match.params.category || 'all' ;
    return (
        <div>
            <Categories />
            <NewsList category={category} />
            
        </div>
    );
};

export default NewsPage;

 

 

음음 우리가 그 때 리액트 라우터로 match.params.category 로 value 값을 정해줄 수 있다고 말했지!!!!! 끌끌 좋았어 여기 역시 false 이면 'all' 이라고 해주고 이렇게만 해주면 URL 파라미터로 해당 카테고리 값을 사용할 수 있다!!!

카테고리 컴포넌트에서 현재 선택된 카테고리 값을 알려 줄 필요도 없고, 그렇다면 카테고리 상태를 업데이트 해주는 onSelect 함수를 따로 전달해줄 필요가 없는 것이다!!! 알아서 파라미터로 잘 전달해주니까!

 

 

그렇다면 이제 남은 문제는 단 한가지 !

아까 카테고리에서 선택된 카테고리를 따로 스타일을 active 를 통해서 정해주었는데 그 부분도 NavLink 를 통해서 스타일링 해보자!

 

 

src/components/Categories

import React from 'react';
// NavLink import
import { NavLink } from 'react-router-dom';

import styled, { css } from 'styled-components';

const categories = [
  { name: 'all', text: '전체 뉴스' },
  { name: 'business', text: '비지니스' },
  { name: 'entertainment', text: '엔터테이먼트' },
  { name: 'health', text: '헬스' },
  { name: 'science', text: '과학' },
  { name: 'sports', text: '스포츠' },
  { name: 'technology', text: '기술' },
];

const CategoriesBlock = styled.div`
  display: flex;
  padding: 1rem;
  width: 768px;
  margin: 0 auto;
  @media screen and (max-width: 768px) {
    width: 100%;
    overflow-x: auto;
  }
`;

// html 요소가 아닌 컴포넌트에 사용할 때는 컴포넌트를 괄호로 감쌈.
const Category = styled(NavLink)`
  font-size: 1.125rem;
  cursor: pointer;
  white-space: pre;
  text-decoration: none;
  color: inherit;
  padding-bottom: 0.25rem;

  &:hover {
    color: #495057;
  }
  // active 클래스의 스타일 정의 
  &.active{
      font-weight: 600;
      border-bottom: 2px solid #ccccff;
      color: #ccccff;
      &:hover {
        color: #6495ed;
        border-bottom: 2px solid #6495ed;
      }
    }

  & + & {
    margin-left: 1rem;
  }
`;

const Categories = () => {
  return (
    <CategoriesBlock>
      {categories.map((c) => (
        <Category
          key={c.name}
          //  NavLink 에서는 링크가 활성화 되었을 때 active 이름의 class props 로 설정해주면 해당 클래스의 스타일이 적용됨.
          activeClassName="active"
          //   /가 true 여야 각자 정확한 경로에만 스타일을 적용함. 해당 값이 false 면 다른 카테고리에서도 전부 중복된 active 클래스가 나타남.
          exact={c.name === 'all'}
          //  all 이면 path는 '/' 나머지는 카테고리에 따라서 /카테고리이름 으로 경로 지정
          to={c.anme === 'all' ? '/' : `/${c.name}`}
        >
          {c.text}
        </Category>
      ))}
    </CategoriesBlock>
  );
};

export default Categories;

 

 

여기서 주석 부분만 보면 된다! html 태그가 아니라 NavLink 컴포넌트를 직접 사용했고, path 에 대한 경로 지정이기 때문에 all 일 경우 exact true 인 값을 설정해주었다.

 

 

 

이렇게 경로와 함께 스타일도 잘 적용되고, 해당 카테고리의 뉴스가 뜬다면 정말 !!!!!!! 완성!!!!!!! 

 

 

 

 


홀홀.. 분량 조절 실패지만 지금 또 새벽 3시 다 되어 가는데 난 진ㅉ ㅏ역시 찐 새벽파인가봄 저녁에는 횡설수설 적은거 겁나 많네..... 오늘꺼 쓰면서 저번에 썼던 포스팅들 정말 많이 고쳤닼ㅋㅋㅋㅋㅋㅋ 뜨문뜨문 조금 바뀐게 있다면 오늘 이후 일듯 ㅎ

모르겠다..ㅎ 그냥 운명이라 생각하고 시차 반대로 살아야겠다...ㅎㅎ 쨋든 맑은 정신으로 차근차근 치니 코드도 잘쳐지는듯! 오늘 포스팅 끝!

댓글