1.Styled-Component 코드 리팩토링
2.Polish 사용
저번에 만들었던 styled-component코드를 조금 정리하고 hover 되었을 때 파란색으로만 변하는 효과를
Polished 를 사용해서 고쳐주었다.
ButtonTotal.js
import React from "react";
import styled, { css } from "styled-components";
const ButtonTotal = ({ children, size, color, ...rest }) => {
const StyledButton = styled.button`
/*공통스타일*/
display: inline-flex;
outline: none;
border: none;
border-radius: 4px;
color: white;
font-weight: bold;
cursor: pointer;
padding: 2em;
margin: 10px;
/*크기*/
font-size: 1em;
/* 조건별색깔 */
${({ theme, color }) => {
const selectcolor = theme.palette[color];
return css`
background: ${selectcolor};
`;
}}
/*조건별크기*/
${(props) => {
return (
props.size === "large" &&
css`
height: 3.5em;
font-size: 1.5em;
width: 40%;
`
);
}}
${(props) => {
return (
props.size === "medium" &&
css`
height: 2.25em;
font-size: 1em;
`
);
}}
${(props) => {
return (
props.size === "small" &&
css`
height: 1.5em;
font-size: 0.75em;
`
);
}}
/*효과*/
&:hover {
background: #1c7ed6;
}
`;
return (
<StyledButton size={size} color={color} {...rest}>
{children}
</StyledButton>
);
};
ButtonTotal.defaultProps = {
size: "medium",
color: "blue"
};
export default ButtonTotal;
전에썻던 코드이다 StyledButton 쪽 코드에 조건으로 주는 값이 많아서 읽기 불편한 것 같다.
/*조건별크기*/
${(props) => {
return (
props.size === "large" &&
css`
height: 3.5em;
font-size: 1.5em;
width: 40%;
`
);
}}
${(props) => {
return (
props.size === "medium" &&
css`
height: 2.25em;
font-size: 1em;
`
);
}}
${(props) => {
return (
props.size === "small" &&
css`
height: 1.5em;
font-size: 0.75em;
`
);
}}
위의 이 코드를 지우고 스타일 컴포넌트 윗줄코드에
// 사이즈
const sizes ={
large:{
height: '3em',
fontSize: '1.25em',
width:'40%'
},
medium:{
height: '2.5em',
fontSize: '1em',
width:'25%'
},
small:{
height: '1.75em',
fontSize: '1em',
width:'15%'
},
}
const sizestyle = css`
${({size})=>css`
height:${sizes[size].height};
font-size:${sizes[size].fontSize};
width:${sizes[size].width};
`}
`;
이렇게 사이즈로 들어갈 sizes 객체를 하나 만들고ButtonTotal에 props로 받아오는 size 를 css 로 작성해서 sizestyle 에 저장해 준다. 그러면 스타일이 변수에 저장되게 되고 변수를 스타일 컴포넌트에 넣어주면 스타일 컴포넌트가 만들어지기 전에 미리 조건으로 스타일을 정해서 컴포넌트에 넣어주게 된다.
색깔지정코드
/* 조건별색깔 */
${({ theme, color }) => {
const selectcolor = theme.palette[color];
return css`
background: ${selectcolor};
`;
}}
이 코드도 스타일 컴포넌트 밖으로 빼서 변수에 css를 저장해 준 뒤에
저장된 변수만 스타일컴포넌트에 넣어주겠다.
const colorStyle=css`
${({theme,color})=>{
const selected = theme.palette[color];
return css`
background: ${selected};
&:hover {
background:${lighten(0.2,selected)}
}
`;
}}`;
위와 똑같이 스타일컴포넌트가 만들어지기 전에 ButtonTotal 에서 받은 props 를 이용해서 스타일을 만들고 css를 colorstyle 에 저장해 주었다
완성된 코드
import React from 'react';
import styled,{css} from 'styled-components';
import { lighten } from 'polished';
const ButtonTotal = ({children,color,size,fullWidth,...res}) => {
//배경색변수
const colorStyle=css`
${({theme,color})=>{
const selected = theme.palette[color];
return css`
background: ${selected};
&:hover {
background:${lighten(0.2,selected)}
}
`;
}}`;
// 사이즈
const sizes ={
large:{
height: '3em',
fontSize: '1.25em',
width:'40%'
},
medium:{
height: '2.5em',
fontSize: '1em',
width:'25%'
},
small:{
height: '1.75em',
fontSize: '1em',
width:'15%'
},
}
const sizestyle = css`
${({size})=>css`
height:${sizes[size].height};
font-size:${sizes[size].fontSize};
width:${sizes[size].width};
`}
`;
// 스타일컴포넌트
const StyledButton = styled.button`
/*공통스타일*/
display:inline-flex;
outline: none;
border:none;
border-radius:4px;
color:white;
font-weight:bold;
cursor:pointer;
padding: 1em;
justify-contents:center;
/*크기*/
font-size:1em;
margin: 0.5em;
align-items:center;
/*색상*/
${colorStyle}
/*크기스타일*/
${sizestyle}
//전체너비 100%스타일
${props=>{
return props.fullWidth &&
css `
width:100%;
`;
}}
&+&{
margin-left: 1em;
}
`;
return (
<StyledButton color={color} size={size} fullWidth={fullWidth} {...res}>{children}</StyledButton>
);
};
ButtonTotal.defaultProps={
color:'blue',
size:'medium'
}
export default ButtonTotal;
코드를 보면 스타일컴포넌트를 만들기 전에 변수에 css를 저장하고 css가 저장된 변수를 스타일 컴포넌트에 넣어서 사용해 준다. ${colorStyle} , ${sizestyle}
꼭 이렇게 코드를 작성하는 게 더 좋다는 건 아니고 이러면 보기에 조금 깔끔해 보이기는 하는데
내 개인적인 생각으로는 스타일 컴포넌트 자체가 모든 스타일을 컴포넌트 안에 정의해 줘서 코드를 간결하게 해 주는데 이렇게 밖에서 만들어서 적용해 주면 저 변수에 어떤 게 들어가는지 또 찾아가서 봐야 하고 좀 '굳이?'라는 생각이 들긴 한다. 취향인 쪽을 쓰거나 취업을 한다면 사수가 원하는 방향으로 써주면 되겠지... 그래도 나는 안에 다 들어있는 게 좋다고 생각함 왔다 갔다 하는 거 진짜 너무 힘들다.. 컴포넌트도 많아서 걔네들 돌아가면서 보는 것도 힘든데..
Polished 라이브러리-- wooah(우아한..)
Polished
저번에 sass와 styled-component를 설치할 때처럼 사용하려는 프로젝트 폴더로 이동해서 설치해 준다
npm install --save polished
이제 polish에 있는 기능을 import 해서 사용해 주면 된다. 여기서는 lighten이라는 기능을 사용해 줄 것이다.
https://polished.js.org/docs/#lighten
✨ polished | Documentation
A lightweight toolset for writing styles in JavaScript.
polished.js.org
스타일컴포넌트 아래에 있던
&:hover {
background: #1c7ed6;
}
요기를 지워주고
colorstyle css 에 같이 넣어주겠다.
import { lighten } from 'polished';
const ButtonTotal = ({children,color,size,fullWidth,...res}) => {
//배경색변수
const colorStyle=css`
${({theme,color})=>{
const selected = theme.palette[color];
return css`
background: ${selected};
&:hover {
background:${lighten(0.2,selected)}
}
`;
}}`;
여기 완성코드의 위쪽 부분을 잘라온 건데 lighten을 import 해주고 colorStyle 을 만들 때 색을 받아오기 때문에
colorstyle안에다 적어주고 지정된 색이 들어있는selected를 같이 사용해 주고 밝기를 조절해 준다
이제 여기에 삭제 버튼을 박스 안에 하나 추가해 주고 삭제버튼을 추가하면 대화창이 나오게 해 주겠다.
App.js
import "./styles.css";
import styled, { ThemeProvider } from "styled-components";
import ButtonTotal from "./ButtonTotal";
const AppBlock = styled.div`
width: 512px;
margin: 0 auto;
margin-top: 4em;
border: 1px solid black;
padding: 1em;
`;
export default function App() {
return (
<ThemeProvider
theme={{
palette: {
blue: "#228be6",
gray: "#495057",
pink: "#f06595"
}
}}
>
<AppBlock>
<div>
<ButtonTotal size="large">Button</ButtonTotal>
<ButtonTotal>Button</ButtonTotal>
<ButtonTotal size="small">Button</ButtonTotal>
</div>
<div>
<ButtonTotal size="large" color="pink">
Button
</ButtonTotal>
<ButtonTotal color="pink">Button</ButtonTotal>
<ButtonTotal color="pink" size="small">
Button
</ButtonTotal>
</div>
<div>
<ButtonTotal size="large" color="gray">
Button
</ButtonTotal>
<ButtonTotal color="gray">Button</ButtonTotal>
<ButtonTotal color="gray" size="small">
Button
</ButtonTotal>
</div>
<div>
<ButtonTotal fullWidth>Button</ButtonTotal>
</div>
<div>
<ButtonTotal fullWidth color="pink">
삭제
</ButtonTotal>
</div>
</AppBlock>
</ThemeProvider>
);
}
ButtonTotal컴포넌트를 하나 더 넣고 children으로 "삭제"를 넣어준다
버튼을 하나 추가
이제 저 삭제 버튼을 누르면 화면이 어두워지고 대화창? 같은 게 뜨도록 해주겠다.
Dialog.js
import React from 'react';
import styled from 'styled-components';
import ButtonTotal from './ButtonTotal';
//배경 컴포넌트
const DarkBackground = styled.div`
position:fixed;
left: 0;
top:0;
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items:center;
background: rgba(0,0,0,0.8);
`;
// 컨펌창 블럭
const DialogBlock = styled.div`
width:320px;
padding: 1.5em;
background: white;
border-radius: 2px;
h3{ //DialogBlock안에 있는 h3태그에적용
margin: 0;
font-size: 1.5em;
}
p{ //DialogBlock안에 있는 p태그에 적용
font-size: 1.125em;
}
`;
const Dialog = ({title,children,confirmText,cancelText}) => {
return (
<DarkBackground>
<DialogBlock>
<h3>{title}</h3>
<p>{children}</p>
<div>
<ButtonTotal color="gray">{confirmText}</ButtonTotal>
<ButtonTotal color="pink"}>{cancelText}</ButtonTotal>
</div>
</DialogBlock>
</DarkBackground>
);
};
export default Dialog;
어두운 배경이 될 DarkBackground 컴포넌트를 만들고 그 안에
확인창이 될 DialogBlock 컴포넌트를 만들어서 넣어줬다.
app.js 에서 props 로 {title, children, confirmText, cancelText}를 받아와서 내용으로 넣어준다.
ButtonTotal 컴포넌트를 import 해서 버튼을 만들어준다.
App.js
import "./styles.css";
import styled, { ThemeProvider } from "styled-components";
import ButtonTotal from "./ButtonTotal";
import Dialog from "./Dialog";
const AppBlock = styled.div`
width: 512px;
margin: 0 auto;
margin-top: 4em;
border: 1px solid black;
padding: 1em;
`;
export default function App() {
return (
<ThemeProvider
theme={{
palette: {
blue: "#228be6",
gray: "#495057",
pink: "#f06595"
}
}}
>
<AppBlock>
<div>
<ButtonTotal size="large">Button</ButtonTotal>
<ButtonTotal>Button</ButtonTotal>
<ButtonTotal size="small">Button</ButtonTotal>
</div>
<div>
<ButtonTotal size="large" color="pink">
Button
</ButtonTotal>
<ButtonTotal color="pink">Button</ButtonTotal>
<ButtonTotal color="pink" size="small">
Button
</ButtonTotal>
</div>
<div>
<ButtonTotal size="large" color="gray">
Button
</ButtonTotal>
<ButtonTotal color="gray">Button</ButtonTotal>
<ButtonTotal color="gray" size="small">
Button
</ButtonTotal>
</div>
<div>
<ButtonTotal fullWidth>Button</ButtonTotal>
</div>
<div>
<ButtonTotal fullWidth color="pink">
삭제
</ButtonTotal>
</div>
</AppBlock>
<Dialog
title="정말로 삭제하시겠습니까"
confirmText="삭제"
cancelText="취소"
>
데이터를 정말로 삭제하시겠습니까
</Dialog>
</ThemeProvider>
);
}
Dialog 컴포넌트도 내부에서 쓰고 있는 ButtonTotal 컴포넌트에서 ThemeProvider 값을 쓰고 있기 때문에
안쪽에 마운트 해준다.
Dialog 에 props로 title , confirmText , cacelText , 그리고 태그 사이에 있는 값인 children 을 전송해 준다.
지금은 아무것도 누르지 않았는데 Dialog가 마운트 되어서 보이게 된다. 이걸 삭제버튼을 클릭하면 mount가 보이도록 설정하고 삭제나 취소를 누르면 다시 사라지도록 해주겠다.
App.js
import "./styles.css";
import styled, { ThemeProvider } from "styled-components";
import ButtonTotal from "./ButtonTotal";
import Dialog from "./Dialog";
import {useState} from 'react';
const AppBlock = styled.div`
width: 512px;
margin: 0 auto;
margin-top: 4em;
border: 1px solid black;
padding: 1em;
`;
export default function App() {
const[dialog,setDialog]=useState(false); //dialog를 보이고 안보이게 해줄 상태값 false일때 안보임
function onClick(){
setDialog(true); //함수실행되면 true로 바뀌면서 dialog가 보이게 해줌
}
const onConfirm = ()=>{
console.log('확인')
//확인 버튼을 누르면 false가 되고 dialog가 사라짐
setDialog(false)
}
const onCancel = ()=>{
console.log('취소')
//취소 버튼을 누르면 false가 되고 dialog가 사라짐
setDialog(false)
}
return (
<ThemeProvider
theme={{
palette: {
blue: "#228be6",
gray: "#495057",
pink: "#f06595"
}
}}
>
<AppBlock>
<div>
<ButtonTotal size="large">Button</ButtonTotal>
<ButtonTotal>Button</ButtonTotal>
<ButtonTotal size="small">Button</ButtonTotal>
</div>
<div>
<ButtonTotal size="large" color="pink">
Button
</ButtonTotal>
<ButtonTotal color="pink">Button</ButtonTotal>
<ButtonTotal color="pink" size="small">
Button
</ButtonTotal>
</div>
<div>
<ButtonTotal size="large" color="gray">
Button
</ButtonTotal>
<ButtonTotal color="gray">Button</ButtonTotal>
<ButtonTotal color="gray" size="small">
Button
</ButtonTotal>
</div>
<div>
<ButtonTotal fullWidth>Button</ButtonTotal>
</div>
<div>
<ButtonTotal onClick={onClick} fullWidth color="pink">
삭제
</ButtonTotal>
</div>
</AppBlock>
<Dialog
title="정말로 삭제하시겠습니까"
confirmText="삭제"
cancelText="취소"
dialog={dialog}
onConfirm={onConfirm}
onCancel={onCancel}
>
데이터를 정말로 삭제하시겠습니까
</Dialog>
</ThemeProvider>
);
}
App에서 Dialog를 보이거나 보이지 않게 해 줄 상태값을 만들고 그 값을 변경하는 함수를 만들어서 Dialog에 props로 보내준다 onClick 함수는 삭제 버튼이 클릭되면 실행되어야 하기 때문에 ButtonTotal컴포넌트에 넣어준다.
ButtonTotal.js
import React from "react";
import styled, { css } from "styled-components";
import { lighten } from "polished";
// 스타일컴포넌트
const StyledButton = styled.button`
/*공통스타일*/
display:inline-flex;
outline: none;
border:none;
border-radius:4px;
color:white;
font-weight:bold;
cursor:pointer;
padding: 1em;
justify-contents:center;
/*크기*/
font-size:1em;
margin: 0.5em;
align-items:center;
/*색상*/
${(props) => props.colorstyle}
/*크기스타일*/
${(props) => props.sizestyle}
//전체너비 100%스타일
${(props) => {
return (
props.fullWidth &&
css`
width: 100%;
`
);
}}
&+&{
margin-left: 1em;
}
`;
const ButtonTotal = ({ children, color, size, fullWidth, ...res }) => {
//배경색변수
const colorstyle = css`
${({ theme, color }) => {
const selected = theme.palette[color];
return css`
background: ${selected};
&:hover {
background: ${lighten(0.2, selected)};
}
`;
}}
`;
// 사이즈
const sizes = {
large: {
height: "3em",
fontSize: "1.25em",
width: "40%"
},
medium: {
height: "2.5em",
fontSize: "1em",
width: "25%"
},
small: {
height: "1.75em",
fontSize: "1em",
width: "15%"
}
};
const sizestyle = css`
${({ size }) => css`
height: ${sizes[size].height};
font-size: ${sizes[size].fontSize};
width: ${sizes[size].width};
`}
`;
return (
<StyledButton
color={color}
size={size}
fullWidth={fullWidth}
colorstyle={colorstyle}
sizestyle={sizestyle}
{...res}
>
{children}
</StyledButton>
);
};
ButtonTotal.defaultProps = {
color: "blue",
size: "medium"
};
export default ButtonTotal;
ButtonTotal컴포넌트에서는 보내주는 onClick을 받아야 하기 때문에... res로 받고 스타일 버튼에도 다시 Props를 {... res}로 넘겨준다
Dialog.js
import React from "react";
import styled from "styled-components";
import ButtonTotal from "./ButtonTotal";
//배경 컴포넌트
const DarkBackground = styled.div`
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: rgba(0, 0, 0, 0.8);
`;
// 컨펌창 블럭
const DialogBlock = styled.div`
width: 320px;
padding: 1.5em;
background: white;
border-radius: 2px;
h3 {
margin: 0;
font-size: 1.5em;
}
p {
font-size: 1.125em;
}
`;
const Dialog = ({
title,
children,
confirmText,
cancelText,
dialog,
onCancel,
onConfirm
}) => {
if (!dialog) return null; //dialog가 false면 null을 리턴하고 아래의 리턴은 읽지않는다.
return (
<DarkBackground>
<DialogBlock>
<h3>{title}</h3>
<p>{children}</p>
<div>
<ButtonTotal color="gray" onClick={onConfirm}>
{confirmText}
</ButtonTotal>
<ButtonTotal color="pink" onClick={onCancel}>
{cancelText}
</ButtonTotal>
</div>
</DialogBlock>
</DarkBackground>
);
};
export default Dialog;
App에서 보내준 상태값과 상태값을 바꾸는 함수들을 Dialog에서 받아서 함수는 버튼에 클릭이벤트로 넣고 dialog는
조건문으로 dialog가 true일 때는 return null 이 되게 조건을 준다. 그러면 dialog에서 null을 return 해주기 때문에 결과적으로 아무것도 보이지 않는다.
삭제버튼을 클릭하면 dialog가 뜨고 확인이나 취소를 클릭하면 dialog가 사라지는 걸 확인할 수 있다.
이 제거의 다 왔다.. dialog가 생기고 없어질 때 지연시간을 주고 생길 때는 아래에서 올라오게 해주고 사라질 때는 아래로 내려가는 애니메이션 효과를 주겠다. 지연시간을 주려면 처음에 생길때는 크게 상관없지만 사라질때는 useEffect를 이용해서 값이 변경될 때 애니메이션을 실행시켜 주고 지연시간뒤에 애니메이션을 지워준뒤에 dialog가 사라지도록 해줘야 한다..
나도 아직 완전히 이해한 게 아니라서 최대한 풀어서 설명해도 이 정도가 나의 최선...
바로 시작!
Dialog.js
styled-components로 애니메이션만들기
styled-components 에서 keyframes 를 import해준다음 애니메이션을 만들어준다.
styled-componets에 애니메이션적용
컴포넌트 안에 animation이름을 넣어주고 애니메이션실행시간(animation-duration) 애니메이션 끝난 후상태(animation-fill-mode)를 설정해 줬다 지금은 컴포넌트가 생길 때 실행될 애니메이션만 추가되어 있고 사라질 때 실행될 애니메이션도 추가해 주겠다.
Dialog 에서 스타일컴포넌트에 disappear 라는 props를 보내주고 disappear가 true이면 아래의 css코드가 적용되면서 윗줄에 있는 animation-name 을 덮어써준다.
props.disappear
이렇게 disappear props 를 전달해 주는데 dialog가 false가 될 때(창이 사라질 때) → disappear는 true 가 되면서 slideDown, fadeOut 애니메이션 이 실행되게 해 준다.
이렇게 하면 실행될 것 같지만 실행되지 않는다
애니메이션이 실행되기 전에 이미 return null이 돼서 사라져 있기 때문..
그래서 애니메이션을 실행한 후에 사라지도록 조건과 값을 주겠다.
조건을 주기 위해 Dialog컴포넌트 안에 상태값(State)을 2개 만들어줬다 animate , localdialog
animate
dialog가 false가 돼서 사라질 때(사라지는 애니메이션 실행) true가 되고
애니메이션의 끝나는 시간인(animation-duration) 0.5초 뒤에 false 가 되게 해 준다.
(사라지는 애니메이션 지속시간 동안만 true가 되는 상태값)
localdialog
지금은 확인이나 취소버튼을 클릭해서 dialog 가 false로 바뀌면 바로 return null이 되는데
바로 return null이 되지 않게 해줘야 한다.
localdialog 에 dialog 값(true)을 넣어준다 dialog 가 false로 바뀌고 localdialog 가 true일 때는
return null을 하지 않고
animate 상태값을 true로 만들어주고 사라지는 애니메이션을 0.5초 실행 후 animate 값을 false로 바꿔준다
그 후에 localdialog 값(true)도 dialog값(false)과 똑같이 만들어주고
만약localdialog와 animate 가 둘 다 false라면 null을 리턴해준다.
if(localdialog && !dialog){ //확인이나취소버튼을 클릭해서 dialog가 false로 set된 시점
setAnimate(true); //사라지는 애니메이션실행
setTimeout(()=>setAnimate(false),500); //사라지는 애니메이션 종료시점 0.5초 뒤
}
setlocaldialog(dialog); //로컬dialog은false가 됨
if(!animate && !localdialog) return null; //fadeout,slideDown애니메이션이 끝나고 localdialog가 false면 null을 리턴
이런 조건문이 나오는데 이게 실행되어야 하는 시점이 dialog 값이 변하는 시점에 실행시켜 주면 된다.
값이 변하는 시점에 실행시켜주는 hook으로 useEffect 를 사용해 준다
useEffect(()=>{
//dialog값이 true에서 false로 바뀔때를확인. (확인이나취소버튼클릭하면)
if(localdialog && !dialog){
setAnimate(true); //fadeout,slideDown 애니메이션 실행중~
setTimeout(()=>setAnimate(false),500); //fadeout,slideDown 애니메이션 실행끝~
}
setlocaldialog(dialog); //localdialog는false가 됨
},[localdialog,dialog]) //localdialog,dialog값이 변하는시점에 실행
if(!animate && !localdialog) return null; //fadeout,slideDown애니메이션이 끝나고 localdialog가 false면 null을 리턴
이렇게 길고 긴 styled-component 프로젝트가 끝났다.. 마지막에 useEffect 가 압권이긴 했는데 리액트에 점점 익숙해져서 조금씩 이해가 되는 것 같아서 기분은 좋다,,
확실히 코드는 처음에는 이해가 안돼도 천천히 읽어보고 다시 만들어보면 뇌에 스며들게 되는 것 같다.. 물론 안 보고 똑같이는 아직 못 만들겠지만.. (사실 이것도 과제로 내주면 검색해 가면서 어떻게든 만들겠지..)