반응형

useCallback 은 useMemo와 비슷한 React-Hook이다. 값을 메모리에 저장해 두고 일반적으로 리렌더링 될 때에는 초기화되지 않고 값이 변경되는 것을 감지해서 값이 변경될 때만 함수를 실행시켜 주고 값을 업데이트해 준다.

 

차이점

useMemo(콜백함수, [ ]) 을 memoization (메모리에 저장)
useCallback(콜백함수, [ ]) 함수를 memoziation (메모리에 저장)

 

useCallback을 쓰는 이유 

 

자바스크립트에서 함수는 객체의 한 종류로 취급된다. 그래서 리렌더링 되면 함수가 초기화되면서 새로운 주소값을 할당받기 때문에 이전과 같은 모양의 함수라도 다른 함수로 취급된다. 그래서 함수를 저장하고 값이 변경되었을 때만 함수를 바꿔주는 hooks이 필요하다.

 

예제) 크기가 바뀌는 박스, 다크모드 

App.js

import { useState } from "react";
import Box from "./Box";
import "./styles.css";

export default function App() {
  const [size, setSize] = useState(100);
  const createBoxStyle = () => {
    return {
      width: `${size}px`,
      height: `${size}px`,
      backgroundColor: "turquoise"
    };
  };
  return (
    <div>
      <input
        type="number"
        value={size}
        onChange={(e) => {
          setSize(e.target.value);
        }}
      />
      <Box createBoxStyle={createBoxStyle} />
    </div>
  );
}

Box.js

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

const Box = ({ createBoxStyle }) => {
  const [style, setStyle] = useState({});

  useEffect(() => {
    console.log("👍👍박스 키우기");
    setStyle(createBoxStyle()); //스타일에 함수가반환하는 스타일값을 넣어줌
  }, [createBoxStyle]); //createboxstyle함수가 변경될때만 실행

  return <div style={style}></div>;
};

export default Box;

이렇게 인풋태그의 숫자가 변경되면 박스의 크기가 변경되고 useEffect로 createBox함수가 바뀔 때마다 '박스 키우기'가 콘솔에 뜨게 해 줬다. 지금은 이 코드가 잘 작동하지만 상태값을 하나 더 추가하면 문제가 생긴다. 

 

App.js (dark상태값추가 버튼태그추가)

import { useState } from "react";
import Box from "./Box";
import "./styles.css";

export default function App() {
  const [size, setSize] = useState(100);
  const [dark, setDark] = useState(false);
  const createBoxStyle = () => {
    return {
      width: `${size}px`,
      height: `${size}px`,
      backgroundColor: "turquoise"
    };
  };
  const style = {
    backgroundColor: dark ? "#333" : "#fff",
    color: dark ? "#fff" : "#333"
  };
  return (
    <div style={style}>
      <div>
        <input
          type="number"
          value={size}
          onChange={(e) => {
            setSize(e.target.value);
          }}
        />
        <Box createBoxStyle={createBoxStyle} />
        <button
          onClick={() => {
            setDark(!dark);
          }}
        >
          모드변경
        </button>
      </div>
    </div>
  );
}

이렇게 dark라는 상태값을 하나 추가해 주고 버튼을 누르면 상태값이 변경된다. 모드변경모드를 누를 때마다 상관없는 박스 키우기가 콘솔에 뜨게 되는데 이유는 dark상태값이 변경되면서 컴포넌트가 리렌더링 되고 createBox함수도 리렌더링 되면서 같은 모양의 함수이지만 다른 주소를 가지게 되면서 Box컴포넌트에서 useEffect가 다른 함수라고 인식하게 되어 실행되어서 그렇다. 그래서 이를 막기 위해서는 함숫값이 변경되지 않게 해줘야 한다. 사이즈가 변경될때만 함수를 다시호출하고 변경되지않을 때는 저장되어있는 함수를 그대로 이용해서 같은주소의 함수가 보내지게 해줘야한다.

 

useCallback을 이용해서 함수를 저장해 준다.

 

App.js

import { useCallback, useState } from "react";
import Box from "./Box";
import "./styles.css";

