SMALL

 

app router공식문서 한글화 정리

https://velog.io/@asdf99245/Next.js-app-router-%EA%B3%B5%EC%8B%9D%EB%AC%B8%EC%84%9C-%EC%A0%95%EB%A6%AC#fetch-api

 

Next.js app router 공식문서 정리

지난번엔 next.js pages router 공식문서 정리글을 올렸다. 이번에는 최근에 stable이 된 app router 공식문서의 핵심 내용에 대해 번역 및 정리해보았다.

velog.io

 

 

서버컴포넌트에서 데이터 prop으로 가져오기 
https://velog.io/@yoonvelog/React-Server-Component

 

React Server Component

한 페이지에서 여러 정보를 보여주는 컴포넌트를 개발한다면 우리는 흔히 아래와 같이 컴포넌트를 구성한다.부모 컴포넌트인 와 자식 컴포넌트인 , 모두 다른 유저의 정보가 필요하고 필요한

velog.io

 

LIST

'일반' 카테고리의 다른 글

Next 배포 시 TypeError fetch failed  (0) 2024.04.19
레벨 별 사운드 구현하기  (0) 2024.04.17
새 글 정보 알람 기능  (0) 2024.03.22
로컬스토리지에 값 저장하기  (0) 2024.03.22
modal dialog component만들기  (0) 2024.03.20
SMALL

배열의 길이를 이용하여 새글이 추가됐는지를 추적하고자 함

1. useState를 이용하여 배열의상태와 알람이 켜졌는지를 추적함

    const [allErrors, setAllErrors] = useState<{ [key: string]: any }[]>([]);
    const [isAlarmChecked, setIsAlarmChecked] = useState(true);

 

2. useEffect를 이용하여 배열의 정보가 업데이트가 되는 것을 추적

  useEffect(() => {
    const farmCodes = Object.keys(groupedMessages);
    let tempAllErrors:{[key:string]:any}[] = [];

    farmCodes.forEach(farmCode => {
      const { errorList, robot, farmName } = groupedMessages[farmCode];
      if (errorList && errorList.length > 0 && robot.length > 0) {
        tempAllErrors = [...tempAllErrors, ...errorList.map(error => ({
          ...error,
          farmCode,
          farmName,
        }))];
      }
    });

    setAllErrors(tempAllErrors);
  }, [groupedMessages]); // groupedMessages가 변경될 때마다 알람 목록 업데이트

  useEffect(() => {
    if (allErrors.length > 0) {
      setIsAlarmChecked(false); // 새로운 알람이 있을 경우 빨간점 표시
    }
  }, [allErrors.length]);

 

3. 특정버튼을 클릭시 알람이 체크된 것을 표시하고 isAlarmChecked을 컴포넌트에 전달

setIsAlarmChecked(true)
LIST

'일반' 카테고리의 다른 글

레벨 별 사운드 구현하기  (0) 2024.04.17
fetch를 위해 알아본 자료들  (0) 2024.04.02
로컬스토리지에 값 저장하기  (0) 2024.03.22
modal dialog component만들기  (0) 2024.03.20
필터기능 구현  (0) 2024.03.18
SMALL

1. 페이지 정보를 로컬스토리지에 저장하려고 하는데 라우팅을 사용하지않고 좌우 컴포넌트를 별개로 두고 zustand 에 저장값을 넣어서 적용시킴

 

import {create} from 'zustand';

type Store = {
  leftComponent: string | null;
  rightComponent: string | null;
  activeButton: string | null;
  setLeftComponent: (name: string) => void;
  setRightComponent: (name: string) => void;
  setActiveButton: (name: string) => void;
};

export const usePageStore = create<Store>((set) => ({
  leftComponent: null,
  rightComponent: null,
  activeButton: null,
  setLeftComponent: (name) => set(() => ({ leftComponent: name })),
  setRightComponent: (name) => set(() => ({ rightComponent: name })),
  setActiveButton: (name) => set(() => ({ activeButton: name })),
}));

 

 

2. 여기서 zustand에서 저장하는 값을 바로 localStorage에 업데이트함

3. 로컬스토리지에 값이 있으면 그 값을 불러오고 값이 없으면 빈객체를 가져옴

