8.React/1)개념_React

React_개념_Day_02

구이제이 2024. 4. 17. 22:22

1

2

3

4

 

5

6

7

8

 

ㅡㅡㅡ

 




#인텔리제이에서 리엑트 써보기



#StyleExam.jsx

import React from "react";

 

const styles={ //자바스크립트이 객체로 취급되어 key와 value의 개념을 생각

 

    wrapper:{

        marginTop:10,

        marginBottom:10,

        border:"1px solid red",

        borderRadius:16,

        width:300

    },

    nameText:{

        // marginTop:10,

        // marginBottom:10,

        // marginRight:10,

        // marginLeft:10

        fontSize:40

    },

    content:{

        margin:10,

        float:"left"

    },

    contentSub:{

        padding:10

    }

 

}

 

 

 

 

//1) 2번에 담겨지는 함수

function StyleExam(props){

    return(

        <div style={styles.wrapper}>

            {/* style 직접 스타일을 설정할 때 */}

            <h1 className={styles.nameText}>{` 이름 : ${props.name} `}</h1>

            <h1>Hello {props.name}!!</h1>

            <h1 className={styles.nameText}>즐거운 시간~~~</h1>

        </div>

 

    );

}

//  ` 이것은 백틱입니다.

//  <div style={styles.wrapper}> : 스타일 객체에서 정의된 클래스일 것

// style과 className을 혼용해서 쓸수있다.?  뭐 클래스나 id처럼 호출하는데 다른것도 있지않나요?

 

 

 

 

 

 

 

 

 

//2)  1)번을 담고 있는 함수

function StyleExamList(props){

    return(

        <div className={styles.content}>

            <StyleExam name="홍길동" className={styles.contentSub} />

            <StyleExam name="김액트" className={styles.contentSub} />

        </div>

    );

 

}

 

 

 

export default StyleExamList;

 

ㅡㅡㅡ

 

 

#index.js

import React from 'react';

import ReactDOM from 'react-dom/client';

import './index.css';

import App from './App';

import reportWebVitals from './reportWebVitals';

import Fruit from './exam01/Fruit';

 

import Clock from './exam01/Clock';

import SecondCommentList from './exam01/SecondCommentList';

import StyleExamList from './exam02/StyleExam';

 

const root = ReactDOM.createRoot(document.getElementById('root'));

 

root.render(

  <React.StrictMode>

        {/* <SecondCommentList />

        <Fruit /> */}

        <StyleExamList />

      </React.StrictMode>,

      document.getElementById('root')

  );

 

/*

setInterval(function(){

  root.render(

  <React.StrictMode>

    <Clock />

    <Clock />

    <Clock />

      </React.StrictMode>,

      document.getElementById('root')

  );

}, 1000);

*/

 

 

// 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();

 

 

 

ㅡㅡㅡ

 

#결과





●2

 

#HOOK

 

Hook : 갈고리

 

#CountExam.jsx (유즈 스태이트훅)

import React, {useState} from "react"; //     ,{useState} : 훅을 쓸려면 이것을 입력

 

// Hook(훅) : 원래 존재하는 어떤 기능에 마치 갈고리를 거는 것 처럼

//                   끼어 들어가 같이 수행되는 것

//                   리액트의 state와 생명주기 기능에 갈고리를 걸어

//                   ★★★원하는 시점에 정해진 함수가 실행★★★되도록 만드는 것

// - state     : 리액트 컴포넌트의 변경 가능한 데이터, 각 개발자가 직접 정의해서 사용

//                      자바스크립트 객체

 

 

 

//유즈스태이트훅 :

//유즈이펙트훅  :

 

//컴포넌트는 class로 만들기도하고, function으로 만듭니다.

// 예전에는 class로 만들다가, 이후에는 둘다 섞어서, 지금은 function위주로 사용합니다.

 

