React.js 에서 useTransitionuseDeferredValue 를 몰랐을 때에는 그냥 useEffect 혹은 useMemo 를 사용하여 state를 관리했었는데, 공식홈페이지를 보다가 조금 더 알게된 내용을 공부하고 포스팅한다.

Output

0. useTransition 이란?

- 일반적인 상태 업데이트를 하는데 유용한 React Hook.

useFormStatus 와 유사하게 사용할 수 있다.
( useTransition: 일반적인 상태 업데이트, useFormStatus: Form 제출 상태를 관리)

const {isPending, startTransition} = useTransition()

이런 선언으로 간단하게 시작할 수 있다.

 

일반적인 textInput 값을 사용해보자

 

1. startTransition 으로 query의 값을 변경할 때 setQuery 호출

const [query, setQuery] = useState("");
const [isPending, startTransition] = useTransition();

const handleChange = (e) => {
    startTransition(() => {
        setQuery(e.target.value);
    });
};

//컴포넌트 내용 
return (
	<input
        type="text"
        onChange={handleChange}
    />	
)

1) useTransition은 상태 업데이트를 긴급하지 않은 작업으로 처리한다.
2) isPending 상태를 통해 전환 중임을 사용자에게 표시할 수 있다.
3) React는 더 중요한 업데이트(예: 사용자 입력)를 먼저 처리한 후 이 업데이트를 실행한다.

> Lazy Loading 과 같이 지연 연산으로 처리 한다.
> 만약 onChange 가 한 번 더 호출되면, 이전 연산하던 것은 버리고 새로 연산을 시작 한다.

 

2. defferredValue 선언

const defferredValue = useDeferredValue(query);

위에서 setQuery(newValue) 를 실행할때의 조건을 설정한다고 생각하면 된다.

1번과 마찬가지로 useDeferredValue는 값의 변경을 지연시켜 UI 응답성을 유지합니다.
사용자 입력에 즉시 반응하면서도 무거운 렌더링 작업은 나중에 처리할 수 있게 해줍니다.

 

3. 실제 Filter 처리

const list = Array.from({ length: 1000 }, (_, i) => `Item ${i + 1}`);
const filteredList = useMemo(() => {
    if (defferredValue === "") return list; //공백일때는 초기화

    return list.filter((item) =>
        item.toLowerCase().includes(defferredValue.toLowerCase())
    );
}, [defferredValue]);

실제로 사용할때는 query 값을 사용하는 것이 아니라, deferredValue 값을 사용해야한다. (지연처리를 위함)

추가적으로 useMemo 를 사용하였는데, 변경되지 않은 부분에 대해 불필요한 Re-Rendering 방지합니다.

 

결국, 궁극적 목표는 UI의 최적화이다. (버벅거림을 없애기)

 

전체 코드

const list = Array.from({ length: 1000 }, (_, i) => `Item ${i + 1}`);
const TransitionExample: React.FC = () => {
    const [query, setQuery] = useState("");
    const [isPending, startTransition] = useTransition();

    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        startTransition(() => {
            setQuery(e.target.value);
        });
    };

    const defferredValue = useDeferredValue(query);
    
    const filteredList = useMemo(() => {
        if (defferredValue === "") return list; //공백일때는 초기화

        return list.filter((item) =>
            item.toLowerCase().includes(defferredValue.toLowerCase())
        );
    }, [defferredValue]);
    
    return (
   	<>
        <input
            className="w-full h-10 px-4 py-3 mr-2 border border-gray-300 rounded-full focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent"
            type="text"
            onChange={handleChange}
            placeholder="검색어를 입력하세요..."
        /> 
   
   		<ul className="divide-y divide-gray-200">
            {filteredList.map((item) => (
                <li
                    key={item}
                    className="py-2 px-3 hover:bg-gray-100 transition-colors duration-150 ease-in-out"
                >
                    {item}
                </li>
            ))}
        </ul>
	</>
  )

 

TypeScript 로 연습했기 때문에 위와 같이 React.FC 같은 Type 선언이 추가되어져 있다.

