Modal 팝업 작업을 진행했습니다. 

일단 공식문서에서 useRef, useImperativeHandle, forwardRef를 확인해보세요!

(저는 useRef로만 작업 했습니다.)

ko.reactjs.org/docs/hooks-reference.html#useref

ko.reactjs.org/docs/hooks-reference.html#useimperativehandle

ko.reactjs.org/docs/react-api.html#reactforwardref

 

useImperativeHandle과 forwardRef... 사용해서 해보려고도 하다가.. 

이것저것 하느라 시간이 걸렸습니다.

컴포넌트 형식 작업도 약간 헷갈리기도 하고^^;

 

그러다가 블로거님의 코드를 보고 짜잔 해결했습니다.

아래는 참고한 원본 코드의 주소입니다... 감사합니당(--)(__)(--)!

https://4log.hyeon.pro/post/click-event-outside-the-component

 

해당컴포넌트 outside 영역 클릭 이벤트

구현하고싶은 것은 Modal 을 구현하고싶다. 모달팝업이 띄워졌을때 바깥 영역을 클릭 하였을 때 이벤트를 발생시켜서 모달팝업의 상태를 close 로 변경하고싶다. 바깥 영역을 클릭 이벤트를 구현

4log.hyeon.pro

 

참고하여 코드 수정하여 적용한 모습입니다.

 

부모 컴포넌트입니다.

./Menu

import React, { useRef, useEffect, useCallback, useState } from "react";
import styled from 'styled-components';
import MenuPopup from './MenuPopup';

import { MenuOutlined } from '@ant-design/icons';

const MenuWrap = styled.div`
    position: relative;
`;

const MenuButton = styled.button`
    padding: 0;
    background: none;
    border: none;
    cursor: pointer;
    outline: none;

    &:hover,
    &:focus {
        background: none;
    }

    &:hover,
    &:focus,
    &.active{
        opacity: 0.5;
    }
`;

const MenuIcon = styled(MenuOutlined)`
    font-size: 17px;
    color: ${props => props.themecolor};
`;

const Menu = ({ themecolor }) => {
    const popRef = useRef(null);
    const [isOpen, setIsOpen] = useState(false);

    const onClickOutside = useCallback(({ target }) => {
        if (popRef.current && !popRef.current.contains(target)) {
            setIsOpen(false);
        }
    }, []);

    const onClickMenu = useCallback(() => {
        setIsOpen(!isOpen);
    }, [isOpen]);

    useEffect(() => {
        document.addEventListener("click", onClickOutside);

        return () => {
            document.removeEventListener("click", onClickOutside);
        };
    }, []);

    return(
        <MenuWrap ref={popRef}>
            <MenuButton onClick={onClickMenu}>
                <MenuIcon themecolor={themecolor} />
            </MenuButton>

            <MenuPopup isOpen={isOpen} />
        </MenuWrap>
    );
};

export default Menu;

 

자식 컴포넌트입니다.

./MenuPopupWrap 

import React from 'react';

import styled from 'styled-components';

const MenuPopupWrap = styled.div`
    position: absolute;
    top: 30px;
    right: 0;
    padding-top: 15px;
    display: none;
    width: 80px;
    height: 125px;
    background: rgba(0, 0, 0, 0.4); 
    clip-path: polygon(90% 10%,100% 10%,100% 100%,0 100%,0 10%,74% 10%,90% 0);

    &.active {
        display: block;
    }
`;

const MenuPopup = ({ isOpen }) => {
    return (
        <MenuPopupWrap className={isOpen ? 'active' : ''}>
            <ul>
                <li>1</li>
                <li>1</li>
                <li>1</li>
                <li>1</li>
                <li>1</li>
            </ul>
        </MenuPopupWrap>
    );
};

export default MenuPopup;

(완성된 코드들은 아닙니다)

 

여기서 참고해야할 것을 간추리자면

부모컴포넌트에서..

