요즘 Cursor AI 를 많이 이용 하고 있는데 여기서 몰랐던 코드를 구현해주는 상황때문에 포스팅하게 되었다.

 

기본적인 a 태그 + _blank 일 때

<a href ="" target="_blank">A태그입니다.</a>

 

이것까지는 아주 간단하고 당연한 내용인데,

 

<a
    href={work.link}
    target="_blank"
    rel="noopener noreferrer"
>a태그입니다</a>

추가 된 내용이 바로 rel 속성이다.

 

Rel ?? 보안적 측면에서 필요하다.

  • noopener: 새 탭/창에서 열리는 페이지가 window.opener 속성을 통해 원래 페이지에 접근하는 것을 방지합니다.
    이것은 탭 내빙(tab nabbing)이라는 보안 취약점을 막아줍니다.
    탭 내빙은 새 탭에서 열린 페이지가 원래 페이지의 location을 변경하여 피싱 공격을 시도할 수 있는 방법입니다.

  • noreferrer: 새 페이지로 이동할 때 HTTP 리퍼러(Referer) 헤더를 전송하지 않도록 합니다.
    이는 방문자의 개인정보 보호에 도움이 되며, 새 페이지가 어디에서 방문자가 왔는지 알 수 없게 합니다.

 

 

 

 

 
 
이 속성들은 외부 링크를 사용할 때 보안개인정보 보호를 강화하기 위한 웹 개발 모범 사례입니다.
특히 target="_blank" 를 사용할 때는 항상 rel="noopener noreferrer"를 함께 사용하는 것이 좋습니다.

 

반응형

2025.02.24 - [FrontEnd/React.js] - [React.js] Stream 데이터 fetch로 처리하기 - 2

 

[React.js] Stream 데이터 fetch로 처리하기 - 2

[React.js] Stream 데이터 처리하기 - 1지난 포스팅에 Stream Data 관련해서 글을 작성하였다. 이번 포스팅에서는 해당 내용을 실제로 적용시켜보는 작업을 한다.  How To Decode?그런데 stream 데이터를 가

code-hoon.tistory.com

지난 포스팅에서 Streaming 을 통해서 데이터를 노출하는 것까지 예시를 통해서 완료하였다.

이번에는 ai text 챗. Chatgpt 처럼 중간에 "정지"할 수 있는 기능을 추가하려고 한다.

 

1. AbortController

// 새로운 AbortController 인스턴스 생성
const abortController = new AbortController()

바로 abortController 기능을 사용하는건데, 해당 기능은 node15 이상부터 사용할 수 있으니 저버전의 서버를 사용하고 계신다면 주의 바랍니다. (그 이하를 쓰는거면 얼마나 오래된겁니까..)

이렇게 abrotController 를 선언하였다면, 

// API 요청에 signal 추가
const response = await fetch(`/api/streaming`, {
    method: 'POST',
    headers: Config.headers,
    body: JSON.stringify(requestBody),
    signal: abortController.signal, // AbortController의 signal 추가
})

signal 을 추가하여 처리할 수 있다.

 


결과 맛보기

zustand(store) 을 이용하여 분리되어있는 api 와 화면을 연결하였고,

실제로 사용한 모습은 다음과 같다.

//api.js
const abortController = new AbortController()

const response = await fetch(`api/`, {
    method: 'POST',
    headers: Config.headers,
    body: JSON.stringify(requestBody),
    signal: useStore.getState().abortController.signal,
})

 

React.js 화면

// 검색종료버튼
const handleStop = useCallback(() => {
    const store = useStore.getState()
    store.abortController.abort()
    store.setIsStopped(true)
}, [])

return (
    <button onClick={handleStop}>
        <FaRegStopCircle className='text-lg' /> {/* 정지아이콘 */}
    </button>
)

 

이제 실제로 동작하는 모습은 "개발자도구 -  네트워크 - REST API 명 - 응답"탭에서 볼 수 있는데,

Streaming 응답 데이터 모습

이런식으로 쭉 하단으로 나열되다가 중단을 시키면

데이터가 생성되다가 끊긴 모습

이렇게 결과가 나온다.

그때 화면에서는 아래와 같이 플로팅 버튼이 노출되도록 설정했고, "재생성하기" 버튼을 누르면 기존에 입력했던(송신했던) 정보를 가지고 python에서는 해당 내용을 가지고 다시 응답을 생성하도록 선언 해놓았다.

재생성하기 버튼

 

중요한건 Streaming 데이터를 REST API 를 통해서 호출하고,
이를 받아서 처리하는 과정에 있으며, 중간에 정지(abort) 하는 기능까지 만들었다는 것이다.

 

반응형