반응형

[React.js] Stream 데이터 처리하기 - 1

지난 포스팅에 Stream Data 관련해서 글을 작성하였다.

이번 포스팅에서는 해당 내용을 실제로 적용시켜보는 작업을 한다.

 

 

How To Decode?

그런데 stream 데이터를 가져온다고 바로 쓸 수 있는 게 아니라, 변환 및 화면에서 사용하는 방법에 대해 포스팅해보려고 한다.

//api.js
export const retrieveStreamingData = async ({ inputValue, selectedItems }) => {
    try {
        const requestBody = { data: 'some data...' }

        // API 요청
        const response = await fetch(`${Config.baseURL}/api/v-1/retrieveStreamingData`, {
            method: 'POST',
            headers: Config.headers,
            body: JSON.stringify(requestBody),
        })

        useRetrieveStore.getState().setText('') //초기화
        useRetrieveStore.getState().setTitle('') //초기화

        const reader = response.body.getReader()
        const decoder = new TextDecoder()

        while (true) {
            const { done, value } = await reader.read()

            const decodedChunk = decoder.decode(value, { stream: true })
            if (decodedChunk) {
                try {
                    const beforeData = removeLeadingData(decodeUnicodeString(decodedChunk))
                    const afterData = remmoveBackslash(
                        removeFirstAndLastQuotes(decodeUnicodeString(beforeData)),
                    )
                    const cleanData = parseNestedJSON(afterData)

                    if (
                        cleanData?.msg === 'process_generating' &&
                        cleanData?.output?.data[0][0] &&
                        cleanData?.output?.data[0][0].length >= 1
                    ) {
                        if (cleanData?.output?.data[0][0][0] === 'append')
                            //store에 저장
                            useRetrieveStore
                                .getState()
                                .appendText(
                                    cleanData.output.data[0][0][2].replace(/\\n/g, '\n'),
                                )
                    } else if (cleanData?.result || cleanData?.thread_id) {
                        // Streaming 종료, 
                        // title이 마지막에 나오기 때문에 마지막에 title 값을 할당
                        useRetrieveStore
                            .getState()
                            .setTitle(cleanData.result?.created_title ?? '')
                    }
                } catch (error) {
                    console.error('Error:', error)
                }
            }

            //done이 true이면 루프 종료
            if (done) break
        }
    } catch (error) {
        console.error('🚨 [API 요청 중 오류 발생]: ', error.message)
    }
}
//utils.js
// 백슬래시를 제거하되 줄바꿈은 유지
const remmoveBackslash = (str) => str.replace(/\\(?![n\r])/g, '').replace(/""/g, '"')