function CountExam(){

    //  useState() : state 훅

    // [변수명, set함수명] = userState(초깃값)

    // "count" : 새로운 상태 값을 정의, count라는 변수명을 쓴것입니다.

    // count 값이 변경되면 컴포넌트가 ★★★다시 랜더링 ★★★되면서 화면에 새로운 count 값이 표시

   

    const [count, setCount] = useState(0);

 

    return(

        <div>

                <p>당신은 총 {count} 번 클릭했습니다.</p>

                <button onClick={() =>setCount(count +1)} >

                    {/* count 값이 변경되면 컴포넌트가 재렌더링되면서 화면에

                      새로운 카운트 값이 표시 */}

                Click Me

                </button>

        </div>

 

    );

 

 

    //카운트 값이 다시 랜더링이 안되는 기존의 자바스크립트이 방식입니다.(훅과 비교하기위해 기록했습니다.)

    // let count =0;

    // return(

    //     <div>

    //             <p>당신은 총 {count} 번 클릭했습니다.</p>

    //             <button onClick={ () => count++} >

    //                 {/* count 값이 변경되면 컴포넌트가 재렌더링되면서 화면에

    //                   새로운 카운트 값이 표시 */}

    //                 {/* <button onClick={test} : 함수호출법 {test} */}

    //             Click Me

    //             </button>

    //     </div>

//      );

 

 

 

}

 

export default CountExam;

 

ㅡㅡㅡ

 

 

#index.js

 

import React from 'react';

import ReactDOM from 'react-dom/client';

import './index.css';

import App from './App';

import reportWebVitals from './reportWebVitals';

import Fruit from './exam01/Fruit';

 

import Clock from './exam01/Clock';

import SecondCommentList from './exam01/SecondCommentList';

import StyleExamList from './exam02/StyleExam';

import CountExam from './exam02/CountExam';

 

const root = ReactDOM.createRoot(document.getElementById('root'));

 

root.render(

  <React.StrictMode>

        {/* <SecondCommentList />

        <Fruit /> */}

        {/* <StyleExamList /> */}

        <CountExam />

      </React.StrictMode>,

      document.getElementById('root')

  );

 

/*

setInterval(function(){

  root.render(

  <React.StrictMode>

    <Clock />

    <Clock />

    <Clock />

      </React.StrictMode>,

      document.getElementById('root')

  );

}, 1000);

*/

 

 

// 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();

 

 

#결과

 








●3

#CounterUseEffect(유즈이펙트훅)

import React, {useState, useEffect} from "react"; //useEffect 유즈이펙트훅 ,번외로 여러 훅 도 있습니다. ex)use에서 검색

// useEffect -> Effect 훅

 

//#

//side Effect(개발용어) 보통 사이트이펙트라고하면, 개발자의 개발입장에서 의도하지 않는 버그가 일어나는 현상을 사이드 이펙트라고 합니다.

//리엑트에서 side effect는 부정적인 용어가 아닙니다.

// side effect, 단 리엑트에서는 side effect가 부정적인 의미가 아님

// 리엑트에서 효과, 또는 영향을 의미

 

//useEffect() 훅은 리액트의 함수 컴포넌트에서 사이드 이펙트를 실행할 수 있도록 해주는 훅

//useEffect(이펙트 함수, 의존성 배열) : 배열 안에 있는 변수 중에 하나라도 값이 변경되면

//                                          이팩트 함수가 실행

//              이팩트 함수 mount(실행), unmount(끝낼때)시에 단 한번만 실행되게 하고 싶으면

//              의존성 배열에 빈 배열([])을 넣으면 됨

 

//              반대로 의존성 배열을 생략하면, 업데이트할때마다, 호출이 됩니다.

 

function CounterUseEffect(){

    const [count, setCount] = useState(0);

 

    // function(){} : 익명함수가 useEffect(()=> 이렇게 사용

    // 백틱(`) : 백틱을 사용하면, 계산식에 담게 해줍니다.

    // 여기는 의존성 배열을 생략하고 이펙트함수만 넣어놨습니다. 업데이트할떄마다, 랜더링이 된다는것(호출된다는 것)

    useEffect(()=>{

        //브라우저 API를 이용해 문서의 타이틀을 업데이트 함

        document.title = `당신은 총 ${count}번 클릭했습니다.`;

    });

 

    return(

        <div>

                <p>당신은 총 {count} 번 클릭했습니다.</p>

                <button onClick={()=> setCount(count+1)} >

                        Click Me

                </button>

        </div>

    );

}

//이펙트함수, 의존성배열, 넣으면

export default CounterUseEffect;

 

#결과

 






#훅의규칙