[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에 관여할 수 있기 때문에 더 큰 범위라고 할 수 있다.

 

반응형
streaming

Stream 데이터란?

우리가 흔히 말하는 유튜버들의 스트리밍이라는 단어가 파생된 근본이다.
Stream Data 는 연속적으로 생성되고 전송되는 데이터이다.
그래서 실시간, 확장성, 대량으로 등 데이터를 다루기 편한 부분이다

뭔가 WebSocket이랑 비슷한 느낌?

React.js 에서 RestAPI로 써보기

const StreamingComponent = () => {
    const [messages, setMessages] = useState([]);

    useEffect(() => {
        const fetchData = async () => {
            const response = await fetch('http://localhost:5000/stream');
            const reader = response.body.getReader();
            const decoder = new TextDecoder();

            let receivedData = [];

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

                const chunk = decoder.decode(value, { stream: true });
                const lines = chunk.split('\n').filter(line => line.startsWith('data: '));

                for (const line of lines) {
                    const jsonData = JSON.parse(line.replace('data: ', ''));
                    receivedData.push(jsonData);
                    setMessages([...receivedData]); // UI 업데이트
                }
            }
        };

        fetchData();
    }, []);

    return (
        <div>
            <ul>
                {messages.map((msg, index) => (
                    <li key={index}>{index}: {msg.message}</li>
                ))}
            </ul>
        </div>
    );
};

await fetch(url) 하는 것은 일반 RestAPI 와 동일하다.

response.body.getReader() 로 해당 데이터를 가져올 수 있고,
decoder.decode(value, { stream: true }) 디코딩을 한 번 해야한다.

Done

그리고 마지막으로 데이터가 끝나면 done 을 호출하게 된다.
추가적으로 done 에 대한 로직을 구현할 수도 있다.

if (done) {
	alert('Streaming 이 종료되었습니다!')
}

Decoding 을 해야하는 이유

위의 소스를 보면, decoder.decode(value) 를 하고 있는데, 디코딩을 해야하는 이유는 다음과 같다.

  1. 데이터 압축 해제: 스트리밍 과정에서 데이터는 효율적인 전송을 위해 압축되고 인코딩됩니다. 수신 장치에서는 이 데이터를 원래의 형태로 복원하기 위해 디코딩이 필요합니다
  2. 호환성 확보: 인코딩된 데이터는 다양한 장치와 플랫폼에서 재생할 수 있도록 표준화된 형식으로 변환됩니다. 디코딩은 이 표준화된 형식을 각 장치에서 재생 가능한 형태로 변환하는 과정입니다
  3. 실시간 재생: 스트리밍 데이터는 작은 세그먼트로 나누어져 전송됩니다. 각 세그먼트를 수신할 때마다 디코딩하여 즉시 재생할 수 있게 합니다
  4. 품질 최적화: 디코딩 과정에서 네트워크 상태나 장치 성능에 따라 적절한 품질의 스트림을 선택하여 재생할 수 있습니다
  5. 다양한 형식 지원: 트랜스코딩(인코딩과 디코딩의 조합)을 통해 다양한 비디오 형식을 지원하고, 각 사용자의 환경에 맞는 최적의 형식으로 변환할 수 있습니다

WebSocket과 Streaming 의 차이점

왠지 비슷하다고 느껴졌는데, 그 이유가 여기 있었다.
단지 사용처가 조금 다를 뿐(텍스트+양방향, 동영상+단방향) 이었다.

 

반응형

 

아래 명령어를 통하여 Next.js 프로젝트를 생성하려고하는데,

npx create-next-app@latest custom-project-name

 

아래와 같은 에러를 직면하였다.

I am ERROR

 

이유인즉슨, Node Version 문제였다.

현재 사용하고 있던 노드 버전은 14.23.1 버전으로,
@latest 버전의 Next 를 사용하기에는 버전이 지원되지 않았기에 발생하였다.

그래서 어쩔 수 없이 nvm 으로 노드를 새로 다운로드 받아 버전 컨트롤을 수행하였다.

 

 

 에러 해결 완료

반응형

 

 

Jenkins Build & Deploy shell

Jenkins 를 통해 FrontEndBackEnd의 CI/CD 를 구축 중에 있었다.

이런식으로 FrontEnd 소스를 빌드&배포하는 작업을 처리해놓았고, 실제로 수동 빌드가 동작하는 것까지 확인을 하였다.

branch를 main으로 잡아놓고, 해당 브랜치가 업데이트 되었을 때 배포하도록 설정해놨다.
그런데 아무리 기다려봐도 동작이 없었다.

그래서 원인이 뭔가를 확인해봤더니 Trigger 가 문제였다.

Trigger

Poll SCM

트리거를 특별히 지정해놓지 않았더니 수동 배포 동작을 눌러주기까지 계속 기다리고 있던 것.
그래서 Trigger(빌드유발) > Poll SCM 을 추가해주었다.

다들 ChatGPT를 확인해서 알겠지만, 위 내용은
평일 오전9시-오후6시에 5분마다 변경사항을 체크하고 배포한다는 내용이다.

그랬더니 정상적으로 build & deploy 가 되는것을 확인하였다.

 

반응형

 

아래와 같이 클래스를 만들었을 경우

1.  Override, 다형성

open class Country ( var fullName: String, var capital: String, var language: String)  {

    fun printFullName () {
        println("fullName: $fullName")
    }
    fun printCapital () {
        println("capital: $capital")
    }
    fun printLanguage () {
        println("language: $language")
    }
    open fun singNationalAnthem () {
        println("singNationalAnthem")
    }
}

