미루고 미루다 드디어 Docker를 활용하여 배포를 한다.

개념은 나중에 정리하고 바로 시작해보자.

1) next.config.js 파일 수정

/** @type {import('next').NextConfig} */
const path = require('path');
const withImages = require('next-images');

module.exports = {
  output: 'standalone',
  distDir: 'build',
  reactStrictMode: true,
  async rewrites() {
    if (process.env.NODE_ENV === "production") {
      return [
        {
          source: process.env.PRODUCTION_JAVA_SERVER_PATH,
          destination: process.env.PRODUCTION_JAVA_SERVER_URL,
        }

      ];
    } else {
      return [
        {
          source: process.env.JAVA_SERVER_PATH,
          destination: process.env.JAVA_SERVER_URL,
        }
      ];
    }
  },
};
 

2) .env.local 변경

NODE_ENV = 'development'

PRODUCTION_JAVA_SERVER_PATH = '/java/:path*'
PRODUCTION_JAVA_SERVER_URL = 'http://사용하는 IP 주소:8083/:path*'

JAVA_SERVER_PATH = '/java/:path*'
JAVA_SERVER_URL = 'http://localhost:8083/:path*'

 

3) Dockerfile 생성


# 위에서 도커 허브 node 이미지를 기반으로 로컬로 다운로드 및 캐싱 되었기 때문에 이미지를 가져올 수 있다.
FROM node:18.4.0

# 만약 컨테이너 안의 이미지의 경로가 /app 이런식으로 되어있다면 작업할 div 경로를 설정할 수도 있다.
# 설정해주면 COPY 의 두번째 경로를 ./ 이것으로 했을 때 자동으로 /app 경로가 된다.
WORKDIR /app

# package.json 파일을 복사한다. 만약 다시 빌드할 때 변경사항이 없을 경우 npm install까지 그냥 넘어간다.
COPY package.json /app

# 이미지를 받으면 npm install을 자동으로 해줌
RUN npm install


# 어떤 파일이 이미지에 들어가야 하는지 
# 첫 번째 .은 이 프로젝트의 모든 폴더 및 파일들 (Dockerfile을 제외한)
# 두 번째 .은 파일을 저장할 컨테이너 내부 경로 (ex /app)
COPY . /app

# 배포환경으로 설정
ENV NODE_ENV=production

RUN npm run build

# 도케에게 우리가 서버를 실행할 포트를 말해준다.
EXPOSE 3000

# 이미지가 생성될 때 실행되지 않고 컨테이너가 실행될 때 수행하는 명령어
CMD ["npm","start"]

가급적 개발 서버와 Node 버전을 맞춰주어야 한다.

필자는 NVM 을 활용하여 버전을 맞춰주었다.

4) dockerignore 생성

/.dockerignore

.node_modules
.next

5) 이미지 빌드

$ docker build -t 도커허브이름/web-client:버전정보 . ex) docker build -t ohkwonseok/web-client:1.0.1 .

 

만약 맥북에서 빌드하고 리눅스, amd64 서버에 배포할 예정인 경우

$ docker buildx build --platform=linux/amd64 -t 도커허브이름/web_client:버전정보 .
ex) docker buildx build --platform=linux/amd64 -t ohkwonseok/web-client:1.0.1 .

개발서버 및 배포서버의 운영체제에 따라 알맞게 선택하여 둘 중 하나를 사용하면 된다.

6) Dockerhub Repository 생성

 

회원가입을 안했을 경우 회원가입을 해주고, 이미 회원가입이 되어있다면

우측 상단의 Sign In 을 클릭하여 로그인한다.

*** 이때, username 이 실질적인 docker hub name 이 되므로, 비교적 간단하게 작성바란다.

명령어에 계속 작성할 일이 많다. ***

7) Create repository 클릭하여 생성한다. (별로 어렵지 않으니 나머지 부분은 생략)

8) 이제 빌드한 것을 도커허브에 업로드한다.

$ docker push 도커허브이름/web-client:1.0.1
ex) docker push ohkwonseok/web-client:1.0.1

*** 이제부터 리눅스 서버에 도커를 배포하는 것을 진행한다. ***

1) 도커 설치

$ sudo wget -qO- http://get.docker.com/ | sh

2) 도커 로그인

$ docker login
 

3) 도커 시작

$ sudo systemctl start docker
 

4) 도커허브에서 이미지 파일 받기(Pull)

$ docker pull 도커허브이름/web_client:버전정보
ex) docker pull ohkwonseok/web_client:1.0.1

5) 이미지 id 확인

$ docker images
 

6) 이미지 컨테이너 실행

