-
320x100
Abstract vector created by fullvector - www.freepik.com 이 글은 NomadCoders의 <React JS 마스터클래스>와 <벨로퍼트와 함께하는 모던 리액트 강의 노트>의 내용 일부를 정리한 글입니다.
🙇🏻♀️ 멋진 강의 감사합니다 🙇🏻♀️
개인 스터디 글로, 맞지 않는 내용이나 더 나은 방법을 공유해 주신다면 복받으실 거예요 👼✨
1) 시작하기
2) 기능 연습 & 3) 앱 만들기
4) styled-components ☀︎
5) 𝒘𝒊𝒕𝒉 타입스크립트
4-1. 스타일드 컴포넌트?
Styled Components는 컴포넌트 단위로 스타일링하기 때문에 개별 케이스로 분리해 CSS를 작성할 수 있어요.
이렇게 분리하지 않고 스크립트 내 작성하는 방식을 CSS-in-JS 라고 합니다.
CSS-in-JS 방식을 사용하는 라이브러리에는 Styled Components, emotion, styled-jsx, JSS 등이 있습니다.
여기서는 스타일드 컴포넌트를 다루는 법을 배워봅니다!
스타일드 컴포넌트의 장점은 아래와 같습니다.
- 페이지에서 렌더링되는 요소에 맞춰 자동으로 해당 스타일만 삽입합니다. 그때그때 필요한 스타일만 로드한다는 거죠!
- 스타일에 대한 고유 클래스명을 생성합니다. 중복이나 오타 걱정 노놉
- 더이상 사용되지 않는 CSS를 쉽게 삭제할 수 있습니다. 모든 스타일은 특정 요소와 연결되어 있기 때문에 해당 요소가 삭제되면 스타일도 삭제돼요.
- 동적 스타일링이 편해집니다. 이 props가 있다면 A, 없다면 B와 같이 직관적으로 개별 스타일링이 가능해요.
자세한 문서 내용은 여기를 참고하세요 👇👇
https://styled-components.com/docs
styled-components: Documentation
Learn how to use styled-components and to style your apps without stress
styled-components.com
4-2. 시작하기!
4-2-1. 설치
시작하려면 아묻따 뭐다? 패키지 설치!
$ npm i styled-components
그리고 이걸 styled라는 이름으로 가져와서 씁니다.
import styled from 'styled-components';
만만한 건 역시 버튼이니까 버튼 컴포넌트를 하나 만들어 보죠!
const Button = styled.button``;
유의할 점 세가지!
1. 컴포넌트 이름은 대문자로 시작
2. styled 뒤에 사용해야 할 HTML 태그명 입력
3. 백틱(``)으로 감싸서 스타일시트 작성import styled from 'styled-components';const Button = styled.button`display: block;padding: 6px 10px;color: #fff;font-size: 18px;border-radius: 3px;background-color: crimson;border: 0;`;function App() {return (<div><Button>나는 버튼!</Button></div>);}export default App;TIP. styled-components 내에서 emmet을 쓰고 싶다면 vscode-styled-components 을 설치합니다.
컴포넌트 단위이기 때문에 컴포넌트 내에 컴포넌트를 넣을 수도 있고,
sass의 중첩 문법이나 부모 선택 참조자
&
를 사용할 수도 있어요!뿐만 아니라 다른 컴포넌트를
${}
로 데려와 하위에 작성할 수도 있고요.import styled from 'styled-components';const Title = styled.div`color: darkolivegreen;h1 {font-size: 30px;margin: 0 0 10px 0;}`;const Notice = styled.div`padding: 20px;border: 2px solid #aaa;${Title}:hover {color: crimson;}`;const Button = styled.button`display: block;padding: 6px 10px;color: #fff;font-size: 18px;border-radius: 3px;background-color: crimson;border: 0;&:hover {background-color: teal;}`;function App() {return (<div><Notice><Title><h1>아아 공지를 읽으세요 📢</h1><h2>작성자: nykim</h2></Title><Button>확인</Button></Notice></div>);}export default App;4-2-2. 확장하기
한편, 컴포넌트를 확장해서 쓸 수도 있어요.
Sass의 @mixin은 공통 속성을 정의하고 인자로 스타일을 살짝 다르게 설정해 줄 수 있었는데,
이와 유사하게 스타일드 컴포넌트에서도 props로 이래라저래라 해줄 수 있습니다.
import styled from 'styled-components';const Button = styled.button`background-color: ${(porps) => porps.bgColor || 'lightgray' };display: block;// ...`;function App() {return (<div><Button bgColor="skyblue">확인</Button><Button bgColor="salmon">취소</Button></div>);}export default App;그럼 props로 bgColor가 있을 경우 그 값이 적용되고, 아니라면 ‘lightgray’ 값이 적용 되겠죠.
이렇게 하면 우리는 컴포넌트내 클래스를 전혀 달지 않았음에도 불구하고, 리액트에서 알아서 클래스네임을 각각 적용해줍니다.
만약 여러 줄의 코드를 조건부로 설정하고 싶다면 { css } 를 별도로 가져와 써야 합니다.
import styled, { css } from 'styled-components';const Button = styled.button`// ...${(props) =>props.special &&css`background-color: red;font-size: 32px;`};`;function App() {return (<div><Button bgColor="skyblue" special>확인</Button><Button bgColor="salmon">취소</Button></div>);}export default App;그럼 Sass의 @extend 처럼 쓸 수도 있지 않을까요? 한번 해봅시다!
새로운 컴포넌트를 만들 때, 거기에 쓰일 기본 속성들을 다른 스타일드 컴포넌트로부터 확장해서 쓰는 거죠.
사용법은 간단합니다. styled 뒤에 점 대신 괄호()로 기본형이 될 컴포넌트를 넣습니다.
import styled from 'styled-components';const Button = styled.button`//...`;const FullButton = styled(Button)`width: 100%;border-radius: 4px;`;function App() {return (<div><FullButton bgColor="skyblue">확인</FullButton><Button bgColor="salmon">취소</Button></div>);}export default App;그럼 props를 받아와 처리하는 것도 동일하기 때문에 bgColor 를 그대로 사용할 수 있어요.
또 이렇게 사용할 수도 있습니다. 스타일은 그대로 유지하되, 마크업만 바꿔치는 거죠!
예를 들면 button 의 스타일을 유지한 채 <button> 태그가 아닌 <a> 태그로 쓰는 것이 가능합니다.
as 라는 속성만 덧붙여 줍니다.
function App() {return (<div><FullButton bgColor="skyblue">확인</FullButton><Button bgColor="salmon">취소</Button><Button as="a">더보기</Button></div>);}4-2-3. 애니메이션 사용하기
스타일드 컴포넌트 내 애니메이션을 적용하려면 keyframes를 가져와야 합니다.
import styled, { keyframes } from "styled-components";
그런 다음
keyframes``;
으로 애니메이션을 작성하고,${}
으로 가져와서 사용합니다.const animation = keyframes`50% {transform: scale(1.3);}`;const FullButton = styled(Button)`width: 100%;border-radius: 4px;animation: ${animation} 1s infinite;`;
4-3. 템플릿 리터럴(Template literals)
근데 궁금한 점 하나! 🙋♀️
도대체 이
``
(backtick) 이 뭐길래, 요 안에 스타일을 넣으면 뿅하고 적용되는 걸까요?이번에는 이 ‘템플릿 리터럴’에 대해 알아봅니다.
템플릿 리터럴은 ‘내장된 표현식을 허용하는 문자열 리터럴’ 입니다..... 예? 「(゚ペ)
“리터털 = 소스 코드의 값을 표기한 것”이므로, 리터럴은 그냥 변치 않을 데이터 자체를 가리킨다고 생각하면 됩니다.
let a = 1; // 1은 정수 리터럴let b = 'nana'; // 'nana'는 문자열 리터럴아하 그러니까 얘도 문자열 리터럴인데 ``을 써서,
`Hello, ${name}`
처럼 내장되어 있는 이런저런 표현을 써먹을 수 있다는 거군요!이런 템플릿 리터럴은 런타임 시점에 일반 자바스크립트 문자열로 바뀌게 됩니다.
그래서 이 템플릿 리터럴이 지원하는 표현식에는 뭐가 있냐면요... (참고: MDN)
1) Multi-line 지원
console.log(`아아아아 나는 첫 번째 줄그리고 나는 두 번째 줄이지`);2) 표현식 삽입(Expression interpolation) 가능
let a = 0;let b = 100;console.log(`a하고 b를 더하면 ${a+b}(이)가 된다.`);3) 중첩 가능
const tired = true;const taste = '아메리카노';const chocie = `나는 출근길에${ tired ? `${ taste || '카푸치노'}` : '우유'} 를 샀다.`;console.log(chocie);4) Tagged templates
이게 아까 스타일드 컴포넌트에서 본 형태입니다. ``으로 감싼 내용을 함수에 넣어 주는 거죠.
const tag = (str) => alert(str);tag`Hello`;// styled.button``; 과 비슷하죠!이렇게
함수``
형태로 쓸 경우 첫 번째 인수는 표현식을 기준으로 분할된 문자열의 배열이, 나머지 인수는 표현식으로 전달된 값이 됩니다.let name = '붕어빵';let value = 5000;const tag = (a, ...b) => {console.log(a);console.log(b);}tag`${name}을 ${value}원어치 샀다.`;오호 그럼 ${}로 넘어온 녀석들에게, reduce 구문을 사용해 볼 수 있겠네요!
const props = {name: '붕어빵',value: 5000}const tag = (a, ...b) => {return a.reduce((result, text, i) => `${result}${text}${b[i] ? b[i](props) : ''}`, '');}tag`구매한 것: ${props => props.name};가격: ${props => props.value};`;/*구매한 것: 붕어빵;가격: 5000;*/그러면 스타일드 컴포넌트는 이런 식으로 넘어온 값을 설정했던 거구나, 하고 짐작해 볼 수 있겠네요.
styled.button`color: ${props => props.color};background-color: ${props => props.bgColor};`;
4-5. Theme 적용하기
{ ThemeProvider } 를 활용해 스타일에 ‘테마’를 적용해 봅시다.
무슨 테마를 쓸 것인가는 테마를 적용할 컴포넌트의 상위(index.js)에서 지정해 주면 됩니다.
/* index.js */import React from 'react';import ReactDOM from 'react-dom';import { ThemeProvider } from 'styled-components';import App from './App';const darkTheme = {textColor: 'whitesmoke',backgroundColor: '#111'};const lightTheme = {textColor: '#111',backgroundColor: 'whitesmoke'};ReactDOM.render(<React.StrictMode><ThemeProvider Theme={darkTheme}><App /></ThemeProvider></React.StrictMode>,document.getElementById('root'));App이 ThemeProvider 아래에 있기 때문에 darkTheme 또는 whiteTheme의 속성에 접근할 수 있습니다.
이미 Theme={darkTheme}를 적용해놨기 때문에, backgroundColor 는 #111 값이 됩니다.
const Title = styled.div`color: ${(props) => props.theme.textColor};`;const Notice = styled.div`background-color: ${(props) => props.theme.backgroundColor};`;만약 새롭게 pastelTheme를 추가하고 싶다면, 동일하게 객체를 만들어주고 Theme={} 부분을 바꿔주면 되겠죠!
const pastelTheme = {textColor: 'lightpink',backgroundColor: 'beige'};ReactDOM.render(<React.StrictMode><ThemeProvider theme={pastelTheme}><App /></ThemeProvider></React.StrictMode>,document.getElementById('root'));
4-6. 응용하기
4-6-1. 공통 CSS 적용하기
CSS 초기화 또는 통일화를 위해 reset.css나 normalize.css를 쓰곤 하는데 스타일드 컴포넌트에서도 관련 패키지를 제공하고 있습니다.
- reset.css ⇒ styled-reset
- normalize.css ⇒ styled-normalize
한편 전역으로 적용되는 CSS(common.css 등)가 필요하다면 스타일드 컴포넌트에서 제공하는 { createGlobalStyle } 을 사용합니다.
reset이나 normalize를 불러오고 ${} 를 이용해 쓱 넣어줄 수도 있겠죠.
/* GlobalStyle.js */import { createGlobalStyle } from 'styled-components';import Normalize from 'styled-normalize';const GlobalStyle = createGlobalStyle`${Normalize};* {margin: 0;padding: 0;}body {background-color: #f0f0f0;}`;export default GlobalStyle;전역으로 적용될 스타일을 작성하고 export 한 다음, 상위 컴포넌트에 위치시킵니다.
/* index.js */import React from 'react';import ReactDOM from 'react-dom';import { ThemeProvider } from 'styled-components';import GlobalStyle from './GlobalStyle';import App from './App';const darkTheme = {textColor: 'whitesmoke',backgroundColor: '#111'};const lightTheme = {textColor: '#111',backgroundColor: 'whitesmoke'};const pastelTheme = {textColor: 'lightpink',backgroundColor: 'beige'};ReactDOM.render(<React.StrictMode><GlobalStyle /><ThemeProvider theme={pastelTheme}><App /></ThemeProvider></React.StrictMode>,document.getElementById('root'));4-6-2. 유틸 함수 사용
이번엔 polished 를 설치해 CSS를 좀 더 다채롭게 써보죠!
clearFix(), hideText(), darken(), lighten() 같이 편리하게 써먹을 수 있는 함수들을 제공해요.
또, polished 내에도 normalize.css 를 제공하고 있어 이쪽에서 가져와서 써도 됩니다.
/* GlobalStyle.js */import { createGlobalStyle } from 'styled-components';import { normalize } from 'polished';const GlobalStyle = createGlobalStyle`${normalize()};* {margin: 0;padding: 0;}body {background-color: #f0f0f0;}`;export default GlobalStyle;관리하기가 복잡하므로 components/ 폴더 아래 컴포넌트를 두고, 이걸 import 해서 써보죠!
여러 은행사 중 하나를 선택할 수 있는 목록을 보여준다고 해봅니다.
/* components/Bank.js */import React from 'react';import styled, { css } from 'styled-components';const BankButton = styled.button``;function Bank() {return <BankButton></BankButton>;}export default Bank;/* App.js */import Bank from './components/Bank';function App() {return (<div><Bank name="wr" event>우리은행<br/><span>할인 이벤트(~12/31)</span></Bank><Bank name="kb">국민은행</Bank><Bank name="hn">하나은행</Bank></div>);}export default App;이때, <Bank></Bank> 사이에 들어있는 값(우리은행, 국민은행...)을 가져오기 위해 자식 컴포넌트에서
props.children
을 조회합니다. 이건 JSX 태그 사이에 있는 것들을 리턴해 줘요!/* components/Bank.js */import React from 'react';import styled, { css } from 'styled-components';const BankButton = styled.button``;function Bank({children}) {return <BankButton>{children}</BankButton>;}export default Bank;부모(App)에서 내려준 props도 가져올 건데, 추후에 disabled 등의 props가 추가될 수도 있으므로
...rest
형태로 가져옵니다./* components/Bank.js */import React from 'react';import styled, { css } from 'styled-components';const BankButton = styled.button``;function Bank({children, ...rest}) {return <BankButton {...rest}>{children}</BankButton>;}export default Bank;이제 스타일링을 해줍시다. bankColors 내의 컬러값을 props.name으로 선택해 가져오고, hover나 disabled 시의 모습도 설정합니다. 이때 darken() 등의 함수를 써볼 수 있겠네요.
import React from 'react';import styled, { css } from 'styled-components';import { darken } from 'polished';const bankColors = {wr: '#2172b2',kb: '#fdb810',hn: '#0d905d'};const BankButton = styled.button`display: flex;flex-direction: column;align-items: center;justify-content: center;height: 8rem;padding: 1rem;color: #fff;font-size: 1.6rem;font-weight: 700;border: 0;border-radius: 0.4rem;${(props) => {const color = bankColors[props.name];return css`background-color: ${color};&:hover {cursor: pointer;background-color: ${darken(0.05, color)};}&:disabled {color: #999;background-color: #c0c0c0;cursor: not-allowed;}`;}}span {display: inline-block;margin: 0.4rem 0 0;font-size: 80%;font-weight: 300;}`;function Bank({ children, ...rest }) {return <BankButton {...rest}>{children}</BankButton>;}export default Bank;한편, bankColors와 일치하지 않는 props.name이 들어올 경우 에러가 발생하므로 디폴트 설정도 해줍시다.
import React from 'react';import styled, { css } from 'styled-components';import { darken } from 'polished';const bankColors = {wr: '#2172b2',kb: '#fdb810',hn: '#0d905d',default: '#222222'};const BankButton = styled.button`//...${(props) => {const color = bankColors[props.name] || bankColors['default'];return css`//....`;}}//...`;function Bank({ children, ...rest }) {return <BankButton {...rest}>{children}</BankButton>;}export default Bank;Grid CSS를 활용해 보기 좋게 App.js를 다듬고, 유지보수성 테스트를 위해 은행 몇 가지를 추가해봤습니다.
/* App.js */import Bank from './components/Bank';import styled from 'styled-components';const AppBlock = styled.div`display: grid;grid-template-columns: 1fr 1fr 1fr;width: 100%;min-width: 32rem;max-width: 48rem;margin: 4rem auto;column-gap: 1.4rem;row-gap: 1.4rem;`;function App() {return (<AppBlock><Bank name="wr" event>우리은행<br /><span>할인 이벤트(~12/31)</span></Bank><Bank name="kb">국민은행</Bank><Bank name="hn">하나은행</Bank><Bank name="sh" disabled>신한은행</Bank><Bank name="ibk">기업은행</Bank><Bank name="nn">나나은행</Bank></AppBlock>);}export default App;이번에는 event 라는 props가 있을 때 특별한 스타일링을 해보죠!
얘는 줄 하나를 전체 차지하면서 그림자가 생기는 애니메이션을 주려고 합니다.우선 애니메이션을 위해 keyframes 를 쓱 가져옵니다.
import styled, { css, keyframes } from 'styled-components';
애니메이션 내에서도 bankColors 내 값 중 하나를 받아와 box-shadow로 쓸 예정이기 때문에, 인자를 통해 받아올 수 있도록 함수를 사용합니다.
const animation = (color) => keyframes`0% {box-shadow: 0 0 0 ${color}}`;그리고 props.event일 때 적용될 eventStyle을 만들어 줍니다.
const eventStyle = () => {if (props.event) {return css`grid-column: 1 / 4;order: -1;animation: ${onEventAnimation(color)} 3s infinite;`;}};이때 단순 템플릿 리터럴로 리턴
return ``;
하는 경우 오류가 뙇 발생하므로css``;
형태로 리턴해 줍니다.앗 넵 이제
${ (props) => { return css `` } }
내에${eventStyle}
을 넣어주면 완료!/* Bank.js */import React from 'react';import styled, { css, keyframes } from 'styled-components';import { darken, lighten } from 'polished';const bankColors = {wr: '#2172b2',kb: '#fdb810',hn: '#0d905d',ibk: '#0892ce',default: '#222222'};const onEventAnimation = (color) => keyframes`30%, 70% {box-shadow: 0rem 0.2rem 1.2rem -0.1rem ${lighten(0.05, color)};}`;const BankButton = styled.button`display: flex;flex-direction: column;align-items: center;justify-content: center;height: 8rem;padding: 1rem;color: #fff;font-size: 1.6rem;font-weight: 700;border: 0;border-radius: 0.4rem;${(props) => {const color = bankColors[props.name] || bankColors['default'];const eventStyle = () => {if (props.event) {return css`grid-column: 1 / 4;order: -1;animation: ${onEventAnimation(color)} 3s infinite;`;}};return css`background-color: ${color};&:hover {cursor: pointer;background-color: ${darken(0.05, color)};}&:disabled {color: #999;background-color: #c0c0c0;cursor: not-allowed;}${eventStyle};`;}}span {display: inline-block;margin: 0.4rem 0 0;font-size: 80%;font-weight: normal;}`;function Bank({ children, ...rest }) {return <BankButton {...rest}>{children}</BankButton>;}export default Bank;/* App.js */import Bank from './components/Bank';import styled from 'styled-components';const AppBlock = styled.div`display: grid;grid-template-columns: 1fr 1fr 1fr;width: 100%;min-width: 32rem;max-width: 48rem;margin: 4rem auto;column-gap: 1.4rem;row-gap: 1.4rem;`;function App() {return (<AppBlock><Bank name="wr">우리은행</Bank><Bank name="kb">국민은행</Bank><Bank name="hn" event>하나은행<br /><span>추첨 이벤트 (~1/28)</span></Bank><Bank name="sh" disabled>신한은행</Bank><Bank name="ibk">기업은행</Bank><Bank name="nh">농협은행</Bank><Bank name="nn" event>나나은행<br /><span>할인 이벤트 (~12/31)</span></Bank></AppBlock>);}export default App;4-6-3. 레이어 팝업 만들기
이번에는 레이어 팝업을 구현해 봅니다!
LayerPopup 컴포넌트를 일단 슥 만들어줍니다.
import React from 'react';import styled from 'styled-components';function LayerPopup() {return (<Popup><PopupContents><strong>타이틀</strong><p>내용</p><div className="buttonGroup"><button>확인</button><button>취소</button></div><PopupCloseBtn>팝업 닫기</PopupCloseBtn></PopupContents><Dim/></Popup>);}export default LayerPopup;타이틀, 내용, 버튼은 유동적인 부분이므로 props가 될 수 있도록 바꿔줍니다.
여기에 defaultProps도 넣어주면 좋겠네요!
import React from 'react';import styled from 'styled-components';function LayerPopup({ title, children, cancelButton, confirmButton }) {return (<Popup><PopupContents><strong>{title}</strong><p>{children}</p><div className="buttonGroup"><button>{cancelButton}</button><button>{confirmButton}</button></div><PopupCloseBtn>팝업 닫기</PopupCloseBtn></PopupContents><Dim/></Popup>);}LayerPopup.defaultProps = {title: '안녕하세요',children: 'Lorem Ipsum Dolor Sit Amet?',cancelButton: '취소',confirmButton: '확인'};export default LayerPopup;이제 예쁘게 꾸며줄 시간입니다 🧑🎨
/* LayerPopup.js */import React from 'react';import styled from 'styled-components';import Button from './Button';import { hideVisually } from 'polished';import Close from './../assets/close';const Popup = styled.div`position: fixed;top: 0;left: 0;width: 100%;height: 100%;display: flex;align-items: center;justify-content: center;`;const Dim = styled.div`position: absolute;top: 0;left: 0;width: 100%;height: 100%;background-color: rgba(0, 0, 0, 0.7);`;const PopupContents = styled.div`position: relative;width: 30rem;background-color: #fff;padding: 3.6rem;border-radius: 0.3rem;strong {display: block;font-size: 3rem;margin-bottom: 1.6rem;}p {line-height: 160%;font-size: 1.6rem;}.buttonGroup {display: flex;justify-content: space-between;margin-top: 3rem;}`;const PopupCloseBtn = styled.button`position: absolute;top: 2.4rem;right: 2.4rem;width: 1.8rem;height: 1.8rem;border: 0;span {${hideVisually()};}path {stroke: #666;}&:hover path {stroke: #000;}`;function LayerPopup({ title, children, cancelButton, confirmButton }) {return (<Popup><PopupContents><strong>{title}</strong><p>{children}</p><div className="buttonGroup"><Button type="line">{cancelButton}</Button><Button type="fill">{confirmButton}</Button></div><PopupCloseBtn><span>팝업 닫기</span><Close /></PopupCloseBtn></PopupContents><Dim/></Popup>);}LayerPopup.defaultProps = {title: '안녕하세요',children: 'Lorem Ipsum Dolor Sit Amet?',cancelButton: '취소',confirmButton: '확인'};export default LayerPopup;다음은 취소(닫기)와 확인 버튼을 눌렀을 때 이벤트를 만들어 줄 차례입니다.
<Button type="line" onClick={onCancel}/>
처럼 각 버튼에게 이벤트를 달아줍니다.그리고 팝업의 표시 유무를 visible이란 props 로 관리합니다.
visible 이란 props가 있다면 보여지고, 없으면 보이지 않는 상태입니다.
/* LayerPopup.js */function LayerPopup({ visible, onConfirm, onCancel, title, children, cancelButton, confirmButton }) {if (!visible) return null;return (<Popup>{/* ... */}</Popup>);}그럼 이걸 부모 컴포넌트에서 컨트롤 해야겠죠. App.js로 올라와 state로 설정합니다.
/* App.js */import { useState } from 'react';//...function App() {const [popup, setPopup] = useState(false);const onClick = () => {setPopup(true);};const onConfirm = () => {console.log('확인');setPopup(false);};const onCancel = () => {console.log('취소');setPopup(false);};return (<><AppBlock>{/* ... */}</AppBlock><LayerPopup /></>);}export default App;또, 기존에 있던 bank 들을 map()으로 뿌려지게끔 변경했습니다.
/* App.js *///...function App() {const bankList = [{ eng: 'wr', ko: '우리' },{ eng: 'kb', ko: '국민' },{ eng: 'hn', ko: '하나', event: { name: '추첨', date: '1/31' } },{ eng: 'sh', ko: '신한', disabled: true },{ eng: 'ibk', ko: '기업', event: { name: '한정', date: '1/31' } },{ eng: 'nh', ko: '농협' },{ eng: 'na', ko: '나나' }];const [popup, setPopup] = useState(false);const onClick = () => {console.log('onclick');setPopup(true);};const onConfirm = () => {console.log('확인');setPopup(false);};const onCancel = () => {console.log('취소');setPopup(false);};return (<><AppBlock>{bankList.map((bank) => {return (<Bankid={bank.eng}key={bank.eng}name={bank.eng}disabled={bank.disabled}event={bank.event}onClick={onClick}><span>{bank.ko}은행</span>{bank.event && (<i>{bank.event.name} 이벤트 (~{bank.event.date})</i>)}</Bank>);})}</AppBlock><LayerPopup visible={popup} onCancel={onCancel} onConfirm={onConfirm} /></>);}export default App;한편, 심적 안정을 위한(?) 팝업 트랜지션 효과도 넣어줍니다.
팝업이 나타날 때 딤에게 fadeIn 애니메이션을, 팝업창에게 slideInUp 애니메이션을 주려고 합니다.
반대로 사라질 때는 딤에게 fadeOut 애니메이션, 팝업창에게 slideOutDown 애니메이션을 줍니다.
다만 문제가 하나 있는데요,
현재
visible
로 팝업을 컨트롤하고 있어서 이 state에 맞춰 돔이 생기거나 파괴됩니다.따라서
!visible
이 되는 즉시 돔이 사라지므로, 페이드아웃 애니메이션이 적용되기 전에 화면에서 없어져 버립니다.해결 방법으로는 돔을 그냥 그려놓은 채 CSS의 visible 속성과 opacity 속성 값을 변경해 트랜지션 효과를 주는 방법도 있습니다. (참고)
아니면
visible
과 별개로 ‘화면상에 그려지고 있는가’만을 체크하는 state를 따로 두는 방법도 있어요. (참고)예를 들어,
userVisible
이란 state를 만들어 두고,!visible(팝업닫힌 상태)
면서userVisible(화면상에 아직 존재)
이면 페이드아웃 효과를 주는 거죠.이렇게 페이드아웃 중이면
animate
란 state를 TRUE로 두고, 애니메이션이 끝나면!animate
와!userVisible
로 만듭니다.그러면 돔이 사라지는 건
visible(유저가 클릭해서 팝업닫힌 상태)
이 아니라!userVisible(페이드아웃 애니메이션이 끝난 상태)
일 때가 됩니다. 또, 애니메이션 중일 때 팝업이 사라져버리면 안 되니까!animate
일 때만 사라져야겠죠.까 visible과 반대인
!visible
로 주면 됩니다.const Popup = styled.div`//...animation: ${fadeIn} 0.3s; //! 보여질 때 페이드인 애니메이션animation-fill-mode: forwards;${(props) =>props.disappear &&css`animation-name: ${fadeOut}; //! dissappear props를 받으면 페이드아웃 애니메이션`}`;const PopupContents = styled.div`//...animation: ${slideInUp} 0.3s 50ms; //! 보여질 때 슬라이드인 애니메이션animation-fill-mode: both;${(props) =>props.disappear &&css`animation: ${slideOutDown} 0.3s; //! dissappear props를 받으면 슬라이드아웃 애니메이션`}//...`;function LayerPopup({visible,//...}) {const [animate, setAnimate] = useState(false); //! 애니메이션 중인지 판단const [userVisible, setUserVisible] = useState(visible); //! 사용자에게 보여지는 상태인지 판단useEffect(() => {if (userVisible && !visible) { //! 사용자에게는 아직 보여지고 있지만 팝업은 닫은 상태일 때setAnimate(true); //! 애니메이션 활성화}setUserVisible(visible); //! userVisible을 visible과 동일하게}, [userVisible, visible]);if (!animate && !userVisible) return null; //! 애니메이션 상태도 아니고, 사용자에게 보이는 상태도 아닐 때만 그리지 않음return (<Popup disappear={!visible} onAnimationEnd={() => setAnimate(false)}> {/*! disappear값 넘기고, 애니메이션 종료 시 !animate로 설정 */}<PopupContents disappear={!visible}>{/*...*/}</PopupContents><Dim onClick={onCancel} /></Popup>);}728x90나나 (nykim)쉽고, 재밌고, 특별한 걸 좋아해요. 걷고 뛰고 구르면서 나아가는 중.
'Blog > Library' 카테고리의 다른 글
Framer-motion 라이브러리 훑어보기 (9) 2022.09.08 [ReactJS] 5. 𝒘𝒊𝒕𝒉 타입스크립트 (225) 2022.02.13 [ReactJS] 2. 기능 연습 & 3. 앱 만들기 (0) 2022.02.03 [ReactJS] 1. 시작하기 (7) 2022.02.01 [Vue/Vuex] 뷰 실습 - Todo 웹앱 만들기 (4) (2) 2020.06.03