const Menu = ({ themecolor }) => {
	// 1. ref로 클릭한 타겟이 팝업인지 아닌지 체크할 것입니다.
    const popRef = useRef(null);
    const [isOpen, setIsOpen] = useState(false);
	
    // 2. document에 바인딩할 클릭 이벤트입니다. (4번)
    const onClickOutside = useCallback(({ target }) => {
        if (popRef.current && !popRef.current.contains(target)) {
            setIsOpen(false);
        }
    }, []);
	
    // 3. 팝업 여닫는 이벤트입니다.
    const onClickMenu = useCallback(() => {
        setIsOpen(!isOpen);
    }, [isOpen]); // 4. state를 넣어줘야 업데이트 됩니다.
	
    // 4. 컴포넌트 마운팅될때 document 이벤트를(2번) 바인딩해줍니다.
    useEffect(() => {
        document.addEventListener("click", onClickOutside);
		
        // 5. return을 통해 cleanup해줘야합니다.
        // 안해주면 언마운팅이나 업데이트 시 문제가 생깁니다.
        return () => {
            document.removeEventListener("click", onClickOutside);
        };
    }, []);

    return(
    	// 6. 오픈한 버튼이나 팝업 영역 모두 클릭 시 닫히면 안되기때문에... 
        // 전체 영역에 ref를 추가합니다.
        <MenuWrap ref={popRef}>
        	
            // 7. 팝업 여닫는버튼입니다. (3번)
            <MenuButton onClick={onClickMenu}>
                <MenuIcon themecolor={themecolor} />
            </MenuButton>
	
    		// 8. 팝업컨텐츠가 포함된 컴포넌트입니다.
            <MenuPopup isOpen={isOpen} />
        </MenuWrap>
    );
};

export default Menu;

 

자식컴포넌트에서..

import React from 'react';
import styled from 'styled-components';

const MenuPopupWrap = styled.div`
    display: none;
	
    // 2. active일 경우 block처리하는 형식으로 했습니다.
    // 이 부분은 부모 컴포넌트에서 처리해도됩니다.
    &.active {
        display: block;
    }
`;

// 1. props를 받아와서 className을 추가합니다.
// 이 부분은 props를 통해 처리하지 않고 부모 컴포넌트에서 처리해도됩니다.
const MenuPopup = ({ isOpen }) => {
    return (
        <MenuPopupWrap
            className={isOpen ? 'active' : ''}
        >
            <ul>
                <li>1</li>
                <li>1</li>
                <li>1</li>
                <li>1</li>
                <li>1</li>
            </ul>
        </MenuPopupWrap>
    );
};

export default MenuPopup;

 

 

return 을 통해 뒷정리하는 부분 참고글!

velog.io/@velopert/react-hooks#23-%EB%92%B7%EC%A0%95%EB%A6%AC-%ED%95%98%EA%B8%B0

 

 

 

라이브러리를 사용할때, 보통 class나 id는 지정되있기 마련이죠.

또한 이런 것을 수정해야하는 경우도 종종 생깁니다.

 

이때 css를 수정하는 방법을 알아보도록 하겠습니다.!

(next, styled-components 사용 방법입니다.)

 

첫번째 방법은 css 파일을 import하는 방법입니다.

즉 전체 페이지에 대한 스타일시트를 가져오는 방법입니다.

먼저 pages/ 폴더 하위에 _app.js 파일을 생성한 후 스타일시트를 import해주면 됩니다.

import '../styles.css';

export default function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />
}

https://nextjs.org/docs/basic-features/built-in-css-support

 

Basic Features: Built-in CSS Support | Next.js

Next.js supports including CSS files as Global CSS or CSS Modules, using `styled-jsx` for CSS-in-JS, or any other CSS-in-JS solution! Learn more here.

nextjs.org

 

두번째 방법은 글로벌 스타일을 만드는 것인데요.

첫번째 방법과는 달리 styled-components라이브러리를 사용하고 있어야합니다.