$ docker run -p 80:3000 -d --rm 도커허브이름/web_client:버전정보

3000 번 포트를 80포트로 전환 실행한다.

끝~~!

 

지난번에 useRef 를 활용, 버튼을 클릭하여 특정 위치로 이동하는 이벤트를 만들어보았다.

이번 포스팅은 그와 함께 쓸 스크롤을 방향을 인식하여 위,아래로 한단계씩 이동하는

스크롤을 만들어볼 것이다.

일단 스크롤 이벤트를 정의할 때, 크게 세가지로 구분해보자.

1.scroll

2.mouse wheel

3.touch

이렇게 3가지가 있는 데, 필자의 대상은 핸드폰에서 터치로 스크롤 이동하기 때문에 3번 터치 이벤트를 세부적으로 알아보면,

1. touch start -> 터치를 시작할 때 이벤트 발생

2. touch move -> 터치 진행중일 때 이벤트 발생, 터치중일때는 다수의 이벤트가 발생한다.

3. touch end -> 터치가 끝났을 때 이벤트 발생

4. touch cancle -> 터치를 취소할 때 이벤트 발생, 가량 터치중일 때 다른 웹페이지로 이동한다거나 접속장애로 페이지가 다운될 때에도 발생될 수 있다.

통상적으로는 터치가 시작할때의 좌표값을 인지하고, 터치가 종료되었을 때의 좌표값을 인지하여

이벤트를 발생시키므로, 1 & 3번을 활용하여 이벤트를 작성해보자.

아래 포스팅을 확인하면, useRef 의 설정은 동일하다.

위 포스팅의 코드를 유지한 채 아래 코드를 추가한다.

* 필요한 부분만 넣었으므로, 상관없는 코드는 배제하고 작성하겠다.

1. JS 부분

 
 let start_y : any = null; // 시작 Y축 좌표
  let end_y : any = null; // 시작 Y축 좌표
  let index = 0; // 페이지 번호

useEffect(()=>{
    const script = document.createElement('script');
    document.getElementById('app')?.scrollTo(0, 0); // 첫 렌더시 스크롤이 최상단 고정된다

  window.addEventListener("touchstart", initTouch); // 터치 스타트 이벤트를 initTouch에 부여
  window.addEventListener("touchend", (e) => { // 터치 엔드 이벤트를 swipeEnd에 부여
    window.addEventListener("touchend", swipeEnd)
  }, { passive : true});
  }, [])

const initTouch = (e : any) => { 
    e.preventDefault();
    start_y = e.touches[0].pageY; //최초 스타트 시 Y축 좌표를 start_y 변수에 저장한다.
  };

 const swipeEnd = (e:any) => {
    e.preventDefault();
    end_y = e.changedTouches[0].pageY; // 터치가 끝났을 때 Y축 좌표를 end_y 변수에 저장한다.
    if(start_y > end_y){ // 위에서 아래로 이동
      if(index === 0){ 
        element.main.current?.scrollIntoView({ behavior: 'smooth',  block: 'start'});
      }else if(index === 1){
        element.intro.current?.scrollIntoView({ behavior: 'smooth',block: 'start' });
      }else if(index === 2){
        element.customer.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
      }else if(index === 3){
        element.worker.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
      }else if(index === 4){
        element.labor.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
      }else if(index === 5){
        element.schedule.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
      }else if(index === 6){
        element.wage.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
      }
      if(index <= 6){
        index += 1;
      }
    }else if(start_y < end_y){  // 아래에서 위로 이동
      if(index === 7){
        element.wage.current?.scrollIntoView({ behavior: 'auto', block: 'start' });
      }else if(index === 6){
        element.schedule.current?.scrollIntoView({ behavior: 'auto', block: 'start' });
      }else if(index === 5){
        element.labor.current?.scrollIntoView({ behavior: 'auto', block: 'start' });
      }else if(index === 4){
        element.worker.current?.scrollIntoView({ behavior: 'auto', block: 'start' });
      }else if(index === 3){
        element.customer.current?.scrollIntoView({ behavior: 'auto', block: 'start' });
      }else if(index === 2){
        element.intro.current?.scrollIntoView({ behavior: 'auto', block: 'start' });
      }else if(index === 1){
        element.main.current?.scrollIntoView({ behavior: 'auto', block: 'start' });
      }
      if(index >= 0){
        index -= 1;
      }
    }
}

2. HTML 부분

