CSS om JS 관련 라이브러리로, JS안에 CSS를 작성하는 요령이다.
Tagged Template Literal
const name 'react';
const message = `hello ${name}`;
console.log(text);
위와같이 사용하는 문법이다. ` `사이로 온다.
const object = {a : 1};
const text = `${object}`
console.log (text) // "[object Object]"
하지만 안에 객체를 넣으면 위처럼 안의 내용이 제대로 표시되지 않는다.
const fn = () => true
const msg = `${fn}`;
console.log(msg);
// "() => true"
마찬가지로 함수를 넣어도 함수의 문자열 형태를 그대로 리턴하게 된다.
const red = '빨간색';
const blue = '파란색';
function favoriteColors(texts, ...values) {
console.log(texts);
console.log(values);
}
favoriteColors`제가 좋아하는 색은 ${red}과 ${blue}입니다.`
//(3) ['제가 좋아하는 색은 ', '과 ', '입니다.', raw: Array(3)]
//(2) ['빨간색', '파란색']
Template Literal을 사용하면 위같은 예제를 만들 수 있다.
즉, text와 value가 분리가 되서 위처럼 배열로 나타나게 된다.
const red = '빨간색';
const blue = '파란색';
function favoriteColors(texts, ...values) {
return texts.reduce((result, text, i) => `${result}${text}${values[i] ? `<b>${values[i]}</b>` : ''}`, '');
}
favoriteColors`제가 좋아하는 색은 ${red}과 ${blue}입니다.`
// 제가 좋아하는 색은 <b>빨간색</b>과 <b>파란색</b>입니다.
이를 좀 더 활용하면 위처럼 응용할 수 있다.
이러한 속성을 활용하면 styled component를 이용하여 props를 읽기 위해 사용될 수 있다.
좀더 자세히 알아보자.
function sample(texts, ...fns) {
const mockProps = {
title: '안녕하세요',
body: '내용은 내용내용 입니다.'
};
return texts.reduce((result, text, i) => `${result}${text}${fns[i] ? fns[i](mockProps) : ''}`, '');
}
sample`
제목: ${props => props.title}
내용: ${props => props.body}
`
/*
"
제목: 안녕하세요
내용: 내용은 내용내용 입니다.
"
*/
즉, sample함수를 실행하면 texts에는 제목,내용이 ...fns에는 ${props => props.tilte } 함수가 들어가게 된다.
그 이후 reduce문법을 사용해서 fns[i]가 있을때 인자로 넣어주는 값을 출력하게 하는 코딩이다.
자 이제 실제로 적용해보자.
yarn add styled-components
우선 리액트 프로젝트를 만들고 styled-components를 설치해 준다.
import React from 'react'
import './App.css';
import styled from 'styled-components'
const Circle = styled.div`
width : 5rem;
height : 5rem;
background : ${props => props.color};
border-radius : 50%;
`;
function App() {
return (
<Circle color = "blue"/>
);
}
export default App;
위처럼 color값을 props로 받아와서 style을 JS안에서 실행시킬 수 있다.
import React from 'react'
import './App.css';
import styled, { css } from 'styled-components' //css를 넣어야 ``가 styled-componets로 작동
const Circle = styled.div`
width : 5rem;
height : 5rem;
background : ${props => props.color};
border-radius : 50%;
${props => props.huge &&
//``안에 ``사용 가능 (styled-components로)
css`
width : 10rem;
height : 10rem;
` }
`;
function App() {
return (
<>
<Circle color = "blue" huge/>
<Circle color = "blue"/>
</>
);
}
export default App;
huge를 넣어서 그 값에따라 css를 변경하려면 위처럼 하면 된다. 이때 css ` `를 사용해야지 일반 tempelate Literal로 작동되지 않는다.
재사용성이 높은 버튼 만들기
이러한 sytled-component를 사용하여 버튼을 만들어보자 원리는 위에 설명했던것과 똑같다.
import React from 'react';
import styled from 'styled-components';
const StyledButton = styled.button`
display : inline-flex;
outline : none;
border : none;
border-radius : 4px;
color : white;
font-weight : bold;
cursor : pointer;
padding-left : 1rem;
padding-right : 1rem;
height : 2.25rem;
font-size : 1rem;
background : #228be6;
&:hover {
background : #228be6;
}
&:active {
background : #1c7ed6
}
& + &{
margin-left : 1rem;
}
`;
function Button({ children, ...rest}) {
return (
<StyledButton {...rest}>{children}</StyledButton>
);
}
export default Button;
코드를 작성하고 App.js에 연동만 시켜주면 된다.
import React from 'react'
import './App.css';
import styled, { css } from 'styled-components' //css를 넣어야 ``가 styled-componets로 작동
import Button from './components/Button';
const AppBlock = styled.div`
width : 512px;
margin : 0 auto;
margin-top : 4rem;
border : 1px solid black;
padding : 1rem;
`;
function App() {
return (
<>
<AppBlock>
<Button>BUTTON</Button>
</AppBlock>
</>
);
}
export default App;
Polished 스타일 유틸 함수
위 함수에 있는 여러 내장 함수들을 이용하면 편하게 버튼들을 관리할 수 있다.
yarn add polished
모듈을 깔고 사용하면 된다.
//App.js
// ...
const palette = {
blue : '#228be6',
gray : '#496057',
pink : '#f06595'
}
function App() {
return (
<ThemeProvider theme={{
palette
}}>
<AppBlock>
<Button>BUTTON</Button>
</AppBlock>
</ThemeProvider>
);
}
export default App;
ThemeProvider을 불러주고 위처럼 블럭을 감싸주고,
//Button.js
background : ${props => props.theme.palette.blue};
&:hover {
background : ${props => lighten(0.1, props.theme.palette.blue)}
}
&:active {
background : ${props => darken(0.1, props.theme.palette.blue)}
}
위 부분처럼 바꿔주어도 동일작동을 하게 된다. 즉, props의 color을 가져와서 사용할 수 있다.
${props =>{
const color = props.theme.palette.blue;
return css`
background : ${color};
&:hover {
background : ${lighten(0.1, color)}
}
&:active {
background : ${darken(0.1, color)}
}
`;
}}
props로 묶으면 굳이 색상마다 props.them.palette.blue 이렇게 쓰지 않아도 된다.
${({theme,color}) =>{
const selected = theme.palette[color];
return css`
background : ${selected};
&:hover {
background : ${lighten(0.1, selected)}
}
&:active {
background : ${darken(0.1, selected)}
}
`;
}}
비구조화 할당을 통하여 보다 간단하게 만들 수도 있다.
import React from 'react';
import styled, {css}from 'styled-components';
import {darken,lighten} from 'polished';
const colorStyles = css`
${({theme,color}) =>{
const selected = theme.palette[color];
return css`
background : ${selected};
&:hover {
background : ${lighten(0.1, selected)}
}
&:active {
background : ${darken(0.1, selected)}
}
`;
}}
`
const StyledButton = styled.button`
display : inline-flex;
outline : none;
border : none;
border-radius : 4px;
color : white;
font-weight : bold;
cursor : pointer;
padding-left : 1rem;
padding-right : 1rem;
height : 2.25rem;
font-size : 1rem;
${colorStyles}
& + &{
margin-left : 1rem;
}
`;
function Button({ children, color ,...rest}) {
return (
<StyledButton color ={color} {...rest}>{children}</StyledButton>
);
}
Button.defaultProps = { //기본값
color : 'blue'
}
export default Button;
위 접은글은 color까지 추가한 경우이다.
사이즈를 다양하게 하기
import React from 'react';
import styled, {css}from 'styled-components';
import {darken,lighten} from 'polished';
const colorStyles = css`
${({theme,color}) =>{
const selected = theme.palette[color];
return css`
background : ${selected};
&:hover {
background : ${lighten(0.1, selected)}
}
&:active {
background : ${darken(0.1, selected)}
}
`;
}}
`
const sizeStyles = css`
${props =>
props.size === 'large' &&
css`
height : 3rem;
font-size : 1.25rem;
`}
${props =>
props.size === 'medium' &&
css`
height : 2.25rem;
font-size : 1rem;
`}
${props =>
props.size === 'small' &&
css`
height : 1.75rem;
font-size : 0.875rem;
`}
`
const StyledButton = styled.button`
display : inline-flex;
outline : none;
border : none;
border-radius : 4px;
color : white;
font-weight : bold;
cursor : pointer;
padding-left : 1rem;
padding-right : 1rem;
${colorStyles}
${sizeStyles}
& + &{
margin-left : 1rem;
}
`;
function Button({ children, color , size, ...rest}) {
return (
<StyledButton color ={color} size = {size} {...rest}>{children}</StyledButton>
);
}
Button.defaultProps = { //기본값
color : 'blue',
size : 'medium'
}
export default Button;
//App.js
import React from 'react'
import './App.css';
import styled, { ThemeProvider , css } from 'styled-components' //css를 넣어야 ``가 styled-componets로 작동
import Button from './components/Button';
const AppBlock = styled.div`
width : 512px;
margin : 0 auto;
margin-top : 4rem;
border : 1px solid black;
padding : 1rem;
`;
const palette = {
blue : '#228be6',
gray : '#496057',
pink : '#f06595'
}
const ButtonGroup = styled.div`
& + & {
margin-top : 1rem;
}
`;
function App() {
return (
<ThemeProvider theme={{
palette
}}>
<AppBlock>
<ButtonGroup>
<Button size = 'large'>BUTTON</Button>
<Button color = "gray">BUTTON</Button>
<Button size = 'small'color = "pink">BUTTON</Button>
</ButtonGroup>
</AppBlock>
</ThemeProvider>
);
}
export default App;
const sizeStyles = css`
${props =>
props.size === 'large' &&
css`
height : 3rem;
font-size : 1.25rem;
`}
${props =>
props.size === 'medium' &&
css`
height : 2.25rem;
font-size : 1rem;
`}
${props =>
props.size === 'small' &&
css`
height : 1.75rem;
font-size : 0.875rem;
`}
`
마찬가지로 size를 props로 넣어주고 위 코드를 추가하면 사이즈별 버튼을 만들 수 있다.
const sizes = {
large : {
height : '3rem',
fontSize : '1.25rem'
},
medium : {
height : '2.25rem',
fontSize : '1rem'
},
small : {
height : '1.75rem',
fontSize : '0.875rem'
}
};
const sizeStyles = css`
/*크기*/
${({size}) => css`
height : ${sizes[size].height};
fontSize : ${sizes[size].fontSize};
`}
`;
위방식으로 조금 더 간단하게 쓸 수 있다.
이러한 방식이 유지보수할때 조금더 편할수 있는 방법일 순 있으나 굳이 할 필요는 없다.
outline,fulWidth
이번엔 bool값으로 설정을 넣어줄 것이다. 방법은 위와 동일하다!
import React from 'react';
import styled, {css}from 'styled-components';
import {darken,lighten} from 'polished';
const colorStyles = css`
${({theme,color}) =>{
const selected = theme.palette[color];
return css`
background : ${selected};
&:hover {
background : ${lighten(0.1, selected)}
}
&:active {
background : ${darken(0.1, selected)}
}
${props => props.outline &&
css `
color : ${selected};
background : none;
border : 1px solid ${selected};
&:hover {
background : ${selected}
color : white;
}
`}
`;
}}
`;
const fullWidthStyle = css`
${props => props.fullWidth && css`
width : 100%;
justify-content : center;
& + & {
margin-left : 0;
margin-top : 1rem;
}
`}
`;
const sizes = {
large : {
height : '3rem',
fontSize : '1.25rem'
},
medium : {
height : '2.25rem',
fontSize : '1rem'
},
small : {
height : '1.75rem',
fontSize : '0.875rem'
}
};
const sizeStyles = css`
/*크기*/
${({size}) => css`
height : ${sizes[size].height};
fontSize : ${sizes[size].fontSize};
`}
`;
const StyledButton = styled.button`
display : inline-flex;
outline : none;
border : none;
border-radius : 4px;
color : white;
font-weight : bold;
cursor : pointer;
padding-left : 1rem;
padding-right : 1rem;
& + &{
margin-left : 1rem;
}
${colorStyles}
${sizeStyles}
${fullWidthStyle}
`;
function Button({ children, color , size,outline,fullWidth, ...rest}) {
return (
<StyledButton color ={color} size = {size} outline = {outline} fullWidth = {fullWidth} {...rest}>{children}</StyledButton>
);
}
Button.defaultProps = { //기본값
color : 'blue',
size : 'medium'
}
export default Button;
//App.js
import React from 'react'
import './App.css';
import styled, { ThemeProvider , css } from 'styled-components' //css를 넣어야 ``가 styled-componets로 작동
import Button from './components/Button';
const AppBlock = styled.div`
width : 512px;
margin : 0 auto;
margin-top : 4rem;
border : 1px solid black;
padding : 1rem;
`;
const palette = {
blue : '#228be6',
gray : '#496057',
pink : '#f06595'
}
const ButtonGroup = styled.div`
& + & {
margin-top : 1rem;
}
`;
function App() {
return (
<ThemeProvider theme={{
palette
}}>
<AppBlock>
<ButtonGroup>
<Button size = 'large'>BUTTON</Button>
<Button color = "gray">BUTTON</Button>
<Button size = 'small'color = "pink">BUTTON</Button>
</ButtonGroup>
<ButtonGroup>
<Button size = 'large' outline>BUTTON</Button>
<Button color = "gray" outline>BUTTON</Button>
<Button size = 'small'color = "pink" outline>BUTTON</Button>
</ButtonGroup>
<ButtonGroup>
<Button size = 'large' fullWidth>BUTTON</Button>
<Button color = "gray" fullWidth>BUTTON</Button>
<Button size = 'small'color = "pink" fullWidth>BUTTON</Button>
</ButtonGroup>
</AppBlock>
</ThemeProvider>
);
}
export default App;
Styled-components Dialog만들기
이번엔 아래와 같은 dialog를 만들어 보겠다.
import React from 'react';
import styled from 'styled-components'
import Button from './Button';
const Darkbackground = styled.div`
position : fixed;
left : 0;
top : 0;
width : 100%;
height : 100%;
display: flex;
align-items : center;
justify-content : center;
background : rgba(0,0,0,0.8);
`;
const DialogBlock = styled.div`
width : 320px;
padding : 1.5rem;
background : white;
border-radius : 2px;
h3 {
margin : 0;
font-size : 1.5rem;
}
p {
font-size : 1.125rem;
}
`;
const ButtonGroup = styled.div`
margin-top : 3rem;
display : flex;
justify-content : flex-end;
`;
const ShortMarginButton = styled(Button)`
& + &{
margin-left : 0.5rem;
}
`; //상속받아서 사용
function Dialog({
title,
children,
confirmText,
cancelText,
visible,
onConfirm,
onCancel
}) {
if(!visible) return null; //visibile이 없으면 보여지지 않음
return (
<Darkbackground>
<DialogBlock>
<h3>{title}</h3>
<p>{children}</p>
<ButtonGroup>
<ShortMarginButton onClick = {onConfirm}color="gray">
{confirmText}
</ShortMarginButton>
<ShortMarginButton onClick = {onCancel}color="pink">
{cancelText}
</ShortMarginButton>
</ButtonGroup>
</DialogBlock>
</Darkbackground>
);
}
Dialog.defaultProps = {
cancelText : '취소',
confirmText : '확인'
}
export default Dialog;
//App.js
import React, {useState} from 'react'
import './App.css';
import styled, { ThemeProvider , css } from 'styled-components' //css를 넣어야 ``가 styled-componets로 작동
import Button from './components/Button';
import Dialog from './components/Dialog';
const AppBlock = styled.div`
width : 512px;
margin : 0 auto;
margin-top : 4rem;
border : 1px solid black;
padding : 1rem;
`;
const palette = {
blue : '#228be6',
gray : '#496057',
pink : '#f06595'
}
const ButtonGroup = styled.div`
& + & {
margin-top : 1rem;
}
`;
function App() {
const [dialog,setDialog] = useState(false); //보여질지 안보여질지 정하는 변수
const onClick = () =>{
setDialog(true);
}
const onConfirm= () =>{
console.log('확인');
setDialog(false);
}
const onCancel= () =>{
console.log('취소');
setDialog(false);
}
return (
<ThemeProvider theme={{
palette
}}>
<>
<AppBlock>
<ButtonGroup>
<Button size = 'large'>BUTTON</Button>
<Button color = "gray">BUTTON</Button>
<Button size = 'small'color = "pink">BUTTON</Button>
</ButtonGroup>
<ButtonGroup>
<Button size = 'large' outline>BUTTON</Button>
<Button color = "gray" outline>BUTTON</Button>
<Button size = 'small'color = "pink" outline>BUTTON</Button>
</ButtonGroup>
<ButtonGroup>
<Button size = 'large' fullWidth>BUTTON</Button>
<Button color = "gray" fullWidth>BUTTON</Button>
<Button size = 'small'color = "pink" fullWidth>BUTTON</Button>
</ButtonGroup>
<Button color = 'pink' size = 'large' onClick={onClick}>삭제</Button>
</AppBlock>
<Dialog
title = "정말로 삭제하시겠습니까?"
confirmText = "삭제"
cancelText = "취소"
onConfirm={onConfirm}
onCancel={onCancel}
visible = {dialog}
> 데이터를 정말로 삭제하시겠습니까?</Dialog>
</>
</ThemeProvider>
);
}
export default App;
간단하게 설명하면 회색 바탕화면들을 설정해두고, visible을 useState로 만든 이후에 그 값에 따라 보여지거나 안보여지거나를 설정하는 것이다.
그렇다면 나타나거나 사라질 때 좀더 부드럽게 부드럽게 전환하는 트렌지션을 구현해 보겠다.
keyframes
이전에 배운 keyframes를 사용하면 구현할 수 있다.
이를사용하기 위해서는
//Dialog.js
import styled, { keyframes } from 'styled-components'
불러줘야 사용할 수 있다.
//애니메이션 설정
const fadeIn = keyframes`
from{
opacity : 0;
}
to {
opacity : 1;
}
`
const slideUp = keyframes`
from{
transform : translateY(200px);
}
to {
transform : translateY(0px);
}
`
다음과 같이 애니메이션 설정을 해두고
const Darkbackground = styled.div`
position : fixed;
left : 0;
top : 0;
width : 100%;
height : 100%;
display: flex;
align-items : center;
justify-content : center;
background : rgba(0,0,0,0.8);
animation-duration : 0.25s;
animation-timing-function : ease-out;
animation-name : ${fadeIn}; /* 나타날때 transition */
animation-fill-mode : forwards; /* 유지 */
`;
const DialogBlock = styled.div`
width : 320px;
padding : 1.5rem;
background : white;
border-radius : 2px;
h3 {
margin : 0;
font-size : 1.5rem;
}
p {
font-size : 1.125rem;
}
animation-duration : 0.25s;
animation-timing-function : ease-out;
animation-name : ${slideUp};
animation-fill-mode : forwards; /* 유지 */
`;
animation 설정을 해주면 킬때 설정이 완성된다!
취소할때 transition을 적용하는 방법은 조금 더 어렵다
import React , {useState, useEffect }from 'react';
우선 useState와 useEffect를 사용해야 한다.
삭제버튼이 눌렀을때는 변하는 시점을 직접 설정해주어야한다.
fadeIn과 SlideUp을 보면 0.25초동안 애니메이션이 실행되는 것을 확인할 수 있다.
다르게 생각하면 버튼을 누르고 0.25초가 지난 이후에 화면전환이 이루어져야 함을 알 수있다. 그래서 useState와 useEffect를 통해서 변하는 시점을 캐치한 후에, 0.25초가 지나고 화면전환이 이루어지게 설정해두면 된다.
//App.js
import React, {useState} from 'react'
import './App.css';
import styled, { ThemeProvider , css } from 'styled-components' //css를 넣어야 ``가 styled-componets로 작동
import Button from './components/Button';
import Dialog from './components/Dialog';
const AppBlock = styled.div`
width : 512px;
margin : 0 auto;
margin-top : 4rem;
border : 1px solid black;
padding : 1rem;
`;
const palette = {
blue : '#228be6',
gray : '#496057',
pink : '#f06595'
}
const ButtonGroup = styled.div`
& + & {
margin-top : 1rem;
}
`;
function App() {
const [dialog,setDialog] = useState(false); //보여질지 안보여질지 정하는 변수
const onClick = () =>{
setDialog(true);
}
const onConfirm= () =>{
console.log('확인');
setDialog(false);
}
const onCancel= () =>{
console.log('취소');
setDialog(false);
}
return (
<ThemeProvider theme={{
palette
}}>
<>
<AppBlock>
<ButtonGroup>
<Button size = 'large'>BUTTON</Button>
<Button color = "gray">BUTTON</Button>
<Button size = 'small'color = "pink">BUTTON</Button>
</ButtonGroup>
<ButtonGroup>
<Button size = 'large' outline>BUTTON</Button>
<Button color = "gray" outline>BUTTON</Button>
<Button size = 'small'color = "pink" outline>BUTTON</Button>
</ButtonGroup>
<ButtonGroup>
<Button size = 'large' fullWidth>BUTTON</Button>
<Button color = "gray" fullWidth>BUTTON</Button>
<Button size = 'small'color = "pink" fullWidth>BUTTON</Button>
</ButtonGroup>
<Button color = 'pink' size = 'large' onClick={onClick}>삭제</Button>
</AppBlock>
<Dialog
title = "정말로 삭제하시겠습니까?"
confirmText = "삭제"
cancelText = "취소"
onConfirm={onConfirm}
onCancel={onCancel}
visible = {dialog}
> 데이터를 정말로 삭제하시겠습니까?</Dialog>
</>
</ThemeProvider>
);
}
export default App;
//Dialog.js
import React , {useState, useEffect }from 'react';
import styled, { keyframes ,css} from 'styled-components'
import Button from './Button';
//애니메이션 설정
const fadeIn = keyframes`
from{
opacity : 0;
}
to {
opacity : 1;
}
`
const slideUp = keyframes`
from{
transform : translateY(200px);
}
to {
transform : translateY(0px);
}
`
const fadeOut = keyframes`
from{
opacity : 1;
}
to {
opacity : 0;
}
`
const slideDown = keyframes`
from{
transform : translateY(0px);
}
to {
transform : translateY(200px);
}
`
const Darkbackground = styled.div`
position : fixed;
left : 0;
top : 0;
width : 100%;
height : 100%;
display: flex;
align-items : center;
justify-content : center;
background : rgba(0,0,0,0.8);
animation-duration : 0.25s;
animation-timing-function : ease-out;
animation-name : ${fadeIn}; /* 나타날때 transition */
animation-fill-mode : forwards; /* 유지 */
${props => props.disappear && css `
animation-name : ${fadeOut};
`}
`;
const DialogBlock = styled.div`
width : 320px;
padding : 1.5rem;
background : white;
border-radius : 2px;
h3 {
margin : 0;
font-size : 1.5rem;
}
p {
font-size : 1.125rem;
}
animation-duration : 0.25s;
animation-timing-function : ease-out;
animation-name : ${slideUp};
animation-fill-mode : forwards; /* 유지 */
${props => props.disappear && css `
animation-name : ${slideDown};
`}
`;
const ButtonGroup = styled.div`
margin-top : 3rem;
display : flex;
justify-content : flex-end;
`;
const ShortMarginButton = styled(Button)`
& + &{
margin-left : 0.5rem;
}
`; //상속받아서 사용
function Dialog({
title,
children,
confirmText,
cancelText,
visible,
onConfirm,
onCancel
}) {
const [animate,setAnimate] = useState(false); //보여주고 있다라는 정보
const [localVisible,setLocalVisible] = useState(visible); //현재 상태가 true에서 false로 변함을 감지
useEffect(() => {
//visible true => false의 시점 캐치
if ( localVisible && !visible) {
setAnimate(true);
setTimeout(() => setAnimate(false),250) //0.25초 이후에 삭제기능이 동작하게 됨
}
setLocalVisible(visible); //visible값이 바뀔때마다 해당값을 동기화시켜줌
}, [localVisible,visible]); //deps
if(!localVisible && !animate) return null; //visibile이 없으면 보여지지 않음
return (
<Darkbackground disappear = {!visible}> {/*disappear 값 전달*/}
<DialogBlock disappear = {!visible}>
<h3>{title}</h3>
<p>{children}</p>
<ButtonGroup>
<ShortMarginButton onClick = {onConfirm}color="gray">
{confirmText}
</ShortMarginButton>
<ShortMarginButton onClick = {onCancel}color="pink">
{cancelText}
</ShortMarginButton>
</ButtonGroup>
</DialogBlock>
</Darkbackground>
);
}
Dialog.defaultProps = {
cancelText : '취소',
confirmText : '확인'
}
export default Dialog;
'FrontEnd > React' 카테고리의 다른 글
20_리액트_라우터 (0) | 2021.12.30 |
---|---|
19_리액트 API연동 (0) | 2021.12.29 |
17_리액트_CSS Module (0) | 2021.12.24 |
16_리액트_컴포넌트스타일링 (0) | 2021.12.24 |
15_리액트_유용한 tool (0) | 2021.12.24 |