class Korea (fullName: String, capital: String, language: String) : Country( fullName, capital, language) {
    override fun singNationalAnthem () {
        super.singNationalAnthem()
        println("sing Korea")
    }
}


class USA ( fullName: String, capital: String, language: String) : Country( fullName, capital, language) {
    override fun singNationalAnthem () {
        super.singNationalAnthem()
        println("sing USA")
    }
}

 

  1. Country 라는 클래스를 Korea, USA 라는 클래스에서 상속.
  2. 자식 클래스에서는 super.METHOD 를 통해 부모 클래스의 메서드를 호출 할 수 있다.
  3. +커스터마이징이 가능하다 (== 다형성)

 

fun main (args: Array<String>) {
    val korea = Korea("대한민국", "서울", "한국어")
    korea.singNationalAnthem()
    val usa = USA("미국", "워싱턴", "영어")
    usa.singNationalAnthem()
}

/** output
singNationalAnthem
sing Korea
singNationalAnthem
sing USA
*/

이렇게 출력이 된다.

Java의 문법과는 또 달라서 [ Java(public) / Kotlin(open) ] 신기하다.
뭔가 public 공공의 라는 뜻보다는 open 이 더 직관적이기도 하고

 


 

2. Abstract. 추상매서드

다음으로 추상메서드에 대해 메모한다.

//추상클래스
abstract class Game {
    //일반메서드
    fun startGame () {
        println("Game Start")
    }

    //추상메서드
    abstract fun printGameName ()
}

class OverWatch: Game() {
    //추상메서드는 하위 클래스에서 반드시 구현해야함
    override fun printGameName() {
        println("This game is OverWatch.")
    }
}

Game이라는 클래스를 만들었다.
Game > 메서드로 printGameName 이라는 메서드를 만들어놓았다.
: 추상메서드는 반드시!!! 오버라이딩 한 메서드에서 만들어야한다.

fun main (args: Array<String>) {
    val overwatch = OverWatch() //새로운 인스턴스 생성

    overwatch.startGame()
    overwatch.printGameName()
}

새로운 인스턴스 overWatch 를 생성.
해당 클래스에 선언된 메서드를 실행하였다.
printGameName 메서드는 추상메서드로 선언(abstract)

예전에는 이게 무슨소린지 전혀 이해를 못했었는데, 다시 보니 또 바로 이해되는게 신기하다.

반응형

코틀린을 사용하는데에 차이점이 존재했다.

1. Array

//Java
String[] javaArray = {"1", "2", "3"};

//Kotlin  : Array<String> 부분은 생략이 가능하다.
val kotlinArray: Array<String> = arrayOf("1", "2", "3");
val kotlinArray arrayOf("1", "2", "3");

문법이 너무 신기하다.


2. 배열의 사용법

//Until 끝 값을 포함하지 않는 범위를 생성 (시작값 ≤ i < 끝값).
for (i in 1 until 10) {
    println(i) // Prints numbers from 1 to 9 (10 is excluded)
}

//DownTo 값을 감소시키며 반복.
for (i in 10 downTo 1) {
    println(i) // Prints numbers from 10 to 1
}

//Step 반복 간격을 설정 (기본값은 1).
for (i in 1..10 step 2) {
    println(i) // Prints 1, 3, 5, 7, 9
}

꽤나 많은 것들이 확장 되는 것 같다.


3. print

여러 가지가 있지만, 그 중에 JavaScript template literal 처럼 사용하는게 너무 신기했다.

//JavaScript
const literalWord = 'value of Literal'
console.log(`literalWord : ${literalWord}`)

//Kotlin
val literalWord: String = "value of Literal"
println("literalWord : $literalWord");

어떤가? 너무 유사해보이지 않은가??


4. when

fun getGrade (examScore: Int): Char {
    return when {
        examScore >= 90 -> 'A'
        examScore >= 80 -> 'B'
        examScore >= 70 -> 'C'
        examScore >= 60 -> 'D'
        else -> 'F'
    }
}
val grade = getGrade(80)
println("grade: $grade") //B

JavaScript 에서 switch case 와 동일해보인다.
문법자체는 더 간결하고 깔끔하며 break; 를 걸어줄 필요가 없어서 편의성이 더 높다.


5. set

//1. mutable set : 가변
val mutableAnimalSet = mutableSetOf("Lion", "Elephant", "Dog", "1", "2", "1")
println("mutableAnimalSet: $mutableAnimalSet")

//2. immutable set : 불변
val animalSet = setOf("Lion", "Elephant", "Dog", "1", "2", "1")
println("animalSet: $animalSet")

신기했던 건 setOf, mutableSetOf 함수를 사용하면, 중복된 내용은 삭제가 된다는 것이었다.

//output
mutableMap: {key1=Lion, key2=Elephant, key3=Dog}




React.js, Next.js 등을 공부하던 와중
회사에서 인턴 직원분이 백엔드 코드를 짜고 제출하는 모습을 봤다
내용도 꽤나 괜찮았다
이에 자극받아 백엔드 쪽 공부의 필요성을 느끼게 되어 Kotlin 코틀린 공부를 시작하게 되었다.

 

반응형

+ Recent posts