<div id='app' style={{overflowY : 'scroll' }} >
          <Section position={`relative`} ref={element.main}>. // 0페이지
            <SectionInner>
              <Main />
            </SectionInner>
          </Section>
         <Section position={`relative`} ref={element.intro}>  // 1페이지
            <SectionInner>
              <Intro />
            </SectionInner>
          </Section>
         <Section position={`relative`} ref={element.customer}>  // 2페이지
            <SectionInner>
              <CustomerStepper />
            </SectionInner>
          </Section>

          <Section position={`relative`} ref={element.worker}> // 3페이지
            <SectionInner>
              <WorkerStepper />
            </SectionInner>
          </Section>

          <Section position={`relative`} ref={element.labor}> // 4페이지
            <SectionInner>
              <LaborStepper />
            </SectionInner>
          </Section>

          <Section position={`relative`} ref={element.schedule}> // 5페이지
            <SectionInner>
              <ScheduleStepper />
            </SectionInner>
          </Section>
          <Section position={`relative`} ref={element.wage}> //6페이지
            <SectionInner>
              <WageStepper />
            </SectionInner>
          </Section>
</div>

 

브라우저 호환성 때문인지, 크롬 개발자모드에서 핸드폰모드로 할시,

터치 스크롤을 할때 scrollIntoView({ behavior: 'auto', block: 'start' });

auto모드는 잘 동작하지만,

scrollIntoView({ behavior: 'smooth', block: 'start' });

smooth 모드는 동작하지 않았다. 왜 이러지 하면서 실제 핸드폰으로 테스트해보니 잘 동작한다.

다양한 기기에서 테스트해보길 권장한다.

 

Next.js 를 모바일 웹용으로 사용하던 중 이미지와 css 를 늦게 불러오는 이슈를 발견했다.

아마도 SSR 에서 Document를 불러올 때 HTML을 우선 불러오고 나중에 css 를 불러오는 듯하다.

 

 

CSS가 늦게 불러와져서 화면이 깨진다.

구글링을 해보니 몇 가지 옵션을 추가하면 된다고 한다.

1.Next.config.js 옵션 추가

 
 compiler : {
    styledComponents : true,
  },

 

2._document.tsx 추가 (_app.tsx 와 별개로 페이지를 만들어야 함)

 

import Document, { DocumentContext } from "next/document";
import { ServerStyleSheet } from "styled-components";

export default class MyDocument extends Document {
  static async getInitialProps(ctx: DocumentContext) {
    const sheet = new ServerStyleSheet();
    const originalRenderPage = ctx.renderPage;

    try {
      ctx.renderPage = () =>
        originalRenderPage({
          enhanceApp: (App) => (props) =>
            sheet.collectStyles(<App {...props} />),
        });

      const initialProps = await Document.getInitialProps(ctx);
      return {
        ...initialProps,
        styles: [initialProps.styles, sheet.getStyleElement()],
      };
    } finally {
      sheet.seal();
    }
  }
}

적용 후 화면.. 매우 잘 된다.

 

 

 

 

생각보다 쉽다

리액트에서 특정 메뉴를 클릭할 경우

특정 위치로 이동하는 것을 해보자.

* 시나리오

1) 메뉴 버튼 클릭

2) 메뉴 리스트에서 특정 버튼 클릭

3) 이동

import { useState, useRef} from 'react';
import {
  MenuButton,
  Section,
  SectionInner,
  NavItem,
  Navigation,
  TitleWrapper,
} from './promotionComponents';

const Promotion: NextPage<any> = (props: any) => {

  const [menuOpen , setMenuOpen] = useState(false); // 메뉴 팝업
   
    const handleClick = () => {
    setMenuOpen(true);
    };
    const handleClose = () => {
    setMenuOpen(false);
    };
    const element = {
      customer : useRef<HTMLDivElement>(null), 
      worker : useRef<HTMLDivElement>(null),
      labor : useRef<HTMLDivElement>(null),
      schedule : useRef<HTMLDivElement>(null),
      wage : useRef<HTMLDivElement>(null),
      insurance : useRef<HTMLDivElement>(null),
    };
    const onMoveToElement = (e:any) => {
      const targetId = e.target.id;
      if(targetId === 'customer'){ // 타겟이 거래처면 
        element.customer.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }); //거래처로 이동하라
        setMenuOpen(false); // 메뉴는 종료
    };

   return (
   <>
     <MenuButton
          onClick={handleClick}
      >
        <MenuIcon fontSize="large"></MenuIcon>
      </MenuButton>

       {menuOpen ?
          <Navigation>
            <NavItem id= 'customer' onClick={(e:any) => onMoveToElement(e)}>사업장 관리</NavItem>
            <NavItem id= 'worker' onClick={(e:any) => onMoveToElement(e)}>근로자 관리</NavItem>
            <NavItem id= 'labor' onClick={(e:any) => onMoveToElement(e)}>근로계약서</NavItem>
            <NavItem id= 'schedule' onClick={(e:any) => onMoveToElement(e)}>근무일정</NavItem>
            <NavItem id= 'wage' onClick={(e:any) => onMoveToElement(e)}>임금명세서</NavItem>
            <NavItem id= 'insurance' onClick={(e:any) => onMoveToElement(e)}>4대보험</NavItem>
            <div style={{ backgroundColor:'rgba(0,0,0,0.6)', width:'100%', height:'100%'}} onClick={handleClose}></div>
        </Navigation>
        :<></>
       }
        // 이동 대상 거래처 화면, ref 부분의 element.customer 를 확인하자
         <Section position={`relative`} ref={element.customer}>
            <SectionInner>
              <WrapperDiv ju={`center`}>
                <TitleWrapper>
                  <TitleP fontSize={`20px`}>
                    사업장 관리
                  </TitleP>
                </TitleWrapper>
              </WrapperDiv>
              <CustomerStepper />
            </SectionInner>
          </Section>
</>
  );



}