/*

Hook의 규칙

1. 무조건 "최상위 레벨"에서 "호출"해야합니다. ()

    반복문이나 조건문 또는 중첩된 함수들 안에서 훅을 호출하면 안된다는 의미

   

    컴포넌트가 렌더링될 때마다 매번 같은 순서로 호출되어야 함

    그래야 순서대로 랜더링이 진핸된다는 의미

 

#최상위 레벨이란?

    : 이 컴포넌트는 일반적으로 애플리케이션의 기본 구조를 정의하고,

    다른 컴포넌트들을 렌더링하거나 조직하는 역할을 합니다.

    따라서 리액트에서 "최상위 레벨"이란 보통 이러한 루트 컴포넌트를 의미합니다.

 

    #

    네, 일반적으로 리액트 애플리케이션에서 최상위 컴포넌트는 보통 index.js 파일에서 렌더링됩니다.

 

 

 

    #참고

    이 규칙을 따르면 컴포넌트가 렌더링 될 때마다 항상 동일한 순서로 Hook이 호출되는 것이 보장됩니다.

    이러한 점은 React가 useState 와 useEffect 가 여러 번 호출되는 중에도 Hook의 상태를 올바르게 유지할 수 있도록 해줍니다.

 

 

2. "리엑트 컴포넌트에서만" "훅을 호출"해야 함

     훅은 리액트 함수 컴포넌트에서 호출하거나 직접 만든 커스텀 훅에서만 호출할 수 있음

 

 

 

 

*/





#이벤트핸들러











●4ConfirmButton.jsx

 

#

import React, {useState} from "react";

 

function ConfirmButton(props){

    //        변수명            set함수명               = useState(초깃값)

    const[isConfirmed, setIsConfirmed] = useState(false);

 

 

    //#)화살표함수 (익명함수), ( 화살표 함수,와 람다 함수(자바에서불림))

       //클릭 이벤트 처리하기

       const handleConfirm = () =>{             //  () =>{      과 function(){  이것은 동일합니다.

        //                          prevIsConfirmed : 해당 함수의 바로 직전의 state가 전달됨

    setIsConfirmed( (prevIsConfirmed) => !prevIsConfirmed); //화살표함수 : 펑션 2개 안쓸려고 썻습니다.

    // setIsConfirmed(!isConfirmed)

 

    //이전값을 완전히 보존하고 싶어서 리턴하기 위해 이렇게 처리

}

 

 

    //#) 기본

    // //클릭 이벤트 처리하기

    // const handleConfirm = function(){

    //         //                          prevIsConfirmed : 해당 함수의 바로 직전의 state가 전달됨

    //     setIsConfirmed( (prevIsConfirmed) => !prevIsConfirmed); //화살표함수 : 펑션 2개 안쓸려고 썻습니다.

    //     // setIsConfirmed(!isConfirmed)

 

    //     //이전값을 완전히 보존하고 싶어서 리턴하기 위해 이렇게 처리

    // }

 

    return(

        <button onClick={handleConfirm} disabled={isConfirmed}>

            {isConfirmed ? "확인됨" : "확인하기"}

        </button>

    );

}

export default ConfirmButton;




#index.js

 





#NameForm.jsx

import React, {useState, useEffect} from "react";

 

function NameForm(props){

    //      state 훅 [변수명, set함수명]

    const [value, setValue] = useState(""); // useState("") : 빈 문자열 넣어줫습니다.

 

    //event : 이벤트 객체

    //event.target : 현재 발생한 이벤트의 타켓 (input element)

    const handleChange = (event) => {

       

        //set함수를 사용해서 새롭게 변경된 값을 value라는 이름의 state에 저장

        //현재 발생한 이벤트의 타켓 value 속성값((input element의 값)

        //셋벨류, 이벤트가 발생한 곳에 타겟 , 타겟 .value

        setValue(event.target.value);

    };

 

    const handleSubmit = (event) =>{

        //alert('입력한 이름 : ' + value);   //홑따옴표 처리

        alert(`입력한 이름 : ${value}`);//백틱 처리 //1. jstl과 비슷한 개념이라 보면되나요? 2. $은 jquery인가요? 그냥 사용방법인가요?

        event.preventDefault(); // 이벤트 취소

 

 

        //alert : 콘솔창에서  확인

        //document.write : 본문에서 확인

 

 

    };

 

 

    //

    return(

         <form onSubmit={handleSubmit}>

            {/*    value={value} :  리액트 컴포넌트의 state에서 값을 가져다 넣어 주는 것

                    그래서 항상 "state"에 들어 있는 값이 input에 표시 됨

 

                     onChange={handleChange} : 함수명을 호출하는 것

            */}

                이름 : <input type="text" value={value} onChange={handleChange} />

                <button type="submit">전송</button>

 

         </form>

    );

 

}

