2024년 10월 2일 수요일

Java 8 Spring Boot 세팅

오랜만에 확인해보니 Spring Initializer(https://start.spring.io/)에서 Java 8 지원이 끊겼다. 17부터만 제공하여 17 프로젝트로부터 Java 8로 변경하는 내용을 간단히 정리.


1. spring-boot-starter-parent의 버전을 2.6.2로 변경한다.

<parent>

  • <groupId>org.springframework.boot</groupId>
  • <artifactId>spring-boot-starter-parent</artifactId>
  • <version>2.6.2</version>


2. properties 태그에서 java.version을 1.8로 변경해주고, maven-jar-plugin.version도 3.1.1로 낮춰준다.

<properties>

  • <java.version>1.8</java.version>
  • <maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>

React Query

React Query에 대해 간단히 정리.

React Query는 서버와의 통신 작업을 진행할 때 반복적으로 발생하는 번거로운 작업을 안정적으로 대신해주는 역할을 한다고 이해하고 있다. 데이터의 상태와 생명 주기를 관리해주는 도구로 볼 수 있고, 사용했을 때 코드가 간결해지고 유지 보수가 쉬워지는 장점이 보인다.

React Query는 고맙게도 데이터를 가져온 뒤 그 결과를 상태로 관리해준다. useState와 useEffect를 이용해 직접 데이터를 다루지 않아도 되고, 로딩 중이나 에러가 발생하는 상황의 처리도 더 간결한 방식으로 사용할 수 있다. 

const { data, isLoading, isError } = useQuery(['api이름'], 초기로드함수);

- data: 데이터를 성공적으로 가져온 경우 여기에 데이터가 담긴다.

- isLoading: 데이터가 로딩 중일 때

- isError: 데이터 로드 실패 시


React Query는 데이터 요청을 캐싱한다. 무슨 얘기냐면 데이터 변경 없이 컴포넌트가 다시 렌더링 되어야 하는 상황에서 동일한 데이터에 대한 불필요한 추가 요청을 막아준다. 이전 데이터를 캐시에서 가져오는데, 사용자가 원하면 invalidateQueries를 통해 캐시를 무효화하고 최신 데이터를 가져오도록 할 수도 있다.


구현 패턴은 비슷해지는데

1. useQuery로 데이터를 가져오고

2. useMutation을 사용해 데이터를 수정한다. 그리고 queryClient.invalidateQueries로 데이터를 갱신한다.


// api 호출부.
import axios from 'axios';

export const fetchItems = async () => {
  const response = await axios.get('/items');
  return response.data;
};

export const updateItem = async ({ id, field1, field2 }) => {
  const response = await axios.put(`/items/${id}`, { field1, field2, });
  return response.data;
};


// 컴포넌트에서 사용
import React from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { fetchItems, updateItem } from './api';

const 컴포넌트 = () => {
  const queryClient = useQueryClient();
  const { data: items, isLoading, isError, refetch } = useQuery(['items'], fetchItems);

  // 업데이트 시 mutation.mutate() 식으로 사용.
  const mutation = useMutation(updateItem, {
    onSuccess: () => {
      queryClient.invalidateQueries(['items']); // 서버 데이터와 다시 동기화
    },
  });

  if (isLoading) return <div>Loading...</div>;
  if (isError) return <div>Error loading data</div>;

  // 렌더링 코드 반환 구문
};

2024년 2월 2일 금요일

REACT - JSX

회사에서 리액트 스터디를 하게되서..

책을 한권 구매해서 순번을 정해 한 챕터씩 맡아 내용을 정리하기로 했다.

난 챕터 3의 JSX를 정리하게 되서 짧막하게 정리해본다.


배경은?

최근의 웹은 더욱 상호 작용적으로 변하면서 점점 JavaScript 코드가 컨텐츠를 결정하게 되었다.

React가 렌더링 로직과 마크업을 같은 장소, 즉 컴포넌트에 위치시키게 된 것은 흐름을 반영한 자연스러운 결과다. 로직과 마크업을 합치기로 했다면, 이왕에 HTML 구문 그대로를 JavaScript 파일 안에 넣을 수 있으면 더 직관적일 것이다.


JSX : Javascript Syntax eXtension

한줄 정리 : JSX는 JavaScript 파일 안에 HTML과 유사한 마크업을 작성할 수 있게 해주는 JavaScript 확장 문법이다. (HTML과 살짝 다르다.)


그럼 왜 사용하는가?

JSX를 사용하면 코드가 간결해져 보기에 편하다. createElement 함수를 이용해 엘리먼트를 만드는 방법과 비교하면 차이가 극명한데, JSX 방식을 사용하면 JSX 내용을 보고 결과물을 예상하기에도 좋다.


JSX는 HTML보다 조금 더 엄격하게 동작하는데 아래의 3가지 룰이 추가된다.

1. 단일 루트 요소를 반환해야 한다.

 : 여러 개의 요소를 반환하고 싶은 경우 <div> 태그와 같은 것으로 감싸줄 필요가 있다. HTML 트리에 흔적을 남기고 싶지 않은 경우라면 <>와 </>의 빈 태그를 활용해도 된다. (이런 태그를 Fragment라 부른다.) 


2. 태그는 반드시 받아야 한다.

 : <img>, <li>와 같은 태그도 예외가 없다. 반드시 <img />, <li> ~~~ </li>와 같이 닫아주어야 한다.


3. 낙타 표기법으로 작성한다.

 : JSX도 결국 JavaScript 코드로 변환하는 과정을 거치게 된다. JSX로 작성된 속성은 JavaScript 객체의 키가 되는데 JavaScript에는 변수 이름에 제한이 있다. 예를 들면 대시를 포함하거나 class와 같은 예약어를 사용할 수 없는데, 이에 대한 방안으로 React에서는 낙타 표기법으로 속성을 표기하기로 하였다.

stroke-width는 strokeWidth, class는 className 식으로 작성해야 한다.


다행스러운 것은 HTML을 JSX로 변환해주는 온라인 컨버터가 존재한다는 것인데 주소는 다음과 같다.

https://transform.tools/html-to-jsx


누군가는 마크업에 간단한 로직을 포함하거나 동적으로 변하는 프로퍼티를 넣고 싶을 수 있다. JSX에선 중괄호를 이용해 이러한 작업을 할 수 있게 지원한다. (값의 대입 뿐만 아니라 함수의 호출도 지원한다.)

+ CSS나 오브젝트를 전달하고 싶은 경우 '{{ ... }}'의 이중 중괄호로 표현해야 동작한다.



2023년 3월 11일 토요일

Protobuf-net 서버-클라이언트 데이터 송수신 예제

성능 신경쓰지 않고 단순히 데이터 주고 받는데 집중하면 동기 방식의 TCP/IP에 protobuf-net 조합이 가장 간편한 것 같다. 예제는 ChatGPT에서 찾은 것으로 단순하게 메시지 하나를 전달한다. 이 정도 예제만 바로 찾을 수 있어도 접근하기가 얼마나 쉬워지는지. 아쉬운 부분은 회사에서는 ChatGPT가 막혀있다.


아래는 서버 코드. 클라이언트 네트워크 스트림을 대상으로 Serialize를 호출하면 데이터가 클라이언트 쪽으로 전송된다. Protobuf-net을 사용하는 경우 *.proto 파일을 별도로 작성하고, 관리하지 않아도 되기 때문에 사용하기 간편하다.


using System;
using System.Net;
using System.Net.Sockets;
using ProtoBuf;

public class Program
{
    private const int Port = 12345;
    private const int BufferSize = 1024;

    [ProtoContract]
    public class Message
    {
        [ProtoMember(1)]
        public int Id { get; set; }
        [ProtoMember(2)]
        public string Text { get; set; }
    }

    static void Main(string[] args)
    {
        var listener = new TcpListener(IPAddress.Any, Port);
        listener.Start();
        Console.WriteLine($"Listening on port {Port}");

        while (true)
        {
            using (var client = listener.AcceptTcpClient())
            {
                var stream = client.GetStream();
                var buffer = new byte[BufferSize];
                var bytesRead = stream.Read(buffer, 0, BufferSize);
                var message = Serializer.Deserialize<Message>(new ArraySegment<byte>(buffer, 0, bytesRead));

                Console.WriteLine($"Received message from client: {message.Text}");

                var response = new Message { Id = 1, Text = "Hello from server!" };
                Serializer.Serialize(stream, response);
                Console.WriteLine($"Sent response to client: {response.Text}");
            }
        }
    }
}

다음은 클라이언트. ProtoContract 클래스는 서로 일치해야 한다.

using System;
using System.Net.Sockets;
using ProtoBuf;

public class Program
{
    private const string Host = "localhost";
    private const int Port = 12345;
    private const int BufferSize = 1024;

    [ProtoContract]
    public class Message
    {
        [ProtoMember(1)]
        public int Id { get; set; }
        [ProtoMember(2)]
        public string Text { get; set; }
    }

    static void Main(string[] args)
    {
        using (var client = new TcpClient(Host, Port))
        {
            var stream = client.GetStream();

            var message = new Message { Id = 1, Text = "Hello from client!" };
            Serializer.Serialize(stream, message);
            Console.WriteLine($"Sent message to server: {message.Text}");

            var buffer = new byte[BufferSize];
            var bytesRead = stream.Read(buffer, 0, BufferSize);
            var response = Serializer.Deserialize<Message>(new ArraySegment<byte>(buffer, 0, bytesRead));

            Console.WriteLine($"Received response from server: {response.Text}");
        }
    }
}


2021년 9월 26일 일요일

Visual Studio 2019+ D(서브) 드라이브 설치

Visual Studio 2019 버전을 메인 드라이브인 C 드라이브가 아닌 다른 드라이브에 설치하는 방법에 대해 정리해 본다.


SSD 128 GB 정도의 작은 저장 장치를 메인 드라이브로 사용하는 경우 용량 부족으로 Visual Studio 2019 설치가 어려운 경우가 있는데 윈도우 시스템의 Junction(리눅스 시스템의 Symbolic Link) 기능을 이용해 일부 설치 용량을 서브 드라이브에 할당시킬 수 있다.


어느 정도의 공간 절약이 되냐면 Professional 버전 기준 .NET 데스크탑 개발 환경과 C++ 데스크탑 개발 환경을 설치할 때 12GB 가량 공간이 필요한데 Junction을 이용하면 4GB 정도 공간 절약이 가능하다.


원문 링크 : https://eventhorizon.tistory.com/110


방법>

1. 관리자 권한으로 cmd.exe를 실행한다.

2. 실제 설치가 이루어질 디렉토리를 D 드라이브에 생성한다.

3. 링크를 배치할 디렉토리를 C 드라이브에 생성한다.

4. mklink 명령어를 이용해 링크를 생성한다.

5. Visual Studio 설치를 진행한다.


ex) Professional 기본 설치 경로를 이용하는 경우 예시. 2단계 메인 드라이브에 디렉터리를 생성할 때 'Professional' 디렉토리는 제외해서 생성해야 한다.

