기본 스티커 메모를 아시나요?

기본스티커 메모

헤더 부분을 드래그하면 이동되는 것과 메모 입력하는 기능을 흉내내보았는데요.

제 사이트에는 딱 저 2가지 기능만 필요하여 저 부분만 작업했습니다! 

 

Memo.js

import React, { useCallback, useRef, useState } from 'react';
import styled from 'styled-components';

const Wrap = styled.div`
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 200px;
    background: yellow;
    border: 1px solid #c0c0a4;
    box-sizing: border-box;
`;
const Header = styled.div`
    padding: 5px;
    width: 100%;
    font-size: 14px;
    font-weight: 700;
    color: yellow;
    background: #c0c0a4;
    text-align: center;
`;
const Content = styled.div`
    padding: 5%;
    max-height: 300px;
    overflow-y: auto;

    textarea {
        padding: 5%;
        width: 100%;
        min-height: 150px;
        box-sizing: border-box;
        background: none;
        outline: none;
        border: none;
    }
`;

const Memo = () => {
    const [memoText, setMemoText] = useState('');
    const wrapRef = useRef(null);
    const headerRef = useRef(null);
    let lastX = 0;
    let lastY = 0;
    let startX = 0;
    let startY = 0;

    const onFocusout = useCallback(({ target }) => {
        setMemoText(target.value);
    }, [memoText]);

    const onMove = useCallback((e) => {
        e.preventDefault(); 
        lastX = startX - e.clientX;
        lastY = startY - e.clientY;

        startX = e.clientX;
        startY = e.clientY;

        wrapRef.current.style.top = `${wrapRef.current.offsetTop - lastY}px`;
        wrapRef.current.style.left = `${wrapRef.current.offsetLeft - lastX}px`;
    }, []);

    const removeEvent = useCallback(() => {
        document.removeEventListener('mouseup', removeEvent);
        document.removeEventListener('mousemove', onMove);
    }, []);

    const onMouseDown = useCallback((e) => {
        e.preventDefault(); 
        startX = e.clientX;
        startY = e.clientY;

        document.addEventListener('mouseup', removeEvent);
        document.addEventListener('mousemove', onMove);
    }, []);

    return (
        <Wrap ref={wrapRef}>
            <Header 
                ref={headerRef}
                onMouseDown={onMouseDown}
            >
                Memo
            </Header>
            <Content>
                <textarea 
                    placeholder="메모를 입력해주세요..."
                    onBlur={onFocusout}    
                ></textarea>
            </Content>
        </Wrap>
    );
};

export default Memo;

포커스가 빠져나갈때 value값을 저장하도록 해두었습니다.

 

아! 코드는 js에서 사용하던 그대로 사용했으며.. ↓

okayoon.tistory.com/entry/%EB%A7%88%EC%9A%B0%EC%8A%A4%EB%A1%9C-%EC%B0%BD%EC%9D%84-%EC%9B%80%EC%A7%81%EC%97%AC%EB%B3%B4%EC%9E%90?category=835827

 

ref를 통해 style 값을 넣는 부분을 안하고 싶지만..

useState를 사용할 경우 안되더라구요. 비동기여서 그런듯한데....

동기로 잘 사용할 방법을 찾으면 해결 가능할 것 같은데....

velog.io/@aerirang647/32setState-%EB%B9%84%EB%8F%99%EA%B8%B0

 

[+32]setState -> 비동기

동기와 비동기동기: 코드 한 줄 한 줄 끝날 때까지 기다렸다가 다음줄로 넘어가는 성질.ex. forEach비동기: 코드 한 줄이 하고 있는 일이 끝나지 않았는데 일을 시켜놓고 내려가다가 일이 끝났다는

velog.io

여튼.. 그래서 그냥 ref를 통해 style을 수정해주고있습니다.

더 좋은 방법이 있을 것 같은데 아직 찾지를 못해서...ㅠ

쪼랩에게 좋은 의견이 있으면 말해주심 감사하겠습니다.

 

완성된 모양과 동작입니다.

메모 완성!

 

 

폴더창이나 인터넷 창 등.....

보통 상단부 부분을 마우스로 꾹!누르고 움직이면 창을 이동시킬수 있잖아요?

 

이렇게요 ↓

창 움직이는 gif

 