export default NameForm;

 

 

//어떤 것을 하려고 하시는지

 

 

 

#index.js

 

#결과







#        event.preventDefault(); 

//event.preventDefault()은 이벤트의 기본 동작을 취소하는 메서드입니다

 

 //관련 사이트

https://developer.mozilla.org/ko/docs/Web/API/Event/preventDefault

 








#앞으로 해야할 것(스프링부트와 연동)

#리엑트 라우터

#리엑트 테스터



●5



#SignUp.jsx

 

import React, {useState} from "react";

 

function SignUp(props){

 

    const [name, setName] = useState("");

    const [gender, setGender] = useState("여자");

 

    const handleChangeName = (event) => {

        setName(event.target.value);

    };

 

    const handleChangeGenger =(event) => {

        setGender(event.target.value);

    }

 

    const handleSubmit = (event) => {

        alert(`이름 : ${name}, 성별 : ${gender}`);

        event.preventDefault();

    };

 

    return(

        <form onSubmit={handleSubmit}>

            이름 : <input type="text" value={name} onChange={handleChangeName} />

            <br />

            성별 :

            <select value={gender} onChange={handleChangeGenger} >

                <option value="남자">남자</option>  

                <option value="여자">여자</option>

            </select>

            <button type="submit">전송</button>

        </form>

    );

 

 

}

 

export default SignUp;



#index.js

 

#결과

 

#

새터미널열어서 경로설정 c:\react > 설정

1.clear  : 콘솔 지움

2.cd..  :(cd점점) 이전의 경로로 들어갑니다. 

3.npx create-react-app miniproject(miniproject 프로젝트명) : 프로젝트 생성

 

#프로젝트명에 대문자 사용하면 에러납니다.



#정상 결과(소문자로만 구성 나머지는 - 로 연결)

 

#최상위로 올리기(저렇게 폴더를 설정하면 됩니다.)




●6

 

 

글 목록 보기 기능(리스트 형태) : PostList, PostListItem

 

글 보기 기능 : Post

 

댓글 보기 기능 : CommentList, CommentListItem

 

글 작성 기능 : PostWrite

 

댓글 작성 기능 : CommentWrite

 

—-----------------------------------------------------------

component

list : 리스트와 관련된 컴포넌트들을 모아 놓을 폴더

CommentList.jsx

CommentListItem.jsx

PostList.jsx

PostListItem.jsx

 

page : 페이지 콤포넌트들을 모아놓은 폴더

MainPage.jsx

PostViewPage.jsx

PostWritePage.jsx

 

ui : ui 컴포넌트들을 모아 놓은 폴더 

Button.jsx : 글작성/댓글 작성 완료 후 버튼을 눌렀을 때 내용을 저장

TextInput.jsx : 사용자로부터 문자열을 입력 받는데 사용

  글이나 댓글을 작성하기 위해 사용 

 

#Topㅡdown(설계할때 좋습니다. 큰그림 그리고 작은 부분을 구체화시킵니다.)

위에서부터 내려오는 방식

 

#buttomㅡUp(구현할때, (개발할때)에 좋습니다. 작은것부터 구현)

작은것부터 구현해서  올라가는 방식



#네이밍

모두가 알아 볼 수 있는 이름

설계,디자인,기획자 함께 작업



# npm install --save react-router-dom styled-components 입력

 

ㅡ참고