import { create } from 'zustand';

// 스토어 타입 정의
type Store = {
  leftComponent: string | null;
  rightComponent: string | null;
  activeButton: string | null;
  setLeftComponent: (name: string) => void;
  setRightComponent: (name: string) => void;
  setActiveButton: (name: string) => void;
};

// 로컬 스토리지 키 이름 정의
const STORE_KEY = 'page';

// 로컬 스토리지에서 스토어 상태 로드
function loadFromLocalStorage() {
  const savedState = localStorage.getItem(STORE_KEY);
  return savedState ? JSON.parse(savedState) : {};
}

// 스토어 생성 및 초기 상태 설정
export const usePageStore = create<Store>((set) => ({
  ...loadFromLocalStorage(), // 로컬 스토리지에서 로드한 상태로 초기화
  setLeftComponent: (name) => set(() => ({ leftComponent: name })),
  setRightComponent: (name) => set(() => ({ rightComponent: name })),
  setActiveButton: (name) => set(() => ({ activeButton: name })),
}));

// 스토어 상태 변경 시 로컬 스토리지에 저장
usePageStore.subscribe((state) => {
  localStorage.setItem(STORE_KEY, JSON.stringify(state));
});
LIST

'일반' 카테고리의 다른 글

fetch를 위해 알아본 자료들  (0) 2024.04.02
새 글 정보 알람 기능  (0) 2024.03.22
modal dialog component만들기  (0) 2024.03.20
필터기능 구현  (0) 2024.03.18
자료구조 변경 비교  (0) 2024.03.13
SMALL

1. zustand 에 modal dialog관련 store생성하기

import {create} from 'zustand';

interface ModalState {
  isModalOpen: boolean;
  openModal: () => void;
  closeModal: () => void;
}

// 모달 상태를 관리하는 스토어
export const useModalStore = create<ModalState>((set) => ({
  isModalOpen: false, // 모달의 초기 상태는 닫혀있음
  openModal: () => set({ isModalOpen: true }), // 모달을 여는 액션
  closeModal: () => set({ isModalOpen: false }), // 모달을 닫는 액션
}));

 

 

2. Component만들기

  • draggable한 modalComponent를 만들기위해 mouseup mousedown mousemove이벤트핸들러 이용
  • 뒤쪽 배경을 누르면 종료되게 설정 이벤트 버블링을 막기위해서 e.stopPropagation과 e.preventDefault 적절하게 활용하기
  • 배경보다 상위에 노출되기 위해 z-index적용하기
import { useState, useEffect } from 'react';
import { useModalStore } from '@/store/useModalStore';

// 마우스 이벤트 핸들러에서 사용될 위치 타입 정의
interface Position {
  x: number;
  y: number;
}

