회사 프로젝트에 적용해볼 Atom 라이브러리 테스트 기록

 


 

 

현재 진행중인 프로젝트 특성상 폴링을 엄청나게 땡겨야 한다. 못해도 화면마다 api5개 이상을 5초~10초 단위로 땡겨야 함!

협력사에서도 우리쪽에서 보내는 이벤트를 interval로 땡겨야 하는데 atom이라는 라이브러리를 쓴다고 하시기에 나도 테스트해봄.

 

 

1. Library Atom from 'jotai'

  • Jotai 는 React 애플리케이션에서 사용하는 경량 상태 관리 라이브러리.
    • 여기서 말하는 atom 은 상태 단위를 의미.
  • 쉽게 말하면, atom 하나가 전역적으로 관리되는 상태 하나
import { atom } from 'jotai';

// 1. atom 정의 (상태 생성)
const countAtom = atom(0);

// 2. atom 사용
import { useAtom } from 'jotai';

function Counter() {
  const [count, setCount] = useAtom(countAtom);

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={() => setCount(c => c + 1)}>Increment</button>
    </div>
  );
}

 

atom 을 쓰는 이유?

  • 간단한 전역 상태 관리 (Redux, Recoil 보다 가볍고 빠름)
  • 비동기 API 통합이 쉬움 (atom 안에 fetch, WebSocket 등 넣기 쉬움)
  • 의존성 기반 업데이트 (atom 이 바뀌면 구독하는 컴포넌트만 다시 렌더링)
  • 한 번만 API 관리 (PollingManager 하나로 끝)
  • 여러 화면에서 같은 데이터 사용 가능
    • 새로 고침 / 실시간 업데이트 신경 안 써도 됨
    • 컴포넌트 간 데이터 전달할 필요 없음 (props drilling X)
  • 신규 화면 추가해도 atom 구독만 하면 끝!
PollingManager (API 호출) 
       │
       ↓
[datacenterInfoAtom]   ← 저장소
       │        │
       ↓        ↓
panel1 		panel2

 

 

2. Polling

리액트 특성상 뱅뱅 돌면서 좀 헷갈리는 부분이 있긴 한데

처음엔 [ PollingManager.tsx ] - [ UI컴포넌트.tsx ]  로 호출해서 쓰다가, URL이 워낙 많기때문에 3등분을 했다.

[ PollingManager.tsx ] - [ PollingConfig.ts ] - [ UI컴포넌트.tsx ]

 

PollingManager.tsx

  • PollingConfig.ts에서 apiList를 배열로 넘기면 여기선 singlePolling 컴포넌트로 넘겨서 개별 fetch(interval)을 진행한다.
import {useEffect, useRef} from 'react';
import { useSetAtom, Atom } from 'jotai';

// API 항목 타입 정의
type ApiItem<T> = {
    url: string;
    atom: Atom<T>;
    interval?: number;
};

// PollingManager props 타입
type PollingManagerProps = {
    apiList: ApiItem<any>[]; // 제네릭으로 하려면 <T> 넘겨줘야 하고, 일단 any
    interval?: number;
};

// PollingManager 컴포넌트
function PollingManager({ apiList, interval = 5000 }: PollingManagerProps) {
    return (
        <div>
            {apiList.map(({ url, atom, interval: itemInterval }) => (
                <SinglePolling
                    key={url}
                    url={url}
                    atom={atom}
                    interval={itemInterval ?? interval}
                />
            ))}
        <div/>
    );
}

// SinglePolling props 타입
type SinglePollingProps<T> = {
    url: string;
    atom: Atom<T>;
    interval: number;
};