이 명령어는 npm(Node Package Manager)을 사용하여 두 가지 패키지를 설치하는 것입니다. 설치할 패키지는 다음과 같습니다:

  • react-router-dom: 리액트 애플리케이션에서 라우팅을 관리하기 위한 라이브러리입니다. 이를 사용하면 다른 URL에 따라 다른 컴포넌트를 렌더링할 수 있습니다. 예를 들어, 사용자가 /home, /about, /contact 등의 경로를 방문할 때 각각 다른 컴포넌트를 표시할 수 있습니다.
  •  
  • styled-components: 리액트 애플리케이션에서 CSS 스타일을 구성하는 데 사용되는 라이브러리입니다. 이를 사용하면 JavaScript 코드 내에서 CSS 스타일을 정의하고 컴포넌트에 스타일을 적용할 수 있습니다. 이를 통해 컴포넌트 기반의 스타일링을 구현할 수 있으며, CSS 클래스 이름 충돌과 같은 문제를 방지할 수 있습니다.

--save 옵션은 패키지를 프로젝트의 package.json 파일에 추가하고, dependencies 항목에 설치된 패키지의 버전 정보를 저장합니다. 이를 통해 프로젝트를 다른 환경에서 실행할 때 필요한 패키지를 쉽게 설치할 수 있습니다.

 

#스타일 사이트