export default Promotion;
 

결과

 

 

끝~

오픈소스로 나와있는 calendar api 들이 많지만,

필자의 경우 커스텀할 일이 많아 직접 캘린더를 구현해볼 것이다.

1.전제조건

1) 종료일자기준 30일전부터 캘린더에 전부 표현할 것 , 1개의 캘린더에 모든 날짜가 들어간다.

2) 설명을 위한 글이므로 소스코드를 여러페이지에 분리하지 않고 한 페이지에 구현

3) state, dispatch 등 calendar 설명에 불 필요한 부분은 과감히 생략

4) 꼭 필요한 부분은 소스코드에 명시할 것이며 css 관련해서는 자세하게 작성하지 않는다.

5) 순수하게 캘린더에 필요한 부분만 작성할 것이며 상황에 따른 커스텀은 따로 하길 바란다.

6) 설명을 위해 일부 주석이 들어갈 수 있다.

2. 소스코드(JS)

 

import React, {useState } from 'react';
import moment from 'moment'; 
    const [dayArray, setDayArray] = useState<any>({ array: [] }); // 실제 캘린더 데이터

    const dayInit: any = {
        monthIndex: 0,
        weekIndex: 0,
        weekData: "",
        day: "--",
        status: {
            영업구분 : { check : true, 근로자수 : 0} ,
        },
        func: {
            onClick: false,
        }
    }
    var calcEndDate = moment(data.basicInfoState.customer_calc_basic_date);
    var calcStartDate = moment(data.basicInfoState.customer_calc_basic_date).subtract(30,'d');
    var tempCalcStartDate =  moment(data.basicInfoState.customer_calc_basic_date).subtract(30,'d');

  const dayArrayInit = () => { // 캘린더 작성 버튼 클릭 이후
       
        let monthIndex = 0; // 일자별 구분
        let totalResult: any[] = []; // 전체 배열
        let calc_day : any = calcStartDate; // 시작일자 기준값
        if(calc_day.day() === 0){  // 종료일자 기준 30일 전이 일요일이면 아무행위를 하지않음
          
        }else { // 종료일자 기준 30일 전이 일요일이 아닐 경우 해당 날짜만큼 차감하여 일요일로 만든다.
            calc_day.subtract(calc_day.day(),'days');
          }

            for(let j= calc_day.week(); j <= calcEndDate.week(); j++){ // 첫주차~마지막주차까지 반복
                let weekArray : any[] = []; // 일주일 단위 데이터
                for(let z = 0; z < 7; z++){ // 주차별 일요일~토요일까지 반복한다.

                    var dayData: any = {
                        monthIndex: 0,
                        weekIndex: 0,
                        weekData: "",
                        day: "--",
                        status: {
                            영업구분 : {check : true, 근로자수 : 0},
                        },
                        func: { onClick:  true }
                    }
                    let  dayType : any = moment(calc_day.format('YYYY-MM-DD').day()); // 무슨 요일인지 선언함
              
                   
                    if(calc_day.isAfter(calcEndDate) === false && calc_day.isAfter(tempCalcStartDate) === true){
                      
                     // 시작일자부터 종료일자에 속하면, 정확한 데이터를 넣음
                   
                    weekArray.push({
                        ...dayData,
                        monthIndex: monthIndex,
                        weekIndex: z,
                        weekData: moment(calc_day.format('YYYY-MM-DD').day()),
                        day: calc_day.format('YYYY-MM-DD');
                        func: { onClick:  true }
                    });

                    }else{

                     // 날짜 범위밖에서는 날짜를 입력하지 않고, 클릭할 수 있는 기능을 없앰
                        weekArray.push({
                            ...dayInit,
                            monthIndex: 0,
                            weekIndex: z,
                            weekData: moment(calc_day.format('YYYY-MM-DD')).day(),
                        }) ;

                    }
                    calc_day.add(1,'days'); // 데이터를 넣었으니 기준일자를 하루씩 증가시킨다.
                    monthIndex++; // monthIndex 값을 1씩 증가
                }
               
                var jsonData = {
                    'weekData' : weekArray,
                };
                totalResult.push(jsonData); // 실제 데이터를 넣는다.
        }
        data.setDayArray({ array: totalResult }); // 실제 캘린더 데이터 배열
  }