// SinglePolling 컴포넌트
function SinglePolling<T>({ url, atom, interval }: SinglePollingProps<T>) {
    const setAtom = useSetAtom(atom);
    const previousDataRef = useRef<T>();

    useEffect(() => {
        const fetchData = async () => {
            try {
                const response = await fetch(url);
                const result = (await response.json()) as { success: boolean; message: string; data: T };
                // setAtom(result.data); 리랜더링을 막기위해 전후값을 비교하고 useRef로 처리함
                if (JSON.stringify(result.data) !== JSON.stringify(previousDataRef.current)) {
                    setAtom(result.data);
                    previousDataRef.current = result.data;
                }
            } catch (error) {
                console.error(`Polling error for ${url}:`, error);
            }
        };

        fetchData(); // 초기 로딩
        const timer = setInterval(fetchData, interval);

        return () => clearInterval(timer); // 언마운트 시 정리
    }, [url, atom, interval, setAtom]);

    return null; // UI 없음 (배경에서 동작)
}

export default PollingManager;

 

 

 

PollingConfig.ts

  • polling할 url들 리스트 생성. 
  • url, 구독할 atom이름, setInterval 설정
import {atom} from 'jotai';

type panel1Type = {
    clctDate : string;
    clctTime: string;
    output: number;
}

type panel2Type = {
    deviceId: string;
    clctDate : string;
    clctTime: string;
    supplyTemperature: number;
}

export const panel1Atom = atom<panel1Type[] | null>(null);
export const panel2Atom = atom<panel2Type[] | null>(null);

type pollingApiItemType = {
    url: string;
    atom: any; // atom 타입 더 구체화 가능
    interval: number;
};

// 필요한 api list 준비 (ui에서 호출하는 메서드)
export const getPollingApiList = (screenId: '1' | '2' | '3' | '4', deviceId: number): pollingApiItemType[] => {
    switch (screenId) {
        case '1': // 화면에 따라 url을 나눠서 처리 가능
            return [];
        case '2':
            return [];
        case '3': 
            return [];
        case '4':
            return [
                {
                    url: `http://localhost:8080/panel1List?deviceId=${deviceId}`,
                    atom: panel1Atom, // 위 url은 이 atom이름으로 구독하면 됨
                    interval: 5000,
                },
                {
                    url: `http://localhost:8080/panel2List?deviceId=${deviceId}`,
                    atom: panel2Atom,
                    interval: 3000,
                },
            ];
        default:
            return []; // 안전을 위해 default 도 명시
    }
};

 

 

UIComponent.tsx

  • UI에서 사용할때에는 pollingConfig.ts에 만들어둔 getPollingApiList에 인자 넘김 → 해당하는 apiList[] 반환받음 → PollingManager.tsx에 전달 → 결과값 useAtom(지정한 Atom이름) 으로 구독하여 받아옴 → 파싱하여 사용
const panel1 = memo(() => {
    const [panel1] = useAtom(panel1Atom); // useAtom으로 해당 api구독 (저장된 값 읽어옴)
    return <div>{panel1?.[0]?.clctTime ?? '데이터 없음'}</div>;
});

const panel2 = memo(() => {
    const [panel2] = useAtom(panel2);
    const clctTime = panel2?.[0]?.supplyTemperatrue ?? '데이터 없음';
    return <div>{supplyTemperatrue}</div>;
});

// pollingManager를 바로 호출하지않고 useMemo로 감싸서 호출시 주변 리렌더링을 막음
const PollingWrapper = ({ deviceId }: { deviceId: number }) => {
    const apiList = useMemo(() => getPollingApiList('4', deviceId), [deviceId]);
    return <PollingManager apiList={apiList} interval={5000} />;
};


// [ 최종반환 컴포넌트 ]
const UIComponent = () => {
    // atom test
    const [deviceId, setDeviceId] = useState(1); // deviceId를 1로 세팅하여 PollingManager 호출
  
    return(
        <div>
          	<PollingWrapper deviceId={deviceId} /> // polling manager호출
            <div>
                <panel1 /> // ui렌더링
                <panel2 />
            </div>
		<div/>
    )

}

 

 

3. 예상되는 추후 개선사항

  • 다른곳에서 폴링까지 받아서 값을 다 끼운 컴포넌트를 UIComponent에 import하는게 좀 더 깔끔.  return문에서 pollingManager 호출도 하고 ui도 그리니까 조금 정신없음.
  • 리액트 최고의 적 리렌더링...💥 아직 훅을 잘 못써서 디버깅해보면 같이 끌려와서 리렌더링되는 컴포넌트들이 있는데 조금더 최적화가 필요할것 같다.