1. mkdir -p "D:\Program Files (x86)\Microsoft Visual Studio\2019\Professional"

2. mkdir -p "C:\Program Files (x86)\Microsoft Visual Studio\2019"

3. mklink /j Professional "D:\Program Files (x86)\Microsoft Visual Studio\2019\Professional"




2021년 1월 26일 화요일

[Protocol Buffers] optimize_for 옵션

프로토콜 버퍼 컴파일러(protoc) 옵션으로 사용 목적에 따라 SPEED, CODE_SIZE, 또는 LITE_RUNTIME 값을 지정할 수 있다.


기본 값은 SPEED이다.


• SPEED (기본) : 기본 값인 만큼 프로토콜 버퍼의 모든 기능(Descriptor, Reflection)이 포함되며 최적화된 코드를 생성해 준다.


• CODE_SIZE : SPEED 옵션 대비 작은 코드 사이즈를 갖는 결과 파일을 만들어 주지만 SPEED 보다 동작 속도가 느려진다. 매우 많은 수의 .proto 파일을 포함하고(메시지 개수에 코드 사이즈가 비례하므로) 있거나 속도가 중요하지 않은 앱에서 유용한 옵션이다.


• LITE_RUNTIME : 'lite' 런타임 라이브러리에만 의존하는 클래스를 생성한다. (libprotobuf 대신 libprotobuf-lite) 전체 라이브러리보다 10배 작은 라이브러리로 동작이 가능하기 때문에 휴대폰과 같이 제한된 플랫폼에서 실행되는 경우 유용하다. 생성된 클래스는 Message 대신 MessageLite 인터페이스를 구현하며 이 옵션으로 컴파일 하는 경우 Descriptor, Reflection 등의 기능은 제외된다.