https://styled-components.com/docs/api#createglobalstyle

 

styled-components: API Reference

API Reference of styled-components

styled-components.com

createGlobalStyle를 import 한 뒤에 

import styled, { createGlobalStyle } from 'styled-components';

styled.div`` 가 아닌, createGlobalStyle로 작업을 하면 됩니다.

(.slick-slide는 라이브러리가 지정한 class이며 스타일 덮여씌우는 예시입니다.)

const Global = createGlobalStyle`
    .slick-slide {
        display: inline-block;
    }
`;

그리고 아무대나 컴포넌트 넣듯 <Global />로 넣으면 됩니다.

이때 간단히 설명하자면 styled.div`` 형식으로 만든 것은 로컬 스코프를 가지게되고, 

createGlobalStyle``로 만든것은 글로벌 스코프를 가지게 된다고 합니다.

 

저는 작업 특성 상 pages/_app.js 파일을 만들었는데,

글로벌 스타일을 이곳에 넣어줬습니다. 

pages/_app.js

import React from 'react';
import PropTypes from 'prop-types';
import Head from 'next/head';
import { createGlobalStyle } from 'styled-components';
import 'antd/dist/antd.css';
import wrapper from '../store/configurestore';

const App = ({ Component }) => {
    const Global = createGlobalStyle`
        .hidden {
            padding: 0; 
            margin: -1px;
            position: absolute; 
            width: 1px; 
            height: 1px; 
            clip: rect(0 0 0 0); 
            overflow: hidden; 
            border: 0; 
        }
    `;

    return(
        <>
            <Head>
                <meta charSet="utf-8" />
                <title>App</title>
            </Head>
            <Global />
            <Component />
        </>
    );
};


App.propTypes = {
    Component: PropTypes.elementType.isRequired,
};

export function reportWebVitals(metric){
    console.log(metric);
}

export default wrapper.withRedux(App);

 

세번째 방법중복되는 기본 스타일을 어떻게 사용할까? 입니다. 

styled-components 라이브러리에서 css api를 제공합니다.

https://styled-components.com/docs/api#css

 

styled-components: API Reference

API Reference of styled-components

styled-components.com

이렇게 사용하면 됩니다.

import styled, { css } from 'styled-components';

const defaultStyle = css`
    display: block;
    // ...
`;

const CircleButton = styled.button`
	${defaultStyled}
    // ....
`;

const RectangleButton = styled.button`
	${defaultStyled}
    // ....
`;

 

이때 css를 넣지않고 백틱만으로도 작업이 되긴 합니다만 권고하지 않습니다.

문자열로 인식되어 원하지 않는 결과가 생길 수도 있다고 합니다.

 

 

 

react에서 리다이렉션 시키려면 모듈을 설치하여 작업해주어야합니다.

하지만 next에서는 따로 설치 없이 간단히 사용할 수 있습니다.

 

로그인하지 않았다면 로그인 화면으로 보내는 작업을 하겠습니다.

next/router 를 통해 작업하기 위해 import를 해줍니다.

import Router from 'next/router';

 

컴포넌트가 생성될 때 실행되어야하기 때문에 useEffect를 통해 작업합니다.

import React, { useEffect } from 'react';

코드는 아래와 같습니다.

useEffect(() => {
  if(!nickname){
  	Router.replace('/login');
  }
}, [nickname]);

if문을 통해 해당 state 값이 없으면 Router.replace를 통해 login 페이지로 리다이렉션시켜줍니다.

여기서 Router.push를 이용하지 않고 replace를 이용한 것은 history를 남기지 않아 뒤로가기 버튼을 동작하지 않게 만들기 위함입니다.

 

저는 state 값으로 nickname을 사용했는데, 로그인에 관련 된 state값이면 되겠죠? 뭐 id나 useInfo등등

마지막 인자로 [ nickname ] 을 적어주면됩니다.

nickname state가 수정되면 useEffect가 다시 실행됩니다.

 

 