export default function App() {
  const [size, setSize] = useState(100);
  const [dark, setDark] = useState(false);
  const createBoxStyle = useCallback(() => { //박스스타일을 만들어주는 함수를 저장 
    return {								//size상태가 변경되면 실행
      width: `${size}px`,
      height: `${size}px`,
      backgroundColor: "turquoise"
    };
  }, [size]);

  const style = {
    backgroundColor: dark ? "#333" : "#fff",
    color: dark ? "#fff" : "#333"
  };
  return (
    <div style={style}>
      <div>
        <input
          type="number"
          value={size}
          onChange={(e) => {
            setSize(e.target.value);
          }}
        />
        <Box createBoxStyle={createBoxStyle} />
        <button
          onClick={() => {
            setDark(!dark);
          }}
        >
          모드변경
        </button>
      </div>
    </div>
  );
}

이렇게 써주면 useCallback이 boxstyle을 주던 함수를 저장하게 되고 size가 변경될 때만 함수가 다시 만들어진다.

사이즈가 변경되지 않았을 때는 원래 있던 함수를 box컴포넌트에 보내주고 주소가 이전과 같기 때문에 useEffect에서도 다시 실행되지 않는다.

 

 

저번 useMemo와 같이 useCallback도 값을 저장해 뒀다가 쓰는 방식이기 때문에 많이 쓰면 자원을 낭비하게 된다. 꼭 필요할 때 적절히 쓸 수 있도록 해야 한다. 아직은 언제 어떤 방식으로 써야 적절하게 쓰는 건지 감이 잘 오지는 않는다. 나중에 포트폴리오를 하면서 쓰다가 보면 감이 오겠지.

반응형
반응형

 

useMemo


useMemo
동일한 값을 리턴하는 함수를 반복적으로 호출해야 한다면 처음값을 계산할 때
그 값을  메모리에 저장해서 필요할때만 계산을 하고, 일반적으로 리렌더링 되는상황에서는

메모리에서 꺼내서 재사용하는 기법 자주 필요한 값을 처음 계산할 때 캐시에 저장을 해두어  
값이 필요할 때마다 다시 계산하지 않고 캐시에서 꺼내서 재사용. 값이 변경되면 다시 저장

 

사용법

구문 * deps == 의존성배열

useMemo(( )=>{
	return value;
},[deps])

콜백함수와 , 배열을 인자로 받아서 사용한다,

콜백함수는 메모이제이션(메모리에저장) 해줄 값을 리턴해주는 함수,

배열은 의존성 배열이라고도 부르는데 배열 안의 값이 업데이트될 때만 콜백함수를 호출해서

메모이제이션 된 값을 업데이트해서 다시 메모이제이션 해준다.

(저장된 값을 불러와서 변경한 후 다시 저장한다는 소리.) ,

의존성 배열이 없으면 처음마운트될 때 값이 메모이제이션되고 필요할 때마다 같은 값을 사용한다

(바뀌었는지 확인할 수 있는 값이 없으니 처음에 저장된 값을 계속 사용.)

 

App.js

import './App.css';
import { useState } from 'react';
import ShowState from './components/ShowState';

function App() {
  const [number,setNumber] = useState(0);
  const [text,setText] = useState("");
  const increaseNumber = ()=>{
    setNumber(number+1)
  }
  const decreaseNumber = ()=>{
    setNumber(number-1)
  }
  const onChange = (e)=>{
    setText(e.target.value)
  }

  return (
    <div className="App">
      <div>
        <button onClick={increaseNumber}>+1</button>
        <button onClick={decreaseNumber}>-1</button>
        <input type="text" placeholder='lastname' value={text} onChange={onChange}/>
      </div>
      <ShowState number={number} text={text}></ShowState>
    </div>
  );
}

export default App;

ShowState.js

import React from 'react';

const getNumber = (number)=>{
    console.log("숫자가 변동되었습니다.");
    return number
}
const getText = (text) =>{
    console.log("글자가 변동되었습니다.")
    return text;
}
const ShowState = ({number,text}) => {
    const showNumber = getNumber(number);
    const showText = getText(text);
    return (
        <div>
            {showNumber}<br/>
            {showText}
        </div>
    );
};
export default ShowState;

숫자를 변경해도 글자가 변동되었다는 문자가 같이 나온다