* 동일한 proto 파일을 여러 번 로드할 때 Descriptor로 부터 에러가 발생하는 경우가 있는데 이러한 경우엔 Descriptor 기능이 제외된 LITE_RUNTIME 옵션을 사용하는 것을 고려해볼 수 있다.

https://github.com/protocolbuffers/protobuf/issues/4126


일반적인 타겟 환경을 가진 프로그램이라면 기본 값을 그대로 쓰면 될 것 같고, 바이너리 사이즈가 중요한 프로그램이라면 CODE_SIZE, LITE_RUNTIME 등의 옵션을 고려해 볼만 한 것 같다. 사실 protoc에 의해 생성된 클래스를 그대로 사용하는 것이 일반적이라 Descriptor 같은 기능은 고급 사용자가 아니면 잘 쓰이지 않는다.


2021년 1월 11일 월요일

[Protocol Buffers] Base 128 Varints

프로토콜 버퍼의 인코딩 방식을 이해하려면 우선 Varint를 이해해야 한다. Varint는 하나 이상의 바이트를 사용해 정수를 직렬화 하는 방법으로 숫자가 작을수록 적은 바이트 수를 차지하게 된다. (아주 큰 숫자가 아니면 데이터 크기가 절약된다.)


Varint에서 마지막 바이트를 제외한 각 바이트의 최상위 비트(msb)는 1로 하기로 약속되어 있다. 이는 앞으로 올 바이트가 더 있음을 나타내기 위함이다. 