오늘은 이 기능을 만들어 보도록 하겠습니다.!

^^

 

 

먼저 움직일 객체를 만들기위한 작업을 합니다.

HTML

<div class="wrap">
    <div class="header">
        이곳을 드래그 하세요.
    </div>
</div>

 

 

대충 모양을 꾸며줄게요.

CSS

.wrap{
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 300px;
  height: 300px;
  border: 1px solid #ddd;
  border-radius: 5px;
  box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.5);
  overflow: hidden;
}

.header{
  padding: 5px;
  text-align: center;
  font-size: 14px;
  color: #333;
  background: #ddd;
  cursor: default;
}

.header.active{
	cursor: move;
}

 

 

그다음 기능을 추가해줍니다.

JS

const wrap = document.querySelector(".wrap");
const header = document.querySelector(".header");
let lastX = 0;
let lastY = 0; 
let startX = 0; 
let startY = 0; 

// 1.
header.addEventListener('mousedown', function(e){
  e.preventDefault(); 
  startX = e.clientX; 
  startY = e.clientY; 
	
  // 2.  
  header.classList.add('active');
  
  // 3.
  document.addEventListener('mouseup', onRemoveEvent); 
  
  // 4.
  document.addEventListener('mousemove', onMove); 
});

function onRemoveEvent() { 
  header.classList.remove('active');
  document.removeEventListener('mouseup', onRemoveEvent); 
  document.removeEventListener('mousemove', onMove); 
} 

function onMove(e) { 
  e.preventDefault(); 
  lastX = startX - e.clientX; 
  lastY = startY - e.clientY; 

  startX = e.clientX; 
  startY = e.clientY; 

  wrap.style.top = `${wrap.offsetTop - lastY}px`;
  wrap.style.left = `${wrap.offsetLeft - lastX}px`;
}

1, 2, 3, 4에 대해 설명하겠습니다.

 

1. 상단부에 마우스를 꾹 눌렀을때 작동하게 하기 위해서 header 영역을 타겟으로 잡았습니다.

또한, 타겟에 마우스를 꾹 누른상태에서 움직일때 이벤트 발생하기 위해 mousedown 이벤트를 먼저 사용합니다.

이때 최초의 client 좌표 값을 변수에 담습니다.

 

2. mousedown시에 클래스(active)를 추가하여 스타일로 마우스커서를 바꾸어줍니다. 

 

3. 마우스를 꾹 누른것을 풀었을 경우(mouseup)에 객체가 이동되면 안되므로 내부에서 이벤트를 해제하는 함수를 바인딩해줍니다.

 

4. 핵심 함수입니다.

mousedown + mousemove가 함께 작동할 때 동작해야합니다.

이벤트 타겟은 header이나 움직이는 객체는 wrap이기때문에 좌표값을 구해서 wrap 객체 속성을 수정해줘야합니다.

mousedown에서 저장했던 client 좌표값에서 현재 client 좌표값을 뺀 값을 wrap 객체 스타일 속성으로 추가합니다.

 

 

완성된 모습입니다.

완성 예제

 

 

 

 

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

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

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

 

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

즉 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만 가져와서 사용할 수 있도록 설정할 수 있습니다.

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

 

완성 이미지!

배터리 완성 이미지

 

 

React 강의 듣고 나만의 사이트 만들기 시작!! 

먼저 초반부 Front 작업을 진행한다.

쪼랩이라 라이브러리 다운받을때 빼먹는건 진행하면서 차차 고치도록 해야겠다.

친절하게 코드가 다 들어있지않은 이유는 스스로 공부하는 목적이기때문에 사소한 부분이 많이 빠져있다.

 

1.

내 사이트명의 폴더를 생성한다.

OKAYOON

 

2. 

Front 와 Back 따로 진행해야하기에 폴더를 두개 생성한다.

OKAYOON

-front

-back

// front

npm init
npm i next@9 react react-dom 
npm i prop-types
npm i eslint eslint-plugin-import eslint-plugin-react eslint-plugin-react-hooks -d

npm i -D babel-eslint eslint-config-airbnb eslint-plugin-import eslint-plugin-react-hooks eslint-plugin-jsx-a11y

npm i antd styled-components @ant-design/icons

npm init 해준 뒤 install해주자

내가 본 제로초님 강의에서는 next9버전을 사용했다.