// 줄바꿈 관련 처리 제거
const removeFirstAndLastQuotes = (str) =>
    str
        .replace(/"{/g, '{')
        .replace(/}"/g, '}')
        .replace(/\\/g, '\\\\')
        .replace(/"/g, '\\"')
        .replace(/\t/g, '\\t')

//유니코드 문자열 디코딩
const decodeUnicodeString = (str) =>
    str.replace(/u([0-9a-fA-F]{4})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))

//중첩 JSON 파싱
const parseNestedJSON = (jsonString) => {
    let result = jsonString
    while (typeof result === 'string') {
        try {
            result = JSON.parse(result)
        } catch (e) {
            break
        }
    }
    if (result?.content && typeof result?.content === 'string') {
        result.content = JSON.parse(result.content)
    }
    return result
}

//"data:" 제거
const removeLeadingData = (str) => {
    if (str.startsWith(',data:')) {
        return str.slice(str.indexOf(':') + 1).trim()
    }
    if (str.startsWith('data:')) {
        return str.slice(str.indexOf(':') + 1).trim()
    }
    return str
}

조금 길지만 이런 식으로 사용하였다.

1. api.js

  1. /api/v-1/retrieveStreamingData 로 api 요청.
  2. Stream 데이터로 연속적으로 데이터를 내려주게 됨.
  3. 내려온 데이터들을 지속적으로 store에 저장.
    (while 문을 사용하여 단어로 끊어져서 내려오는 데이터들을 useRetrieveStore  set)
  4. React.js 4번 화면에서 사용 하도록 한다

 

2. utils.js - 데이터 전처리

  • 데이터의 포맷이 정상적으로 내려오지 않아서 1.5일 동안 개고생했다.
    이 부분인데,
  1. removeLeadingData(String) : 데이터의 가장 앞에 "data: " 로 내려오는 경우가 있어 해당 로직을 추가하였다. (",data:" 도 있었다..)
  2. decodeUnicodeString(String) : Unicode 로 된 데이터를 변환하는 작업.
  3. removeFirstAndLastQuotes(String) : Escape 문자열을 처리하는 방법. (진짜 할 때 속터져)
str
.replace(/"{/g, '{')    // 문자열 시작의 '"{' 를 '{' 로 변경 (JSON 객체 시작 부분 정리)
.replace(/}"/g, '}')    // 문자열 끝의 '}"' 를 '}' 로 변경 (JSON 객체 끝 부분 정리)
.replace(/\\/g, '\\\\') // 단일 백슬래시를 이중 백슬래시로 변경 (이스케이프 처리)
.replace(/"/g, '\\"')   // 따옴표를 이스케이프 처리된 따옴표로 변경
.replace(/\t/g, '\\t')  // 탭 문자를 이스케이프 처리된 탭으로 변경

4. parseNestedJSON(String|Object) : 결국 받아온 값들을 사용하려면 JSON.parse(String) 이 필요한데, 재귀함수처럼 사용하였다 (사실은 while 반복문)
한 번 파싱했을 때 한개의 요소만 JSON 파싱이 되는 경우가 있어서 만약 String 형태면 다시 JSON.parse() 해주는 작업을 추가하였다.

 

3. Store - zustand

Store는 React.js 에서 유명한 zustand를 사용하였다.

import { create } from 'zustand'

const useRetrieveStore = create((set) => ({
    retrievedText: '', // 텍스트
    retrievedTitle: '', // 제목

    setRetrievedText: (newText) =>
        set(() => ({
            retrievedText: newText,
        })),

    // title setter
    setRetrievedTitle: (newTitle) =>
        set(() => ({
            retrievedTitle: newTitle,
        })),

    // text appender (setter 변형) - 텍스트를 추가한다.
    appendText: (additionalContent) =>
        set((state) => ({
            retrievedText: state.retrievedText + additionalContent,
        })),
}))

export default useRetrieveStore

4. React.js 화면

Chatting 스타일로 만들었기 때문에 Array로 데이터 구조를 만들었다.

    const retrievedText = useRetrieveStore(
        (state) => state.retrievedText,
    )
    const retrievedTitle = useRetrieveStore(
        (state) => state.retrievedTitle,
    )

    // AI 메시지 추가 - store값을 가져와 실시간으로 반영하는것으로 처리.
    useEffect(() => {
        if (messageList.length === 0 || retrievedText === '') return

        const lastMessage = messageList[messageList.length - 1]

        if (lastMessage.type !== 'ai') {
            setMessageList((prev) => [...prev, { id: Date.now(), text: '', type: 'ai' }])
        } else if (lastMessage.type === 'ai') {
            setMessageList((prev) =>
                prev.map((message, index) =>
                    index === prev.length - 1
                        ? { ...message, text: retrievedText }
                        : message,
                ),
            )
        }
    }, [retrievedText])

    //title 수정 되면 기존의 값에 할당
    useEffect(() => {
        if (messageList.length === 0 || retrievedTitle === '') return
        
        const lastMessage = messageList[messageList.length - 1]

        if (lastMessage.type === 'ai') {
            setMessageList((prev) =>
                prev.map((message, index) =>
                    index === prev.length - 1
                        ? { ...message, title: retrievedTitle }
                        : message,
                ),
            )
        }
    }, [retrievedTitle])

각각 useEffect 를 사용하여 해당 값이 업데이트 되었을 때 리렌더링 되도록 유도하였다.

++ 약간 Vue.js의 watch 와 유사하다고 볼 수 있겠지만,
React.js 의 useEffect가 전반적인 LifeCycle에 관여할 수 있기 때문에 더 큰 범위라고 할 수 있다.

 

반응형

안녕하세요 상훈입니다.

Javascript 에서 window.location 을 이용하여 개발/테스트/운영을 구분하기 위해서 쓸 수 있는 간단한 스크립트를 소개합니다.

물론 제약사항이 존재합니다.

예를 들어 devURL, tstURL, URL 이런식으로 접두 주소를 가지고 있어야합니다.

바로 보시죠

 

window.location 이용하기
//개발모드 확인하기
window.location.hostname.startsWith('dev')

//테스트모드 확인하기
window.location.hostname.startsWith('tst')

이런식으로 사용할 수 있네요. return 되는 결과는  boolean 입니다 ( true / false )

 

window.location.hostname.startsWith() 사용하기
// hostname 가져오기
const hostName = window.location.hostname

// hostName의 값에 따라 로직 분기하기
if (hostName.startsWith('dev')) {
  return 'this is dev'
} else if (hostName.startsWith('tst')) {
  return 'this is tst'
} else {
  return 'this is prod or else'
}

이런식으로 사용이 가능합니다.

여지껏 맨날 window.location.indexOf 등으로 찾았었는데 이런 좋은게 있다니,, 감동이네요.

 

returns Boolean

 

감사합니다.

반응형

안녕하세요 상훈입니다!

구글 애드센스가 끊긴지도.... 어언 2주가 되어가네요! 하하 빌어먹게도 멋진 녀석 같으니라고^^7

도움이 되셨다면 광고 한번 클릭해주세요. 블로그 운영에 큰 힘이 됩니다. 감사합니다.

 

자바스크립트로 데이터를 받아왔는데, 숫자만 출력하고 싶습니다. 어떻게 해야할까요?

let str = "안녕하세요1239입니다.";

str = replaceAll(/[^0-9]/g, "");

로 간단하게 처리가 가능합니다.

 

사용 방법은 아래와 같습니다.

str.replaceAll(없애고싶은내용, 바꿀내용);

 

사용 예시를 작성해보겠습니다!

 

숫자의 데이터가 필요한 상황인데, 애석하게도 숫자만 온게 아니라 문자가 포함된 문자열로 출력되었다는 가정입니다.

function changeLetter (str, successCallback) {
	str = str.replaceAll(/[^0-9]/g, "");
    successCallback(str); //str을 반환
}

changeLetter("안녕하세요12395", function (ret) { //성공 콜백함수와 데이터
	
    //ret이 변경된 새로운 str이 되는것이죠.
    $("#여기에작성해주세요").text(ret); //12395
    
});

결과적으로, id: 여기에작성해주세요 라는 아이디를 가진 태그의 텍스트에 숫자만 출력되는 내용이 될겁니다.

콘솔창에 작성해본 예시

이렇게 함수로 쓰면 언제든지 함수만 호출해서 꺼내올 수 있게 작성할 수 있습니다!

 

다만 이것의 문제는 문자열이라는 것에 있죠.

 

조금 더 나아가서 저 문자열을 1)숫자로 변경하고, 2) 뒤에 원을 붙여서 출력하도록 하겠습니다.

function changeLetter (str, successCallback) {
	str = str.replaceAll(/[^0-9]/g, "");
    successCallback(str);
}

changeLetter("안녕하세요12395", function (ret) { //성공 콜백함수와 데이터
	
    //ret이 변경된 새로운 str이 되는것이죠.
    ret = parseInt(ret);
	console.log(ret + "원");
});

typeof 도 함께 출력하여 숫자로 변경되었음을 확인

이렇게  parseInt 혹은 Number... 등을 이용해서 숫자로 변경하고,

출력할 때 "원" 문자까지 합쳐서 출력하는 방식을 알아보았습니다. 

 

감사합니다.

 

도움이 되셨다면 광고 한번 클릭해주세요. 블로그 운영에 큰 힘이 됩니다. 감사합니다.

반응형

도움이 되셨다면 광고 한번 클릭해주세요. 블로그 운영에 큰 힘이 됩니다. 감사합니다.

안녕하세요 상훈입니다.

json 객체(object) 형태문자열 json 객체화 하려면 간단하게 

JSON.parse(str);

하면된다. 

 

추가적으로 배열(Array) 또한 가능하다. (몰랐다)

 

■ 배경설명

현재 프로젝트가 Web → Native(React Native) → Web 이기 때문에 내 입맛대로 데이터의 형식을 설정할 수 없기 때문에, 배열로 변환하는 방법 또한 알게되었다.

 

■ 결론

일단 결과부터 말하자면, 방법은 다음과 같다.

// 배열 형태의 문자열
// (JSON객체형태가 곧바로 string으로 변환되어나온 형태)

let arryStr = "[\"안녕하세요.\",\"안녕히가세요.\"]";

// 문자열 → 배열
arryStr = JSON.parse(arryStr);

 

console창에서 확인

배열(Array) 인 것을 알 수 있다.

 

그러나, 

typeof(arryStr); //Object

라고 출력된다. (으잉?)

 

위처럼 사용해도 배열에 대한 모든 기능들은 동작하니 문제는 없을 것이다.

 

하지만 기분이 찜찜하고 이상하니 조금 더 파고들어가보았다.

 

■ 부연설명

위에서 얻은 arryStr 을 가지고 새로운 배열에 반복문을 통해 push 해서 type을 해보겠다.

let arryStr = "[\"안녕하세요.\",\"안녕히가세요.\"]";
arryStr = JSON.parse(arryStr);

// ---------------------------------------

let result = []; //새로운 빈 배열 선언

arryStr.forEach( (item, idx) => {  //반복문을 통해
    result.push(item);             //빈배열에 값을 집어넣기
});

console.log(result);               //['안녕하세요.', '안녕히가세요.']
console.log(typeof(result));       //object

console.log

 

????

result 라는 녀석은 배열로 선언해서 배열로 끝났고, 각각의 인덱스도 가지고 있는데, 
type을 확인해보니 객체(Object)로 출력되어져 있다.

 

비교대상이 배열인지 여부를 확인하는 메서드를 사용해서 판독해보자.

> Array.isArray() :: 비교대상이 배열이면 true, 아니면 false를 반환하는 메서드.

// 비교 대상 ( ) 안에 있는 대상이 배열이면 true, 아니면 false를 출력한다.

// Array.isArray() 를 통해서 한 번 더 배열인지 여부를 판독
console.log(Array.isArray(arryStr)); //true
console.log(Array.isArray(result2)); //true

 

왜냐하면, 자바스크립트(JavaScript)에서의 배열(Array)는 배열이 아니라 객체(Object)이기 때문이다. 
자세한 내용은 아래의 링크를 참고해주시길 바랍니다.

https://poiemaweb.com/js-array-is-not-arrray

 

Array | PoiemaWeb

자바스크립트 배열은 배열이 아니다.

poiemaweb.com

 

JSON.parse() 하나로 이렇게 글이 길어질 줄 몰랐지만, 길어졌다.

이상입니다.

 

참고

 

Array.isArray() - JavaScript | MDN

Array.isArray() 메서드는 인자가 Array인지 판별합니다.

developer.mozilla.org

 

미리보기를 해보니 역슬래쉬가 \로 출력되네요.

작성할때는 역슬래쉬 정상적으로 되었는데 말이죠. 유의해주시길 바랍니다!

이미지 참고 ㅎㅎ

 

도움이 되셨다면 광고 한번 클릭해주세요. 블로그 운영에 큰 힘이 됩니다. 감사합니다.

반응형

안녕하세요 상훈입니다. 

jQuery - Handlebars.js 를 사용하는데, 다 제대로 한 것 같은데, 아래와 같은 에러를 내뱉습니다.

요놈의 핸들바는 불친절하게 에러를 좀 내뱉는 것 같습니다.

Handlebars Pass a string or Handlebars AST to Handlebars compile ...

undefined 오류도 포함합니다.

 

확인사항

1. 실제 값이 undefinded 인지.
2. 컴파일 하려는 id의 값과 html-script-handlebar 의 id 를 일치시켰는지 여부
3. 해당 script를 포함하고 싶은 태그가 렌더링 되고 난 이후에 작성이 되었는지 여부

저는 3번에 해당하는 것이었습니다.

<div id="handlebarExample">
	<script id="handlebarExampleTarget" handlebar...></script>
</div>

이렇게 작성을 했었는데, 인식이 안되더라구요.

그래서 해당 <div>의 속을 비워주고, script를 아래로 내려주었더니 해결되었습니다.

<div id="handlebarExample"></div>
...
<script id="handlebarExampleTarget" handlebar...></script>

 

아무래도 완전히 컴파일이 되기 전에 script가 동작하게 되어 일어난 현상이 아닐까 싶습니다. 

2시간동안 헤매었는데, 논리상 오류가 없다보니 위치를 고려하지 못했었습니다.

 

 

 

Handlebars

 

handlebarsjs.com

 

 

Handlebars Pass a string or Handlebars AST to Handlebars compile

I know its been asked many times, I have looked at the answers and not sure where I am going wrong. I have looked at the docs on Handlebarsjs and followed a tutorial and both times I am getting the

stackoverflow.com

 

이상입니다.

 

반응형

안녕하세요 상훈입니다.

jQuery - Swiper 를 사용하는데, activeIndex가 뭐지?  slideChange는 어떻게 사용하는것이지? 라는 의문이 들어 찾아보고 메모합니다.

일단 기본적으로 적용시킨것들은 이미 하셨다고 생각하고 넘어가도록 하겠습니다.

 

1. activeIndex 

var swiperExample = new Swiper(".swiper-container", {
	
    ...
     
});

이런 swiper가 있을 때, activeIndex는 간단하게 얻을 수 있습니다.

const swiperIndex = swiperExample.activeIndex

 

이렇게하면 현재 활성화되어있는 swiper의 번호를 얻을 수 있습니다.

 

2.slideChange

슬라이드가 변할 때 특정 동작을 실행합니다.

var swiperIndex;

var swiperExample = new Swiper(".swiper-container", {
	
    ...
     
    on: {
    	slideChnage: function () {
        	alert(this.realIndex);
        }
    }
});

slideChange 가 동작할 때마다 alert이 출력될 것 입니다.

 

이상입니다.

 

 

Swiper - The Most Modern Mobile Touch Slider

Swiper is the most modern free mobile touch slider with hardware accelerated transitions and amazing native behavior.

swiperjs.com

 

반응형

안녕하세요 상훈입니다. 

 

제이쿼리의 $.extend를 처음 보게 된다면 당황하셨을 수도 있습니다. 하지만 간단한 내용이라는 것을 알아주시기 바랍니다.

객체 A, B가 있다면,
$.extend(A, B) 는 A뒤에 B를 합치는 활동이라고 볼 수 있습니다.

여러개의 객체를 extend 내부에 넣을 수 있습니다.

 

const A = {age: 12, weight: 52} 
const B = {name: '상훈', height: 200}

const C = $.extend(A,B) // {age: 12, weight: 52, name: '상훈', height: 200}

 

 

만약 중복되는 key값을 가지고 있다면, "뒤"에서 덮어씌워집니다.

const A = {age: 12, weight: 52} 
const B = {name: '상훈', height: 200, age: 30}

const C = $.extend(A,B) // {age: 30, weight: 52, name: '상훈', height: 200}

이렇게 age가 뒤에 넣은 객체 B의 값으로 변한 것을 확인할 수 있습니다.

 

이상입니다.

 

출처: 블로그 - 클릭시 이동

 

 

반응형

+ Recent posts