반대로 msb가 0이라면 Varint의 끝을 의미하게 된다. 따라서 1바이트를 차지하는 Varint로 표현할 수 있는 가장 큰 숫자는 127(0111 1111)이 된다. 또 하나의 룰로 Varint는 최하위 그룹을 먼저 위치시키는 방식을 택한다. (least significant group first).


표현 예시.

웹 페이지 설명에 나온 것처럼 숫자 1은 1바이트로 표현이 가능하므로 msb는 0이 되고 표현은 '0000 0001'이 된다.


그렇다면 숫자 300은 어떻게 표현될까? 앞서 언급된 것처럼 숫자 300은 하나의 바이트로 표현이 불가능 하므로 첫 바이트의 msb는 1로 셋 되어야 한다.

결과적으로 숫자 300은 아래와 같이 표현된다.


 1010 1100 0000 0010


여기서 msb를 떼면 010 1100 000 0010이 되는데, 앞서 적혀있는 대로 최하위 그룹이 먼저 위치하게 되므로 우리가 이해하는 숫자로 인식하기 위해선 7비트 단위 그룹의 위치를 서로 바꾸어 주어야 한다.


따라서 010 1100 000 0010은, 000 0010 010 1100 되어야 하고, 의미 없는 비트를 버린 뒤 두 그룹을 연결하면

→ 100101100 = 300 (256 + 32 + 8 + 4)이 된다.


https://developers.google.com/protocol-buffers/docs/encoding#varints