상태값이 변경될 때 컴포넌트가 리렌더링 되는데 다시 렌더링 되면서 함수를 다시 전부 실행해 줘서이다.

 

+1 버튼을 클릭하면 increaseNumber함수가 실행되고 setNumbernumber상태를 1 증가시켜 준다

상태가 변경되면서 리렌더링 되고 App, ShowState 컴포넌트를 다시 불러온다.

 

ShowState컴포넌트 내부의 코드

const showNumber = getNumber(number);
const showText = getText(text);

getNumber와 getText함수를 다시 실행하기 때문에 콘솔에 로그가 뜨게 된다. 

 

ShowState.js (useMemo사용)

import React, { useMemo } from "react";

const getNumber = (number) => {
  console.log("숫자가 변동되었습니다.");
  return number;
};
const getText = (text) => {
  console.log("글자가 변동되었습니다.");
  return text;
};
const ShowState = ({ number, text }) => {
  const showNumber = useMemo(() => {
    return getNumber(number);
  }, [number]); //number가 변경될때만 getNumber가 다시 실행된다.
  // const showText = useMemo(() => {
  //   return getText(text);
  // }, [text]);
  const showText = getText(text); //상태값이 변경되서 리렌더링 될때마다 getText다시실행행
  return (
    <div>
      {showNumber}
      <br />
      {showText}
    </div>
  );
};

export default ShowState;

useMemo를 사용하게 되면 의존성배열에 있는 값을 메모리에 저장했다가

값에 변화가 있을 때에만 콜백함수를 실행해서 값을 업데이트하고 다시 메모리에 저장해 준다.

위 코드에서처럼 useMemonumber값을 메모리에 저장하고 number값이 변경될 때만 실행된다

input에 글자를 입력하는 경우는 number값에 변화가 없기 때문에 getNumber함수가 실행되지 않는다.

 

반대로 showText값은 아까처럼  어떠한 상태값이 변경되어서 리렌더링 될 때마다

getText함수를 실행시켜서 콘솔에 

"글자가 변경되었습니다"라는 글이 나오게 된다

그렇지만 useMemo를 이렇게 사용하는 것은 별로 좋은 사용법이 아니다.

메모리의 자원을 점유하게 되기 때문에 자원을 낭비하게 되는 것이다.

꼭 필요한 경우에만 사용할 수 있도록 하자!! 

지금 같은 코드는 useEffect로 값이 변경될 때에만 실행시켜 주는 방식으로 충분히 문제를 예방할 수 있다.

 

ShowState.js (useEffect사용)

import React, { useEffect } from "react";

const getNumber = (number) => {
  console.log("숫자가 변동되었습니다.");
  return number;
};
const getText = (text) => {
  console.log("글자가 변동되었습니다.");
  return text;
};
const ShowState = ({ number, text }) => {
  const showNumber = useEffect(
    (number) => {
      return getNumber(number);
    },
    [number]
  );
  const showText = useEffect(
    (text) => {
      return getText(text);
    },
    [text]
  );
  return (
    <div>
      {showNumber}
      <br />
      {showText}
    </div>
  );
};

export default ShowState;

이렇게 메모리를 사용하지 않고도 해결할 수 있기 때문에 useMemo를 사용할 일이 많지는 않지만 

useEffect로 의존배열을 받을 때 객체는 넣어도 구분할 수 없다는 문제점이 있어서 이때는 useMemo를 사용해서 해결해 줄 수 있다.

 

App.js

import "./styles.css";
import React, { useEffect, useState } from "react";

const MemoComponent = () => {
  const [number, setNumber] = useState(0);
  const [isKorea, setIsKorea] = useState(true);

  const location = isKorea ? "한국" : "외국";
  //함수가 호출될때마다 항상 새로운객체가 생성됨
  // useEffect -->마운트될때, 리렌러링될때(업데이트) ,언마운트될때
  useEffect(() => {
    console.log("useEffect 호출");
  }, [location]);
  // 마운트될때와 location이변경될때 실행됨
  return (
    <div className='App'>
      <h2>좋아하는 숫자는?</h2>
      <input
        type="number"
        value={number}
        onChange={(e) => setNumber(e.target.value)}
      />
      <h2>이동하실까요</h2>
      <p>나라: {location}</p>
      <button onClick={() => setIsKorea(!isKorea)}>이동</button>
    </div>
  );
};