(https://styled-components.com/docs/basics#pseudoelements-pseudoselectors-and-nesting)




●7

 

#배열은 맵으로 처리했습니다.

[]대괄호 : 배열

 

 

#data.json

 

[

{

    "id": 1,

    "title" : "react 테스트1",

    "content" : "hello~~\n",

    "comments": [

        {

            "id" : 11,

            "content" : "json 데이터 댓글 연습11"

        },

        {

            "id" : 12,

            "content" : "json 데이터 댓글 연습12"

        },

        {

            "id" : 13,

            "content" : "json 데이터 댓글 연습13"

        },

        {

            "id" : 14,

            "content" : "json 데이터 댓글 연습14"

        },

        {

            "id" : 15,

            "content" : "json 데이터 댓글 연습15"

        }

    ]

},

{

    "id": 2,

    "title" : "react 테스트2",

    "content" : "hello~~\n",

    "comments": [

        {

            "id" : 21,

            "content" : "json 데이터 댓글 연습21"

        },

        {

            "id" : 22,

            "content" : "json 데이터 댓글 연습22"

        },

        {

            "id" : 23,

            "content" : "json 데이터 댓글 연습23"

        },

        {

            "id" : 24,

            "content" : "json 데이터 댓글 연습24"

        },

        {

            "id" : 25,

            "content" : "json 데이터 댓글 연습25"

        }

    ]

}

 

 

]

 

 

 

#CommentList.jsx

 

import React from 'react';

import styled from 'styled-components';

import CommentListItem from './CommentListItem';

 

const Wrapper = styled.div`

    display: flex;

    flex-direction: column;

    align-items: flex-start;

    justify-content: center;

 

    :not(:last-child) {

        margin-bottom: 16px;

    }

`;

 

function CommentList(props) {

    const { comments } = props; //  const { comments } : 배열로 들어오고 있습니다.

 

    return (

        <Wrapper>

            { /* 배열.map() : 각 댓글 객체를 CommentListItem */}

            {comments.map((comment, index) => {

                // 맵 키와 밸류으로 처리합니다.

                return (

                    <CommentListItem key={comment.id} comment={comment} />

                );

            })}

        </Wrapper>

    );

}

 

export default CommentList;

 

#CommentListItem.jsx

 

import React from "react";

import styled from "styled-components";

 

const Wrapper = styled.div`

    width: calc(100% - 32px);

    padding: 8px 16px;

    display: flex;

    flex-direction: column;

    align-items: flex-start;

    justify-content: center;

    border: 1px solid grey;

    border-radius: 8px;

    cursor: pointer;

    background: white;

    :hover {

        background: lightgrey;

    }

`;

 

const ContentText = styled.p`

    font-size: 16px;

    white-space: pre-wrap;

`;

 

function CommentListItem(props) {

    //props에서 comment 객체 하나만 사용

    // comment : 사용자가 작성한 댓글 내용이 들어 있음

    const { comment } = props;

 

    return (

        <Wrapper>

             {/* styled-components를 통해 만든 ContentText 컴포넌트를 이용해서

                    화면에 표시

             */}

            <ContentText>{comment.content}</ContentText>

        </Wrapper>

    );

}

 

export default CommentListItem;

 

#PostList.jsx

 

import React from 'react';

import styled from 'styled-components';

import PostListItem from './PostListItem';

 

const Wrapper = styled.div`

    display: flex;

    flex-direction: column;

    align-items: flex-start;

    justify-content: center;

 

    :not(:last-child) {

        margin-bottom: 16px;

    }

`;

 

function PostList(props) {

    const { posts, onClickItem } = props;

 

    return (

        <Wrapper>

            {posts.map((post, index) => {

                return (

                    <PostListItem

                        key={post.id}

                        post={post}

                        onClick={() => {

                            onClickItem(post);

                        }}

                    />

                );

            })}

        </Wrapper>

    );

}

 

export default PostList;



#PostListItem.jsx

 

import React from "react";

import styled from "styled-components";

 

const Wrapper = styled.div`

    width: calc(100% - 32px);

    padding: 16px;

    display: flex;

    flex-direction: column;

    align-items: flex-start;

    justify-content: center;

    border: 1px solid grey;

    border-radius: 8px;

    cursor: pointer;

    background: white;

    :hover {

        background: lightgrey;

    }

`;

 

const TitleText = styled.p`

    font-size: 20px;

    font-weight: 500;

`;

 

function PostListItem(props) {

    const { post, onClick } = props;

 

    return (

        <Wrapper onClick={onClick}>

            <TitleText>{post.title}</TitleText>

        </Wrapper>

    );

}

 

export default PostListItem;



#MainPage.jsx

 

import React from 'react';

import { useNavigate } from 'react-router-dom';

import styled from 'styled-components';

import PostList from '../list/PostList';

import Button from '../ui/Button';

import data from '../../data.json';

 

const Wrapper = styled.div`

    padding: 16px;

    width: calc(100% - 32px);

    display: flex;

    flex-direction: column;

    align-items: center;

    justify-content: center;

`;

 

const Container = styled.div`

    width: 100%;

    max-width: 720px;

 

    :not(:last-child) {

        margin-bottom: 16px;

    }

`;

 

 

function MainPage(props){

    // react-router-dom의 useNavigate();  : 훅입니다.

    // 페이징 이동을 위해 사용

    const navigate = useNavigate();

 

    return(

        <Wrapper>

            {/* 대문자 : 첫글자가 대문자로나오면 사용자정의태그(xml태그)

 

            소문자 :  */}

 

            <Container>

                {/* 글을 작성할 수 있는 버튼 */}

                <Button title="글 작성하기" onClick={() => {

                            navigate('/post-write');

                }} />

 

 

 

                {/* 글 목록을 보여주는 부분 */}

                <PostList

                    posts={data} onClick={(item)=>{

                        navigate('/post/${item.id}');

                    }}

                />

 

            </Container>

        </Wrapper>

 

    );

 

 

}

 

export default MainPage;

 

 

#Button.jsx

 

import React from "react";

import styled from "styled-components";

 

const StyledButton = styled.button`

    padding: 8px 16px;

    font-size: 16px;

    border-width: 1px;

    border-radius: 8px;

    cursor: pointer;

`;

 

function Button(props) {

    // props로 받은 title이 버튼에 표시되도록

    // props로 받은 onClick은 StyledButtond의 onClick에 넣어 줌으로써

    // 클릭 이벤트를 상위 컴포넌트에서 받을 수 있도록 설정

   

 

    const { title, onClick } = props;

 

    return <StyledButton onClick={onClick}>{title || "button"}</StyledButton>;

}

 

export default Button;



#TextInput.jsx



import React from "react";

import styled from "styled-components";

 

const StyledTextarea = styled.textarea`

    width: calc(100% - 32px);

    ${(props) =>

        props.height &&

        `

        height: ${props.height}px;

    `}

    padding: 16px;

    font-size: 16px;

    line-height: 20px;

`;

 

function TextInput(props) {

    const { height, value, onChange } = props;

 

    return <StyledTextarea height={height} value={value} onChange={onChange} />;

}

 

export default TextInput;




●8 자습

 

'8.React > 1)개념_React' 카테고리의 다른 글

React_개념_Day_05_02  (0) 2024.04.25
React_개념_Day_05_01  (1) 2024.04.25
React_개념_Day_04  (0) 2024.04.24
React_개념_Day_03  (1) 2024.04.18
React_설치와개념_Day_01  (0) 2024.04.16