next@9의 @는 특정 버전을 지정하여 install 할 수 있다.

eslint를 사용하지 않는다면 install 할 필요없다.

-D는 개발모드에서만 사용할 모듈들이다.

antd를 통해 스타일 작업을 진행하였다. (bootstrap과 같이 웹, 앱 디자인 개발을 위한 프레임워크이다.)

 

index.js

pages 폴더 예시

 

pages 폴더 하위에 파일을 생성하면 next가 자동으로 인식해준다.

라우터 필요없이 주소/login 식으로 접근 가능하다.

next를 통해 localhost로 확인하기위해서는 package.json의 script 부분 수정이 필요하다.

(물론 명령어를 쳐도된다.)

 

package.json

package.json 예시

아래와 같이 하면 포트를 바꿀 수 있다.

// package.json

"scripts": {
	"dev": "next -p 3060"
},

그 후 cmd에서 run 시켜준다.

npm run dev

 

작업을 하다보면 콘솔에 에러가 뜨는것을 확인할 수 있다.

className 콘솔창 예시

babel를 통해 해결해주자.

관련 플러그인을 install 해준다.

npm i babel-plugin-styled-components

설정도 빼먹지말자

 

/front 하위로 .babelrc 파일을 생성하여 아래 내용을 작성한다.

{
    "presets": ["next/babel"],
    "plugins": [
        ["styled-components", {
            "ssr": true,
            "displayName": true
        }]
    ]
}

ssr: 서버사이드 렌더링

displayName: 렌더링이 된 후 보호된 네이밍들을 컴포넌트 이름으로 변경한다.

 

 

redux, saga를 통해 state관리 및 통신작업을 할 것이다.

npm i redux next-redux-wrapper react-redux redux-devtools-extension
npm i redux-saga next-redux-saga axios immer

npm i -d redux-devtools-extension

 

front 하위에 store 폴더를 생성한 뒤 파일을 하나 만들자.

네이밍은 configureStore.js인데, 알아서하자.

내용은 아래와 같다.

import { createWrapper } from 'next-redux-wrapper';
import { applyMiddleware, createStore, compose } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import createSagaMiddleware from 'redux-saga';

import reducer from '../reducers';
import rootSaga from '../sagas';


const loggerMiddleware = ({ dispatch, getState }) => (next) => (action) => {
  console.log(action);
  return next(action);
};

const configureStore = () => {
  const sagaMiddleware = createSagaMiddleware();
  const middlewares = [sagaMiddleware, loggerMiddleware];
  const enhancer = process.env.NODE_ENV === 'production'
    ? compose(applyMiddleware(...middlewares))
    : composeWithDevTools(applyMiddleware(...middlewares));
  const store = createStore(reducer, enhancer);
  store.sagaTask = sagaMiddleware.run(rootSaga);
  return store;
};


const wrapper = createWrapper(configureStore, {
  debug: process.env.NODE_ENV === 'development',
});


export default wrapper;

이렇게 미들웨어를 만든 다음 

_app.js 파일에서 사용해주자.

_app.js 예시

페이지에 _app.js나 _document.js등과 같이 Next.js에 내장되어 있는 _document.js, _app.js, _error.js를 커스터마이징하여 레이아웃을 새롭게 구성하는 방법이 있다.

그냥 pages 폴더 하위에 _app.js라는 네이밍으로 파일을 생성하면된다.

(next에서 제공하는 것으로 특정 컴포넌트 렌더링 시 Layout을 적용 제외하기 위한 방법이라고 한다.)

 

여튼 저기에다가 미들웨어 만들어 둔 것을 wrapper.withRedux로 감싸줘야 프로젝트의 모든 컴포넌트와 페이지에 적용된다고 한다. 

Next.js에서 Redux 사용하기에 대해 읽어보자 --> velog.io/@jehjong/Next.js%EC%97%90%EC%84%9C-Redux%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-Redux-wrapper

 

Next.js에서 Redux사용하기 (Redux wrapper) (1/2)

컴포넌트에서 공통적으로 쓰이는 데이타가 흩어져있기 때문에 부모 컴포넌트에서 데이타를 받아서 자식 컴포넌트에게 각각 보내줘야한다컴포넌트끼리 데이타를 전달하는 과정도 매우 복잡하

velog.io

 

 

+ Recent posts