export default MemoComponent;

아까처럼 useEffect를 사용해서 숫자가 변경될 때에 location 값이 변경될 때에만 "useEffect를 사용 중입니다."라는 코드를 실행하게 해 주었다.

 

 

지금은 의존성 배열에 문자열값이 들어가는데 만약 여기에 객체타입이 들어간다면 어떻게 될까?

App.js

import "./styles.css";
import React, { useEffect, useState } from "react";

export default function App() {
  const [number, setNumber] = useState(0);
  const [isKorea, setIsKorea] = useState(true);

  const location = {
    country: isKorea ? "한국" : "외국"
  };

  useEffect(() => {
    console.log("useEffect 호출");
  }, [location]);

  return (
    <div className="App">
      <div>
        <h2>좋아하는 숫자는?</h2>
        <input
          type="number"
          value={number}
          onChange={(e) => setNumber(e.target.value)}
        />
        <h2>이동하실까요</h2>
        <p>나라: {location.country}</p>
        <button onClick={() => setIsKorea(!isKorea)}>이동</button>
      </div>
    </div>
  );
}

location을 객체로 반환받게 만들어줬다

타입이 객체로 바뀐 것뿐인데 숫자를 바꿔도 'useEffect호출'이 콘솔에 뜨는 걸 볼 수 있다.

 

이건 타입에서 생기는 문제인데 string타입은 변수에 값이 바로 담겨있기 때문에 useEffect에서 봤을 때

같은 값이면 실행시키지 않는다.

그러나 객체타입은 값을 바로 가지는 게 아니고 주소값을 가지기 때문에 상태값이 변경되어 리렌더링이 일어나고 location이 다시 실행되어서 같은 모양의 객체를 만들어도 가지고 있는 주소값이 이전과 다르기 때문에 useEffect에서 보기에는 다른 값이라고 여기게 되어서 리렌더링이 일어날 때마다

안의 console.log가 실행되게 된다.

 

이럴 때에는 useMemo를 이용해서 isKorea 가 변경될 때만

location이 실행되어서 country를 받을 수 있게 해 준다.

 

App.js

import "./styles.css";
import React, { useEffect, useMemo, useState } from "react";

export default function App() {
  const [number, setNumber] = useState(0);
  const [isKorea, setIsKorea] = useState(true);

  const location = useMemo(() => {
    return {
      country: isKorea ? "한국" : "외국"
    };
  }, [isKorea]);
  useEffect(() => {
    console.log("useEffect 호출");
  }, [location]);

  return (
    <div className="App">
      <div>
        <h2>좋아하는 숫자는?</h2>
        <input
          type="number"
          value={number}
          onChange={(e) => setNumber(e.target.value)}
        />
        <h2>이동하실까요</h2>
        <p>나라: {location.country}</p>
        <button onClick={() => setIsKorea(!isKorea)}>이동</button>
      </div>
    </div>
  );
}

이렇게 써주면 숫자가 변경될 때는 isKorea가 변하지 않기 때문에 {country: isKorea? '한국':'외국'} 구문을 실행하지 않게 되고 useEffect에서는 새로 객체를 만들지 않으니 똑같은 값이기 때문에 useEffect호출이 콘솔 로그되지 않는다.

 

아직 useMemo개념은 언제 써야 할지 잘 모르겠다... 조금 더 hooks에 익숙해지고 나면 잘 쓸 수 있겠지.. 지금 이 내용도 그때 다시 포스팅을 올려서 더 알기 쉬운 포스팅이 되었으면 좋겠다..

반응형
반응형

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로 받아오는 sizecss로 작성해서 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이 되지 않게 해줘야 한다.

localdialogdialog값(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가 압권이긴 했는데 리액트에 점점 익숙해져서 조금씩 이해가 되는 것 같아서 기분은 좋다,,

확실히 코드는 처음에는 이해가 안돼도 천천히 읽어보고 다시 만들어보면 뇌에 스며들게 되는 것 같다.. 물론 안 보고 똑같이는 아직 못 만들겠지만.. (사실 이것도 과제로 내주면 검색해 가면서 어떻게든 만들겠지..)

반응형

+ Recent posts