const ModalComponent: React.FC = () => {
  const { isModalOpen, closeModal } = useModalStore();
  const [position, setPosition] = useState<Position>({ x: 0, y: 0 });
  const [dragging, setDragging] = useState<boolean>(false);
  
  // 마우스 클릭 위치 저장 변수의 타입도 Position으로 지정
  const [rel, setRel] = useState<Position | null>(null);
  const { groupedMessages } = useMqttWorkerStore();

//표시될 내용 관련부분 생략

  useEffect(() => {
    // 화면의 크기를 기반으로 중앙 위치 계산
    const updatePosition = () => {
      const x = window.innerWidth / 2;
      const y = window.innerHeight / 2;
      setPosition({ x, y });
    };

    // 컴포넌트 마운트 시에 위치 업데이트
    updatePosition();
  }, []); // 빈 의존성 배열로 마운트 시에만 실행
  useEffect(() => {
    const moveHandler = (e: MouseEvent) => handleMouseMove(e);
    const upHandler = (e: MouseEvent) => handleMouseUpDOM(e);

    if (dragging) {
      document.addEventListener('mousemove', moveHandler);
      document.addEventListener('mouseup', upHandler);
    } else {
      document.removeEventListener('mousemove', moveHandler);
      document.removeEventListener('mouseup', upHandler);
    }

    return () => {
      document.removeEventListener('mousemove', moveHandler);
      document.removeEventListener('mouseup', upHandler);
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [dragging, rel]);

  if (!isModalOpen) return null;

  const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
    setDragging(true);
    setRel({
      x: e.pageX - position.x,
      y: e.pageY - position.y,
    });
    e.stopPropagation();
    e.preventDefault();
  };

  const handleMouseMove = (e: MouseEvent) => {
    if (!dragging || !rel) return;
    setPosition({
      x: e.pageX - rel.x,
      y: e.pageY - rel.y,
    });
    e.stopPropagation();
    e.preventDefault();
  };

  // DOM 이벤트를 위한 핸들러
  const handleMouseUpDOM = (e: MouseEvent) => {
    if (!dragging) return;
    setDragging(false);
    e.stopPropagation();
    e.preventDefault();
  };

  // 리액트 이벤트를 위한 핸들러
  const handleMouseUpReact = (e: React.MouseEvent<HTMLDivElement>) => {
    e.stopPropagation();
    e.preventDefault();
    setDragging(false);
  };

  return (
    <div onClick={closeModal} className="fixed bg-[rgba(167,221,158,0.5)] w-screen h-screen z-[100] top-0">
      <div
        style={{ left: `${position.x}px`, top: `${position.y}px` }}
        className="fixed bg-white border min-w-96 min-h-80 p-5 rounded-lg cursor-move"
        onMouseDown={handleMouseDown}
        onMouseUp={handleMouseUpReact}
        onClick={(e)=>e.stopPropagation()}
      >
         <p className="text-black">모달창입니다</p>
        <button
          type="button"
          onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
            e.stopPropagation();
            closeModal();
          }}
          className="absolute text-lg top-4 right-5"
        >
          ×
        </button>
      </div>
    </div>
  );
};

export default ModalComponent;

 

3. Component호출하기

LIST

'일반' 카테고리의 다른 글

새 글 정보 알람 기능  (0) 2024.03.22
로컬스토리지에 값 저장하기  (0) 2024.03.22
필터기능 구현  (0) 2024.03.18
자료구조 변경 비교  (0) 2024.03.13
Next.js에서 mysql2 연결하기 (net error 해결기)  (0) 2024.02.29
SMALL

1. 정렬 조건은 zutand에서 전역으로 관리

2. 렌더링부분과 count세는부분을 별도로 관리

 

import { useButtonStore } from '@/store/useButtonStore';


const RobotMilkingList: React.FC = () => {
  const { activeFilterButton, setActiveFilterButton} = useButtonStore();
  
  const [allCount, setAllCount] = useState(0);
  const [normalCount, setNormalCount] = useState(0);
  const [errorCount, setErrorCount] = useState(0);
  
  const farmCodes = Object.keys(groupedMessages);
  let sortedFarmCodes;
  
  //정렬코드 생략
  
    const handleClick = (e: React.MouseEvent<HTMLButtonElement>): void => {
    const list = e.currentTarget.dataset.list;
    
    if (!list) return;
    
    setActiveFilterButton(list);
  };

//렌더링 부분
  const renderFarmList = () => {
    let filteredFarmCodes;
  
    switch (activeFilterButton) {
      case 'error':
        filteredFarmCodes = sortedFarmCodes.filter((farmCode) => (groupedMessages[farmCode].errorList.length > 0) && (groupedMessages[farmCode].robot.length > 0));
        break;
      case 'normal':
        filteredFarmCodes = sortedFarmCodes.filter((farmCode) => (groupedMessages[farmCode].errorList.length === 0) && (groupedMessages[farmCode].robot.length > 0));
        break;
      case 'all':
      default:
        filteredFarmCodes = sortedFarmCodes.filter((farmCode) => groupedMessages[farmCode].robot.length > 0);
        break;
    }
  
    return filteredFarmCodes.map((farmCode, index) => {
      return (
        <FarmItem
        key={index}
        farmCode={farmCode}
        cpu={groupedMessages[farmCode].rman?.cpu}
        ram={groupedMessages[farmCode].rman?.ram}
        hdd={groupedMessages[farmCode].rman?.hdd}
        sql={groupedMessages[farmCode].rman?.sql}
        farmName={`${groupedMessages[farmCode].farmName} (${farmCode})`}
        errorList={groupedMessages[farmCode].errorList.length > 0}
        robotState={groupedMessages[farmCode].robot}
        gateState={groupedMessages[farmCode].smartgate}
        rmanTime={groupedMessages[farmCode].rman?.rmanTime}
        webTime={groupedMessages[farmCode].web?.webTime}
        />
        );
      });
    };
    
    // 카운트를 업데이트하는 로직
useEffect(() => {
  const allFiltered = sortedFarmCodes.filter((farmCode) => groupedMessages[farmCode].robot.length > 0);
  const normalFiltered = sortedFarmCodes.filter((farmCode) => (groupedMessages[farmCode].errorList.length === 0) && (groupedMessages[farmCode].robot.length > 0));
  const errorFiltered = sortedFarmCodes.filter((farmCode) => (groupedMessages[farmCode].errorList.length > 0) && (groupedMessages[farmCode].robot.length > 0));

  setAllCount(allFiltered.length);
  setNormalCount(normalFiltered.length);
  setErrorCount(errorFiltered.length);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sortedFarmCodes, activeFilterButton])
    
  return (
    <>
      <section className="grid-container h-max w-full gap-y-2 py-2.5 pr-2.5">
        <h2 className="sr-only">목장별 리스트</h2>
        {renderFarmList()}
        <SortButton />
        <CountFarm allCount={allCount} normalCount={normalCount} errorCount={errorCount} onClick={handleClick}/>
      </section>
    </>
  );
};
LIST
SMALL