node.js 사용하면서 nodemon을 설치하려하는데 설치가 잘 된 것처럼 보였음에도 불구하고, 두가지 오류를 맞딱뜨렸다.

 

1. nodemon을 전역적으로 사용 할 수 있도록 설치 필요

nodemon : 'nodemon' 용어가 cmdlet, 함수, 스크립트 파일 또는 실행할 수 있는 프로그램 이름으로 인식되지 않습니다. 이름이 정확한지 확인하고 경로가 포함된 경우 경로가 올바른지 검증한 다음 다시 시도
하십시오.
위치 줄:1 문자:1
+ nodemon app.js
+ ~~~~~~~
    + CategoryInfo          : ObjectNotFound: (nodemon:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException

 

이 에러 메시지는 nodemon이 시스템에서 인식되지 않는다는 것을 의미합니다. 이는 nodemon이 전역으로 설치되지 않았거나, PATH에 추가되지 않았기 때문일 수 있습니다. 이를 해결하기 위한 몇 가지 방법을 소개합니다.

  1. nodemon을 전역으로 설치하기:
    npm install -g nodemon
    전역 설치가 완료되면 nodemon --version 명령어를 실행하여 제대로 설치되었는지 확인할 수 있습니다.
  2. 전역으로 nodemon을 설치하면 어느 위치에서든 nodemon 명령어를 사용할 수 있습니다. 다음 명령어를 실행하여 전역 설치를 시도해보세요:
  3. npx를 사용하여 nodemon 실행하기:
    npx nodemon app.js
  4. 전역 설치 없이 npx 명령어를 사용하여 프로젝트 내에서 nodemon을 실행할 수 있습니다. 다음과 같이 실행해보세요:

 

 

2. 전역적으로 다운받고 버전도 확인하고! 실행했는데 이젠 보안 오류가 나왔다.

nodemon : 이 시스템에서 스크립트를 실행할 수 없으므로 (경로) 파일을 로드할 수 없습니다. 
자세한 내용은 about_Execution_Policies(https://go.microsoft.com/fwlink/?LinkID=135170)를 참조하십시오.
위치 줄:1 문자:1
+ nodemon app.js
+ ~~~~~~~
    + CategoryInfo          : 보안 오류: (:) [], PSSecurityException
    + FullyQualifiedErrorId : UnauthorizedAccess

 

 

node 처음써서 뭐가 뭔지도 모르는데 이건 더 모르겠으니 지피티 소환.

👉 찾아보니 외부 스크립트가 들어오는걸 파워쉘에서 막고있으므로, 설정을 해주어야 npm 관련 라이브러리들을 쓸 수 있다고 한다

 

 

이 에러는 PowerShell의 실행 정책이 스크립트 실행을 허용하지 않기 때문에 발생합니다. 이를 해결하기 위해서는 PowerShell의 실행 정책을 변경해야 합니다. 그러나, 실행 정책을 변경하기 전에 잠재적인 보안 위험을 이해하고 있어야 합니다.

PowerShell의 실행 정책을 변경하려면 다음 단계를 따르세요:

  1. 관리자 권한으로 PowerShell 실행:
    • "Windows PowerShell"을 검색하고, "관리자 권한으로 실행"을 선택합니다.
  2. 실행 정책 변경: 관리자 권한의 PowerShell에서 다음 명령어를 실행합니다.이 명령어는 사용자의 현재 프로필에 대해 원격 서명된 스크립트의 실행을 허용합니다. 이렇게 하면 로컬에서 만든 스크립트는 실행할 수 있고, 인터넷에서 다운로드한 스크립트는 서명된 경우에만 실행할 수 있습니다.
  3. powershell
    Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
  4. 변경 확인: 정책이 변경되었는지 확인하려면 다음 명령어를 사용하세요.Scope가 CurrentUser인 항목이 RemoteSigned로 설정되었는지 확인합니다.
  5. powershell
    Get-ExecutionPolicy -List

[1] 마우스 이벤트 객체 사용 (클릭위치/드래그방식으로 박스 옮기기)

 

페이지영역: 전체 문서 사이즈

클라이언트영역: 내가 보고있는 화면에 해당하는 문서 사이즈

console.log("(x,y):"+e.x+","+e.y); 
console.log("client (x,y):"+e.clientX+","+e.clientY); // 보고있는 클라이언트 화면 기준
console.log("page (x,y):"+e.pageX+","+e.pageY); // 전체 페이지 기준
--- // 보통 위 세개는 값이 같게 나온다. 페이지를 벗어날땐 페이지만 변동이 있음
console.log("offset (x,y):"+e.offsetX+","+e.offsetY); // 컨테이너 기준 x,y

 

✔ position속성

static 기준 없음 (기본값)
relative 요소 자기 자신 기준으로 배치
absolute 부모(조상) 기준으로 배치
fixed 뷰 기준 배치
stickey 스크롤 영역 기준 배치
<section id="section10">
        <h1>연습문제 10 - 박스의 옵셋 영역 좌표 이용하기 </h1>
            <style>
                .container{
                    width: 800px;
                    height: 400px;
                    border:1px solid gray;
                    position:relative;  <!--  부모객체를 relative로 두면 이 내에서만 자식범위가 적용-->
                    overflow: hidden;
                }

                .box{
                    width:100px;
                    height: 100px;
                    background-color: blueviolet;
                    position:absolute;
                }
            </style>
            <div class="container">
                <div class="box"></div>
                <div class="box"></div>
                <div class="box"></div>

            </div>
            <div class="status"></div> <!-- 박스의 상태좌표 -->

    </section>
    <hr />

// 연습문제 10 - 박스의 옵셋 영역 좌표 이용하기
window.addEventListener("load", function(){
    var section = document.querySelector("#section10");

    var container = section.querySelector(".container");
    var status = section.querySelector(".status");

    var dragging = false; // 드래그(클릭+내려가기) 상태가 아닌 값
    var offset={x:0, y:0};
    var current = null; // 박스 세개중 이동할 대상
    var left = container.offsetLeft; //페이지에서 컨테이너의 위치값
    var top = container.offsetTop;

    document.onmousedown=function(e){ 
        //다큐먼트에서 이벤트 발생. 컨테이너에서 다큐먼트로 변경>컨테이너를 벗어나도 모션캡쳐가 가능하도록.
        if(e.target.classList.contains("box"))
            dragging =true;
            current=e.target;
            offset.x = e.offsetX;
            offset.y = e.offsetY;
    
    };

    document.onmousemove=function(e){ // 클릭이 아닌 마우스 무브로 적용
        //마우스무브는 마우스가 움직일때마다 함수가 실행되는 것
        if(!dragging) return; //드래그 상태가 아닐때 끝냄
        
        var x = e.pageX-offset.x -left; // 타겟(박스좌측상단)의 페이지위치-마우스로 찍은 위치-컨테이너위치
        var y = e.pageY-offset.y - top;

        current.style.left= x +"px";
        current.style.top= y +"px";

        status.innerText = "(x,y):("+x+","+y+")"; // 컨테이너 내 박스좌표

    };

    document.onmouseup=function(e){
        dragging=false; 
    };

});

 


마우스 이벤트 & 오프셋 사용은 통째로 예제와 함께 복습해야 할 것 같다

 

[1] 여러버튼을 가진 화면에서 이벤트 처리

: 아래와 같이 한 부모 아래에 각각의 버튼이 구현되어있을때, 각 버튼의 이벤트 처리

1-1) 레코드 선택 target.classList.contains

<tbody>
                <tr>
                    <td>1</td>
                    <td><a href="1">자바스크립트</a></td>
                    <td>2019-01-25</td>
                    <td>newlec</td>
                    <td>2</td> // tbody내에서 class="sel-button" 명이 a 와 b로 나누어진다 
                    <td><input type="button" class="sel-button a" value="선택" /></td>
                    <td><input type="button" class="edit-button" value="수정" /></td>
                    <td><input type="button" class="del-button" value="삭제" /></td>
                </tr>
                <tr>
                    <td>2</td>
                    <td><a href="2">유투브</a></td>
                    <td>2019-01-25</td>
                    <td>newlec</td>
                    <td>0</td>
                    <td><input type="button" class="sel-button b" value="선택" /></td>
                    <td><input type="button" class="edit-button" value="수정" /></td>
                    <td><input type="button" class="del-button" value="삭제" /></td>
                </tr>
    ----------------------------------
    
    window.addEventListener("load", function(){
    var section = document.querySelector("#section4");
    
    var tbody = section.querySelector(".notice-list tbody");
    
        tbody.onclick=function(e){
            var target = e.target; // e.target그대로 써도 되지만 하나의 변수로 치환해줌

            if(target.nodeName !="INPUT")
            return;

            if(target.classList.contains("sel-button")){ // class명이 서로 다르므로 sel-button 이 들어간 클래스에 모두 해당됨
                var tr = target.parentElement.parentElement;
                for(; tr.nodeName!="TR"; tr=tr.parentElement);
                    tr.style.background="yellow";
            */for( var tr = target.parentElement.parentElement;tr.nodeName!="TR"; tr=tr.parentElement);
            이렇게 한줄 표현했지만 tr이 지역변수라 밖에서 사용 불가하므로 초기값을 밖으로 빼줬다.
            (var tr = 타겟의 부모의 부모값을 찾아갔을때(초기) ; TR이 아닌 경우(조건); 하나 더 부모위로 올라간다 (증감))/*

            } 
            else if(target.classList.contains("edit-button")){

        
            }
            else if(target.classList.contains("del-button")){

            }
            
        };

});

👉 기존 방식대로 그냥 부모의 부모값을 지정해도 되지만, 중간에 다른 태그 (div나 span등)이 끼게 될 수 있으므로 for문을 이용해 <tr>을 찾아 적용

 

 

1-2) 엘리먼트의 기본 행위 막기 e.preventDefault();

: 버튼이 하이퍼링크일때 연동되는 디폴트 기능때문에 선택을 눌러도 지정한 색깔이 자꾸 초기화 됨

: 기본행위를 막아서 설정값이 지속되게 함

var tbody = section.querySelector(".notice-list tbody");
    
        tbody.onclick=function(e){
            e.preventDefault(); // 이벤트가 발생했을때 기본행위(하이퍼링크)를 갖고있다 해도 작동을 못하게 함

            var target = e.target;

            if(target.nodeName !="A")
            return;

            if(target.classList.contains("sel-button")){ // sel-button 이 들어간 클래스에 모두 해당됨
                var tr = target.parentElement.parentElement;
                for(; tr.nodeName!="TR"; tr=tr.parentElement);
                    tr.style.background="green";

            }

 

[2] 이벤트 트리거

: 실제 이벤트가 발생하지 않았는데 해당 이벤트를 발생시킬 수 있음.

: 컨트롤 중에 스타일이 적용되지 않는 컴포넌트 & 브라우저마다 다른 모양 > 기존 디스플레이방식을 안보이지만 기능적으론 작동하게. 예쁜 디자인은 클릭이벤트를 받아서 전달 (트리거역할)

👉 파일 선택 상자를 대신하는 <span /> 태그 사용

    <section id="section6">
        <h1>연습문제 6 - 이벤트 트리거 </h1>
        <style>
            .file-button{
                display:none; // 진짜 파일버튼은 안보이게
            }
            .file-trigger-button{
                background-color: aquamarine;
                border: 1px solid blue;
                border-radius:5px;

                padding: 5px 10px;
                color: #000;;
                cursor:pointer; // 커서가 올라가면 포인터로 변환됨
            }
            .file-trigger-button:hover{ // 커서가 올라가면 배경색이 바뀌게
                background-color: yellow;
            }

        </style>
        <input type="file" class="file-button">
        <span class="file-trigger-button">파일선택</span>

    </section>
    
    -----------------------------
    
    // 연습문제 6 - 이벤트 트리거
window.addEventListener("load", function(){
    var section = document.querySelector("#section6");

    var fileButton = section.querySelector(".file-button"); // 숨어있는 진짜 버튼
    var fileTriggerButton= section.querySelector(".file-trigger-button"); // 겉으로 보여지는 버튼이자 트리거

    fileTriggerButton.onclick=function(){
        var event = new MouseEvent("click",{ // 마우스 이벤트-클릭이 일어났을때
            'view': window, // 윈도우 뷰에서
            'bubbles': true, // 버블링이 일어나는가
            'cancelable': true, // 캔슬도 가능한가
        });
        fileButton.dispatchEvent(event);

        // var event = document.createEvent("MouseEvent");
        // event.initEvent("click", true, true);
    }
});

 

 

[1] 이벤트와 이벤트 객체

: 이벤트객체는 이벤트에스 발생한 부가정보를 제공한다

 

1-1) 이벤트 객체의 target 속성 (event.target)

: 이벤트가 발생한 객체는 event.target

window.addEventListener("load", function(){

    var section = document.querySelector("#section1");
    var imgs = section.querySelectorAll(".img"); // 전체 이미지를 얻어온다
    var currentImg = section.querySelector(".current-img");
    
    for(var i=0; i<imgs.length; i++) // 반복문 사용이 비효율적인 코드 > 버블링 사용해야함
    imgs[i].onclick=function(e){ // 매개로 event
        currentImg.src = e.target.src; // 현재 누가 클릭되었는지를 알려줌. 이미지이므로 src속성을 가질수있다
    };
});

 

 

1-2) 이벤트 버블링

: 이벤트 반복을 줄여서 작성하는 것

: 말단 태그에 일일히 이벤트를 거는게 아니라, 자식노드(들이 같은 구조임)에서 일어난 이벤트가 부모에게 전달이 되므로 부모에게만 이벤트를 걸어서 실행

: 이벤트가 바인딩되어있다면 실행되도록 함

버블링 처리시 사진 옆에 공란을 선택해도 실행되는 문제가 있음 > 조건절필요

 

window.addEventListener("load", function(){

    var section = document.querySelector("#section2");
    var imgList = section.querySelector(".img-list"); // selectorAll (배열)이 아니라 클래스로 리스트를 얻어옴
    var currentImg = section.querySelector(".current-img");
    
    imgList.onclick = function(e){ // 부모에게 이벤트를 달아도 똑같이 작동이 됨
        if(e.target.nodeName != "IMG") // nodeName으로 올바른 클릭인지 검증함
        return;
        
        currentImg.src = e.target.src;
    };

* 이때 NodeName은 콘솔로 출력해서 확인해봄

 

 

1-3) 이벤트 버블링 멈추기 stop.propagation()

: 같은 부모를 공유하지만 다른 행위를 해야 하는 경우

버블링때문에 addButton을 눌렀음에도 imgList.onclick 이벤트가 함께 실행되는 문제가 있음 (같은부모공유)

<div class="img-list">
            <img class="img" src="imgs/img1.jpg" style="height: 50px;" />
            <img class="img" src="imgs/img2.jpg" style="height: 50px;" />
            <img class="img" src="imgs/img3.jpg" style="height: 50px;" />
            <input class="add-button" type="button" value="추가" />
        </div>
        
-------------------
imgList.onclick = function(e){
        if(e.target.nodeName != "IMG")
        return;

        currentImg.src = e.target.src;
    };

    addButton.onclick = function(e){
        e.stopPropagation(); // 함수 내에서 위치는 상관없음
        var img = document.createElement("IMG");
        img.src="imgs/img2.jpg";
        currentImg.insertAdjacentElement("afterend",img);
    };

👉 stopPropagation()덕분에 같은 부모를 공유해도 버블링 없이 단일행위만 일어난다

+ Recent posts