이렇게하면 index에 접근했을때 /login 페이지로 리다이렉트 됩니다.!!

짱간단^^

 

 

 

이번 작업은 배터리 만들기입니다.

핸드폰들 보면 몇 % 남았는지 보이는 것있쬬?

기능은 약간 다르지만 흉내내보겠습니다.

 

제가 원하는 기능은 하루를 기준으로 시, 분을 통해 얼마나 지났는 지를 체크하는 것입니다.

즉 24시가 되면 100%, 오후 12시면 50% 겠죠? 로직을 수정하면 거꾸로도 할 수 있겠죠?

(지금 중요한건 이런 기능에 대한 설명은 아니니깐요.)

 

제로초님 강의에서는 moment를 사용해서 날짜를 구해 사용했는데요,

잠깐 다른 라이브러리에 대해서도 설명해주십니다.

그 중 구글 트렌드를 보시고 요즘 많이 사용한다는 dayjs에 대해 간단한 소개를 해주시는데요.

용량이 가볍기 때문에 많이들 쓴다고 합니다.

저는 dayjs를 통해 만들어보겠습니다.

(사실 moment나 dayjs나 문서보면 비슷비슷한 듯 싶습니다.)

 

찾다보니 moment를 사용해 디지털 시계같은것을 구현한 라이브러리도 있더라구요.ㅎㅎ;

 

moment와 dayjs 대체에 관한 블로그 글

https://john015.netlify.app/moment-js%EB%A5%BC-day-js%EB%A1%9C-%EB%8C%80%EC%B2%B4%ED%95%98%EA%B8%B0

 

dayjs 라이브러리를 써봅쉬다.

https://day.js.org/en/

https://github.com/iamkun/dayjs

 

먼저 install 해주시구요.

npm i dayjs

 

코드는 아래와 같습니다.

뭔가 최적화 시킬 수 있을 것 같기도 한데.

react 공부 중이라 정확히는 아직 잘 모르겠네요.

프로젝트를 더 진행하면서 좋은 수가 떠올르길 바랍니다.ㅠ

import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import dayjs from 'dayjs';

function getCurrentPercent(time){
    const totalMin = 24 * 60;
    const currentMin = (time.format('HH') * 60) + Number(time.format('mm'));
    
    return Math.floor(100 / (totalMin / currentMin));
}

const BatteryWrapper = styled.div`
    &:before {
        margin-right: 3px;
        display: inline-block;
        content: '${props => Math.floor(props.percent)}%';
        color: ${props => props.themecolor};
    }

    .gauge {
        display: inline-block;
        width: 28px;
        height: 12px;
        border: 1px solid ${props => props.themecolor};
        border-radius:3px;
    }

    .gauge:before {
        display: block;
        content: '';
        width: ${props => props.percent}%;
        height: 100%;
        background-color: ${props => props.themecolor};
    }
`;

const Battery = ({ themecolor }) => {
    const [time, setTime] = useState(dayjs());
    const [percent, setPercent] = useState(null);
    let timerInterval = null;

    useEffect(() => {
        timerInterval = setInterval(() => {
            setTime(dayjs());
        }, 1000);

        return () => {
            clearInterval(timerInterval);
        };
    }, []);

    useEffect(() => {
        const currentPer = getCurrentPercent(time);
        
        if(percent === currentPer){
            return;
        }

        setPercent(Math.floor(currentPer));
    }, [time]);

    return(
        <BatteryWrapper themecolor={themecolor} percent={percent}>
            <span className="gauge"></span>
        </BatteryWrapper>
    );
};

Battery.propTypes = {
    themecolor: PropTypes.string,
};

Battery.defaultProps = {
    themecolor: '#333',
};

export default Battery;

 

나중에 할 일인데, 언어를 ko만 가져와서 사용할 수 있도록 설정할 수 있습니다.

(그때쯤 기억나면 추가하도록 할게요)

 

완성 이미지!

배터리 완성 이미지

 

+ Recent posts