기존 : mqtt 수신내역을 배열형태로 일괄적으로 담음

변경 후 : mqtt수신 시 farmCode기준으로 묶어서 내부에 필요한 정보만 저장

 

기존 호출 컴포넌트

const ResizableComponent: React.FC<ResizableProps> = () => {
  const workerRef = useRef<Worker | null>(null);
  const { messages, addMessage } = useMqttStore();

useEffect(() => {
    const URL = '/mqttWorker.built.js';
    if (!workerRef.current) {
      const worker = new Worker(URL);

      workerRef.current = worker;
      workerRef.current.onmessage = function (event) {
        // console.log(typeof event.data.message);
        
        if (event.data.message) {
          const messageTime = extractTime(event.data.message);
          const currentTime = new Date()
            .toLocaleString('ko-KR', {
              hour12: false,
              year: 'numeric',
              month: '2-digit',
              day: '2-digit',
              hour: '2-digit',
              minute: '2-digit',
              second: '2-digit',
            })
            .replace(/. \d{4}/, '')
            .replace(/(\d{4})\. (\d{2})\. (\d{2})\. (\d{2}):(\d{2}):(\d{2})/, '$1-$2-$3 $4:$5:$6');
          const now = new Date();
          const sevenDaysAgo = new Date(now.setDate(now.getDate() - 7));

          if (new Date(messageTime) >= sevenDaysAgo) {
            const splitTopic = event.data.topic.split('/');
            const farmCode = splitTopic.length > 2 ? splitTopic[2] : '';

            // 메시지 객체 생성
            const messageObject = {
              topic: event.data.topic,
              message: event.data.message,
              time: messageTime,
              currentTime: currentTime,
            };

            addMessage(payload);
          }
        }
      };

      if (workerRef.current) {
        workerRef.current.postMessage({ CMD: 'MQTT_START' });
        workerRef.current.postMessage({
          CMD: 'MQTT_SUBSCIBE',
          DATA: ['dawoon/ALIVE/#', 'dawoon/Robot/#', 'dawoon/RMAN/#', 'dawoon/Feeder/#', 'dawoon/SMARTGATE/#'],
        });
      }
    }
return function cleanup() {
      if (workerRef.current) {
        workerRef.current.terminate();
        workerRef.current = null;
      }
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

 

 

변경 호출컴포넌트 => 그룹으로 변경

const ResizableComponent: React.FC<ResizableProps> = () => {
const workerRef = useRef<Worker | null>(null);
const {groupedMessages, addGroupedMessage } = useGroupedMqttStore()

  useEffect(() => {
    const URL = '/mqttWorker.built.js';
    if (!workerRef.current) {
      const worker = new Worker(URL);

      workerRef.current = worker;
      workerRef.current.onmessage = function (event) {
        // console.log(typeof event.data.message);
        
        if (event.data.message) {
          const messageTime = extractTime(event.data.message);
          const currentTime = new Date()
            .toLocaleString('ko-KR', {
              hour12: false,
              year: 'numeric',
              month: '2-digit',
              day: '2-digit',
              hour: '2-digit',
              minute: '2-digit',
              second: '2-digit',
            })
            .replace(/. \d{4}/, '')
            .replace(/(\d{4})\. (\d{2})\. (\d{2})\. (\d{2}):(\d{2}):(\d{2})/, '$1-$2-$3 $4:$5:$6');
          const now = new Date();
          const sevenDaysAgo = new Date(now.setDate(now.getDate() - 7));

          if (new Date(messageTime) >= sevenDaysAgo) {
            const splitTopic = event.data.topic.split('/');
            const farmCode = splitTopic.length > 2 ? splitTopic[2] : '';

            // 메시지 객체 생성
            const messageObject = {
              topic: event.data.topic,
              message: event.data.message,
              time: messageTime,
              currentTime: currentTime,
            };

            // addGroupedMessage를 사용하여 메시지 추가
            addGroupedMessage(farmCode, messageObject);
          }
        }
      };

      if (workerRef.current) {
        workerRef.current.postMessage({ CMD: 'MQTT_START' });
        workerRef.current.postMessage({
          CMD: 'MQTT_SUBSCIBE',
          DATA: ['dawoon/ALIVE/#', 'dawoon/Robot/#', 'dawoon/RMAN/#', 'dawoon/Feeder/#', 'dawoon/SMARTGATE/#'],
        });
      }
    }

    return function cleanup() {
      if (workerRef.current) {
        workerRef.current.terminate();
        workerRef.current = null;
      }
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

 

 

변경 전 zustand store코드

  • 배열에 담는 구조여서 같은내용을 중복처리 하지 않기 위한 코드 필요
  • 배열에 필요한내용을 추가적으로 담는 구조로 배열의 내용자체가 많고 무거워짐
export const useMqttStore = create<Store>((set, get) => ({
  messages: [],
  addMessage: (payload: Message) => {
    const splitTopic = payload.topic.split('/');
    const parsedMessage = JSON.parse(payload.message);
    const cmd = parsedMessage.CMD;

    // 특정 조건에 따라 추가 필드 설정
    let additionalFields = {};
    
    //추가 설정 내역 생략
    
    // 이름값 가져오기
    if ('FNAME' in parsedMessage || 'FARMNAME' in parsedMessage) {
      additionalFields = { ...additionalFields, farmName: parsedMessage.FARMNAME || parsedMessage.FNAME };
    }
// 새로운 메시지를 추가하는 함수
set((state) => {
  // 이전 메시지 중 중복되지 않은 메시지만을 필터링하여 새 배열을 생성
  const noDuplicateArray = state.messages.filter((message) => {
    // 메시지의 topic을 '/'를 기준으로 분리
    const splitTopic = message.topic.split('/');
    // 분리된 topic의 세 번째 항목이 farmCode
    const farmCode = splitTopic.length > 2 ? splitTopic[2] : '';

    let parsedMessage;
    try {
      // 메시지의 내용을 JSON 형태로 파싱
      parsedMessage = JSON.parse(message.message);
    } catch (error) {
      // JSON 파싱에 실패한 경우 오류를 출력하고, 해당 메시지는 필터링에서 제외
      console.error('Invalid JSON:', message.message);
      return true; 
    }

    // 메시지의 CMD를 가져옴
    const cmd = parsedMessage.CMD;


    if (farmCode === splitTopic[2] && message.topic === payload.topic && cmd === parsedMessage.CMD) {
      // CMD가 "MAIN_ERROR" 또는 "ERROR_SMARTGATE"가 아니라면, 중복으로 판단하고 필터링
      if (cmd !== "MAIN_ERROR" && cmd !== "ERROR_SMARTGATE") {
        // array filter에 의해 noDuplicateArray에 포함하지 않음
        return false;
      }
    }
    
    // 위의 조건들에 해당하지 않는 메시지는 필터링에서 제외하지 않음
    // array filter에 의해 noDuplicateArray에 포함함
    return true;
  });

  // 필터링된 메시지 배열에 새 메시지를 추가하여 상태를 업데이트
  return {
    messages: [...noDuplicateArray, { ...payload, additionalFields }],
  };
});

  },

 

 

변경 후 zustand store 코드

  • 그룹화해서 묶어서 객체형태로 저장(farmCode를  key값으로 가짐)
  • 기존에 가져온 데이터에서 필요한 내용만 추출하여 저장
  • 저장형태를 미리 지정 후(typescript) 그 형태에 맞춰서 지정
import { extractTime } from '@/utils/extractTime';
import { create } from 'zustand';

interface Message {
  topic: string;
  time: string;
  message: any;
  currentTime: string;
  additionalFields?: {
    [key: string]: any;
  };
}
interface GroupedMessages {
  [farmCode: string]: {
    robot: { [key: string]: any };
    smartgate: { [key: string]: any };
    rman: { [key: string]: any };
    web: { [key: string]: any };
    feeder: { [key: string]: any };
    errorList: Message[];
    currentTime: string;
    time: string;
  };
}

interface State {
  groupedMessages: GroupedMessages;
  addGroupedMessage: (farmCode: string, messageObject: Message) => void;
}

const useGroupedMqttStore = create<State>((set) => ({
  groupedMessages: {},
  addGroupedMessage: (farmCode, messageObject) =>
    set((state) => {
      // cmd 필드가 있을 경우 바로 JSON.parse 적용
      if (messageObject.message) {
        try {
          // message 필드가 JSON 문자열이면 파싱
          const message = JSON.parse(messageObject.message);
          messageObject.message = message;
        } catch (e) {
          console.error('message 파싱 오류:', e);
        }
      }

      // 기존 메시지들을 가져옴
      const existingGroup = state.groupedMessages[farmCode] || {
        robot: {},
        smartgate: {},
        rman: {},
        web: {},
        errorList: [],
        feeder:{},
        currentTime: messageObject.currentTime,
        time: messageObject.time,
      };

      // 새 메시지에 대한 추가 필드 설정 로직
      const cmd = messageObject.message.CMD;
      const splitTopic = messageObject.topic.split('/');
      let additionalFields = {};
      
      //추가 필드 설정 삭제
      
            // 메시지를 관련된 객체에 추가
      const messageType = messageObject.topic.split('/')[1].toUpperCase(); 
      switch (messageType) {
        case 'ROBOT':
          existingGroup.robot = { ...existingGroup.robot, ...messageObject.additionalFields };
          if (cmd==='MAIN_ERROR') {
            existingGroup.errorList.push(messageObject)
          }
          break;
        case 'SMARTGATE':
          existingGroup.smartgate = { ...existingGroup.smartgate, ...messageObject.additionalFields };
                      if (cmd==='ERROR_SMARTGATE') {
              existingGroup.errorList.push(messageObject)
            }
          break;
        case 'RMAN':
          existingGroup.rman = { ...existingGroup.rman, ...messageObject.additionalFields };
          break;
          case 'ALIVE':
            if (messageObject.topic.split('/')[1].toUpperCase()==='RMAN'){
            existingGroup.rman = { ...existingGroup.rman, ...messageObject.additionalFields };

          }
          if (messageObject.topic.split('/')[1].toUpperCase()==='WEB'){

            existingGroup.web = { ...existingGroup.web, ...messageObject.additionalFields };
          }
          break;
        case 'FEEDER':
          existingGroup.feeder = { ...existingGroup.feeder, ...messageObject.additionalFields };
          break;
        default:
          console.error('Unknown message type:', messageType);
      }

      // 상태를 업데이트
      return {
        groupedMessages: {
          ...state.groupedMessages,
          [farmCode]: existingGroup,
        },
      };
    }),
}));

export default useGroupedMqttStore;

 

LIST

'일반' 카테고리의 다른 글

modal dialog component만들기  (0) 2024.03.20
필터기능 구현  (0) 2024.03.18
Next.js에서 mysql2 연결하기 (net error 해결기)  (0) 2024.02.29
mqtt연결 시 connected반복 에러  (0) 2024.02.28
정렬기능 보완  (0) 2024.02.27

+ Recent posts