이것으로 1차적으로 캘린더를 생성하는 작업은 끝났다.

이제 이 배열을 가공하여 HTML, CSS 작업을 하면 되겠다.

** 원래 시작일자와 종료일자를 나눠, 월 단위로 캘린더를 그려지는 것을

설명하려 했는 데, 소스코드가 너무 복잡하여 이해시킬 자신이 없어

최대한 간단하게 캘린더를 구현했던 버전으로 작성한다.

월별로 캘린더를 나누고 싶은 분은 현재 소스에서 커스텀을 일부 진행하길 바란다.

위 캘린더는 moment.js를 다수 활용했으니, 메서드에 대해서 모르시는 분은 아래 글들을 참고하자.

 

2025.02.10 - [JS] - JS 날짜 라이브러리중 Moment.js에 대해 알아보자

 

리액트를 하다보면 부모 와 자식간에 props를 넘길 일이 많은데,

부모 밑에 직계자식만 있는 것이 아닌

Master -> slave1 -> slave2 -> slave3 -> ... 뭐 이런식으로

중간 단계가 많은 경우가 있는 데,

내가 사용할 곳은 slave3 단계에서만 필요한데 Master 에서 선언할 경우

단계별로 Props를 다 넘겨야 되니 여간 귀찮은 게 아니다.

그럴 때를 대비하여 React Hooks 에서 UseContext API 를 갖춰놓았으니,

활용해보도록 하자.

나는 타입스크립트 환경이므로 각자 환경에 맞는 문법을 찾아 사용해보자.

1. Master.tsx

import { useState, createContext } from "react";

export const calcDataContext : any = createContext('');  

const Master = (props: any) => {
   var machineData = 'Machine-01';
   var mainData = 'Factory-01';
    const calcData = {
      machineData,
      mainData,
    }

 return (
     <calcDataContext.Provider value={{calcData}} >
        <Master />
      </calcDataContext.Provider>

 )
}
export default Input;

 

코드를 보면 알겠지만 최상위단을 useContext.Provider 로 감싸준다.

이렇게 해줌으로 써 Master의 자식들은 Master의 값을 호출할 수 있다.

2. Slave1.tsx (코드를 작성하지 않으므로 생략 - 중간단계)

3. Slave2.tsx (코드를 작성하지 않으므로 생략 - 중간단계)

4. Slave3.tsx (목표지점)

import { calcDataContext } from "../..";
import { useContext } from "react";

const Slave3 = ({  
}: any) => {
 const data : any = useContext(calcDataContext);
    return (
     <>
        <h1>{data.calcData.machineData}</h1>
        <h2>{data.calcData.mainData}</h2>
     </>
    ) 
}
export default withResizeDetector(Slave3);

사용하고 싶은 자식 소스단에서 부모의 useContext 를 호출하여 값을 활용할 수 있다.

변수뿐만 아니라 함수같은 것들도 같이 호출할 수 있으므로, 잘 활용하면 매우 편하다는 것을 알 수 있다.

앞으로는 useContext 를 활용하여

Props를 꼭 필요한 곳에서만 호출하여 쓰자!

 

Next.js 에서 node_module 을 사용하지 않고 스크립트를 직접 불러오는 방식을 적어보자.

Next.js의 _app.tsx 파일

Next.js 의 _app.tsx 파일에

import를 한다.

다만 <script> 속성안에

async 또는 defer 를 추가해야 하는데, 추가하지 않는다면 추후 빌드를 할 때

경고메시지가 뜨므로 꼭 추가해야 한다.

async 는 여러분도 많이 알다시피 비동기성으로 읽히며,

스크립트가 먼저 다운로드된 순서대로 실행하며,

defer 는 HTML 을 전부 읽은 뒤에 실행한다.

가급적 defer 를 사용하는 게 안정성면에서는 좋은 거 같다.


+ Recent posts