17 Jul 2024 |
Etc
이 포스트는 제가 사용하고 있던 nestjs-boilerplate에 대해 ESLint v8에서 v9로 마이그레이션을 진행한 내용을 정리한 포스트 입니다.
ESLint v9
ESLint v9으로 업데이트가 되면서 몇몇 설정이 변경되었습니다. 제일 큰 변경점은 설정 파일이 .eslintrc
파일이 eslint.config.js
로 변경 되면서 설정 파일의 포맷이 모두 변경되었습니다.
v9가 막 업데이트 되었을 때는 아직 많은 플러그인들이 v9를 지원하지 않아서 업데이트를 하지 않았었는데, 이제는 대부분의 플러그인들이 v9를 지원하고 있어서 업데이트를 진행하였습니다.
![ss](https://i.imgur.com/4oQKVSA.png)
마이그레이션
우선 기존 사용하던 플러그인을 모두 업데이트 해주었습니다. 일부 플러그인이 v9을 지원히지 않아 삭제 혹은 대체한 플러그인도 존재합니다.
"eslint": "^9.7.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-security": "^3.0.1",
"eslint-plugin-simple-import-sort": "^12.1.1",
"typescript-eslint": "8.0.0-alpha.10"
"typescript": "5.4.5",
"@eslint/js": "^9.7.0",
"@types/eslint__js": "^8.42.3",
"@typescript-eslint/eslint-plugin": "^7.11.0", // Removed
"@typescript-eslint/parser": "^7.11.0", // Removed
"eslint-plugin-import": "^12.1.0", // Removed
다른 프로젝트를 참고하려고 서칭을 하던 도중 ESLint에서 직접 Migrator를 제공하고 있어서 이를 이용하여 마이그레이션을 진행하였습니다.
ESLint Configuration Migrator
.eslintrc
와 .eslintignore
파일을 참조하여 자동으로 설정파일을 만들어 주는 플러그인입니다. 제가 사용하는 설정의 경우 완벽하게 마이그레이션 되진 않았고 일부 수정이 필요한 부분이 있었습니다. 그래도 대부분의 설정이 마이그레이션 되어서 편하게 업데이트를 진행할 수 있었습니다.
아래 코드는 마이그레이션된 설정 파일입니다. 제가 개인적으로 사용하고 있는 설정이어서 다른 프로젝트에 적용하기 위해서는 수정이 필요할 수 있습니다.
import { FlatCompat } from "@eslint/eslintrc";
import eslintJs from "@eslint/js";
import pluginSecurity from "eslint-plugin-security";
import simpleImportSortPlugin from "eslint-plugin-simple-import-sort";
import globals from "globals";
import path from "path";
import eslintTs from "typescript-eslint";
import tseslint from "typescript-eslint";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: eslintJs.configs.recommended
});
export default eslintTs.config(
eslintJs.configs.recommended,
...eslintTs.configs.recommendedTypeChecked,
...compat.extends(
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
),
{
ignores: ["**/types.d.ts", "**/*.js", "**/node_modules/"]
},
{
languageOptions: {
globals: {
...globals.es2020,
...globals.node,
Atomics: "readonly",
SharedArrayBuffer: "readonly"
},
parserOptions: {
project: "tsconfig.json"
}
},
settings: {
"import/resolver": {
node: {
extensions: [".js", ".jsx", ".ts", ".tsx"]
}
}
},
plugins: {
"@typescript-eslint": tseslint.plugin,
"simple-import-sort": simpleImportSortPlugin,
security: pluginSecurity.configs.recommended,
},
rules: {
"no-empty-pattern": "off",
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-argument": "off",
"@typescript-eslint/no-explicit-any": "off",
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error",
semi: "error"
}
}
);
vscode에 적용
vscode에 ESLint v9의 설정파일을 적용하기 위해서 eslint.useFlatConfig
설정을 활성화할 필요가 있습니다.
![vscode](https://i.imgur.com/48YUfn4.png)
위 설정을 활성화하면 `eslint.config.js’ 파일을 참고하여 설정을 적용할 수 있습니다.
마치며
ESLint v9로 업데이트를 진행하면서 몇몇 설정이 변경되었지만, 대부분의 설정은 그대로 사용할 수 있었습니다. 마이그레이션도 Migrator의 존재 때문에 쉽게 진행할 수 있었고, vscode에서도 설정을 쉽게 적용할 수 있어서 편하게 업데이트를 진행할 수 있었습니다.
지금은 충분히 v9로 마이그레이션을 고려해도 좋을 만큼 충분히 안정화 되었다고 생각합니다.
02 Jan 2024 |
Etc
이 포스트는 advent-of-spin에 업로드 된 Challenge 4에 대해서 진행했던 내용을 정리한 포스트 입니다.
Rust 언어로 진행하였으며 이 포스트에 게시된 소스코드는 Github 에서 확인할 수 있습니다.
Spec
- Bulls’n’Cows 게임을 진행합니다. 숫자야구와 동일한 게임입니다.
- https://bulls-n-cows.fermyon.app/api API를 통해서 게임을 진행할 수 있으며 이 게임을 진행할 수 있는 로직을 구현합니다.
- https://bulls-n-cows.fermyon.app/api?guess=012 식으로 query에 guess 를 통해서 값을 전달할 수 있으며 응답은 아래와 같습니다.
- 이후 동일한 게임을 이어서 할 때 •
https://bulls-n-cows.fermyon.app/api?guess=012&id=1836633b-8dd1-41c2-b084-644b37c6fd1b
처럼 id를 같이 전달합니다.
Work
- 이번 챌린지는 로직 구현에 초점을 맞추어 진행합니다. 라우터는
GET
하나만 뚫어서 진행합니다.
-
spin new
명령어를 이용하여 새로운 프로젝트를 생성해 줍니다.
![spin new](https://i.imgur.com/vuOsoAw.png)
-
그리고 생성된 프로젝트의 Cargo.toml
파일에 아래와 같이 dependencies를 추가해 줍니다.
![dependencies](https://i.imgur.com/aWJeD5Q.png)
- request / response 객체를 serialize / deserialize 하기 위해 serde 라이브러리를 사용합니다.
-
src/lib.rs
에 POST를 처리하기 위한 로직을 구현합니다.
- 경우의 수가 많지 않아(0,1,2,3,4 만 사용) 모든 경우를 array에 넣고 모든 경우를 시도하는 방법으로 구현하였습니다.
use http::StatusCode;
use serde::{Deserialize, Serialize};
use spin_sdk::{
http::{IntoResponse, Method, Request, Response},
http_component,
};
#[derive(Debug, Deserialize, Serialize)]
struct GameResponse {
cows: u64,
bulls: u64,
gameId: String,
guesses: u64,
solved: bool,
}
#[http_component]
async fn handle_request(_req: http::Request<Vec<u8>>) -> anyhow::Result<impl IntoResponse> {
let guess_numbers = [
"013", "014", "021", "023", "024", "031", "032", "034", "102", "103", "104", "120",
"123", "124", "130", "132", "134", "201", "203", "204", "210", "213", "214", "230", "231",
"234", "301", "302", "304", "310", "312", "314", "320", "321", "324",
];
let status = StatusCode::OK;
let body = "Done".to_string();
let uri = format!("https://bulls-n-cows.fermyon.app/api?guess=012");
let req = Request::builder().method(Method::Get).uri(uri).build();
// Send the request and await the response
let res: Response = spin_sdk::http::send(req).await?;
let response: GameResponse = serde_json::from_slice(res.body().to_vec().as_slice()).unwrap();
if response.solved {
println!("{:?}", response);
Ok(Response::builder()
.status(status)
.header("content-type", "application/json")
.body(body)
.build())
}
else {
let game_id = response.gameId;
for guess in guess_numbers.iter() {
let uri = format!("https://bulls-n-cows.fermyon.app/api?guess={}&id={}", guess, game_id);
let req = Request::builder().method(Method::Get).uri(uri).build();
// Send the request and await the response
let res: Response = spin_sdk::http::send(req).await?;
let response: GameResponse = serde_json::from_slice(res.body().to_vec().as_slice()).unwrap();
if response.solved {
println!("{:?}", response);
break;
}
}
Ok(Response::builder()
.status(status)
.header("content-type", "application/json")
.body(body)
.build())
}
}
30 Dec 2023 |
Etc
이 포스트는 advent-of-spin에 업로드 된 Challenge 3에 대해서 진행했던 내용을 정리한 포스트 입니다.
Rust 언어로 진행하였으며 이 포스트에 게시된 소스코드는 Github 에서 확인할 수 있습니다.
Spec
/
에 POST
method 구현하기
- POST:
/
body로 아래의 형식대로 값을 전달
{
"place": "North Pole",
"characters": ["Santa Claus", "The Grinch", "a pingvin"],
"objects": ["A spoon", "Two presents", "Palm tree"]
}
- 결과 값으로 제공된 데이터 세트에 대해서 LLM을 사용하여 크리스마스와 관련된 이야기를 만들어 출력
json
{
"story": "<YOUR STORY HERE>"
}
- 상태코드 200으로 반환
- response header에
Content-Type: application/json
Work
- 이번 챌린지는 단순하게 POST 메소드로 라우팅을 하나만 뚫으면 되는 간단한 챌린지 입니다.
-
spin new
명령어를 이용하여 새로운 프로젝트를 생성해 줍니다.
![spin new](https://i.imgur.com/vuOsoAw.png)
-
그리고 생성된 프로젝트의 Cargo.toml
파일에 아래와 같이 dependencies를 추가해 줍니다.
![dependencies](https://i.imgur.com/aWJeD5Q.png)
- request / response 객체를 serialize / deserialize 하기 위해 serde 라이브러리를 사용합니다.
- 이번 챌린지에선 LLM을 사용하므로 관련 설정을 spin.toml 에 추가합니다.
[component.challenge]
source = "target/wasm32-wasi/release/challenge.wasm"
allowed_outbound_hosts = []
ai_models = ["llama2-chat"]
[component.challenge.build]
command = "cargo build --target wasm32-wasi --release"
watch = ["src/**/*.rs", "Cargo.toml"]
-
src/lib.rs
에 POST를 처리하기 위한 로직을 구현합니다.
use http::{Method, StatusCode};
use serde::{Deserialize, Serialize};
use spin_sdk::{
http::{IntoResponse, Response},
http_component, llm,
};
#[derive(Debug, Deserialize, Serialize)]
struct StoryRequest {
place: String,
characters: Vec<String>,
objects: Vec<String>,
}
#[derive(Debug, Deserialize, Serialize)]
struct StoryResponse {
story: String,
}
fn make_story(place: String, characters: Vec<String>, objects: Vec<String>) -> String {
let model = llm::InferencingModel::Llama2Chat;
let prompt = format!(
"Can you make story with place {:?} and these characters {:?} and this objects {:?}",
place, characters, objects
);
let inference = llm::infer(model, &prompt);
format!("{:?}", inference)
}
#[http_component]
fn handle_request(req: http::Request<Vec<u8>>) -> anyhow::Result<impl IntoResponse> {
let (status, body) = match *req.method() {
Method::POST => {
let request: StoryRequest =
serde_json::from_slice(req.body().clone().to_vec().as_slice()).unwrap();
let place = request.place;
let characters = request.characters;
let objects = request.objects;
let story = make_story(place, characters, objects);
let response = StoryResponse { story: story };
let json_response = serde_json::to_string(&response)?;
(StatusCode::OK, json_response)
}
_ => (StatusCode::METHOD_NOT_ALLOWED, String::new()),
};
Ok(Response::builder()
.status(status)
.header("content-type", "application/json")
.body(body)
.build())
}
28 Dec 2023 |
Etc
이 포스트는 advent-of-spin에 업로드 된 Challenge 2에 대해서 진행했던 내용을 정리한 포스트 입니다.
Rust 언어로 진행하였으며 이 포스트에 게시된 소스코드는 Github 에서 확인할 수 있습니다.
Spec
/
에 POST
method 구현하기
- POST:
/
body로 아래의 형식대로 값을 전달
{
"kids": [1, 4 , 5, 6, 3, 2, 7],
"weight": [23, 45, 23, 43, 12, 32, 45],
"capacity": 120
}
- 결과 값으로 제공된 데이터 세트에 대해서 얼마나 많은 Kids에게 선물을 줄 수 있는지 출력
json
{
"kids": 18
}
- response header에
Content-Type: application/json
Work
- 이번 챌린지는 단순하게 POST 메소드로 라우팅을 하나만 뚫으면 되는 간단한 챌린지 입니다.
-
spin new
명령어를 이용하여 새로운 프로젝트를 생성해 줍니다.
![spin new](https://i.imgur.com/vuOsoAw.png)
-
그리고 생성된 프로젝트의 Cargo.toml
파일에 아래와 같이 dependencies를 추가해 줍니다.
![dependencies](https://i.imgur.com/aWJeD5Q.png)
- request / response 객체를 serialize / deserialize 하기 위해 serde 라이브러리를 사용합니다.
- 이제 구현을 해야합니다.
-
문제를 해결하기 위해서 DP를 사용하여 로직을 구현할 수 있습니다.
fn optimal_kids_reached(kids: Vec<usize>, weight: Vec<usize>, capacity: usize) -> usize {
let n = kids.len();
let mut dp = vec![vec![0; capacity + 1]; n + 1];
for i in 1..=n {
for w in 1..=capacity {
if weight[i - 1] <= w {
dp[i][w] = dp[i - 1][w].max(kids[i - 1] + dp[i - 1][w - weight[i - 1]]);
} else {
dp[i][w] = dp[i - 1][w];
}
}
}
dp[n][capacity]
}
-
src/lib.rs
에 POST를 처리하기 위한 로직을 구현합니다.
use http::{Method, StatusCode};
use serde::{Deserialize, Serialize};
use spin_sdk::{
http::{IntoResponse, Response},
http_component,
};
#[derive(Debug, Deserialize, Serialize)]
struct KidsRequest {
kids: Vec<usize>,
weight: Vec<usize>,
capacity: usize,
}
#[derive(Debug, Deserialize, Serialize)]
struct KidsResponse {
kids: usize,
}
fn optimal_kids_reached(kids: Vec<usize>, weight: Vec<usize>, capacity: usize) -> usize {
let n = kids.len();
let mut dp = vec![vec![0; capacity + 1]; n + 1];
for i in 1..=n {
for w in 1..=capacity {
if weight[i - 1] <= w {
dp[i][w] = dp[i - 1][w].max(kids[i - 1] + dp[i - 1][w - weight[i - 1]]);
} else {
dp[i][w] = dp[i - 1][w];
}
}
}
dp[n][capacity]
}
#[http_component]
fn handle_request(req: http::Request<Vec<u8>>) -> anyhow::Result<impl IntoResponse> {
let (status, body) = match *req.method() {
Method::POST => {
let request: KidsRequest =
serde_json::from_slice(req.body().clone().to_vec().as_slice()).unwrap();
let kids = request.kids;
let weight = request.weight;
let capacity = request.capacity;
let maximum_kids = optimal_kids_reached(kids, weight, capacity);
let response = KidsResponse { kids: maximum_kids };
let json_response = serde_json::to_string(&response)?;
(StatusCode::OK, json_response)
}
_ => (StatusCode::METHOD_NOT_ALLOWED, String::new()),
};
Ok(Response::builder()
.status(status)
.header("content-type", "application/json")
.body(body)
.build())
}
-
spin build
로 빌드를 수행합니다.
![spin build](https://i.imgur.com/KvZQn6T.png)
-
spin up
으로 로컬에서 실행이 가능합니다.
![spin up](https://i.imgur.com/0mSQsgN.png)
-
curl을 사용하여 가볍게 테스트가 가능합니다.
curl localhost:3000 -H 'Content-Type: application/json' -d '{"kids":[1,4,5,6,3,2,7], "weight":[23,45,23,43,12,32,45], "capacity":120}'
![curl](https://i.imgur.com/f8xD60f.png)
-
아래 명령어로 제출 전 테스트가 가능합니다.
![test](https://i.imgur.com/VcyvuRO.png)
30 Nov 2023 |
Etc
이 포스트는 advent-of-spin에 업로드 된 Challenge 1에 대해서 진행했던 내용을 정리한 포스트 입니다.
Rust 언어로 진행하였으며 이 포스트에 게시된 소스코드는 Github 에서 확인할 수 있습니다.
Spec
/index.html
에 Static page 호스팅 하기 (크리스마스 관련된 무언가를 담아서)
/data
path에서 GET/POST method 구현하기
- GET
/data?advent
: 저장된 value값 출력 / 성공시 200
- POST:
/data?advent
body에 저장된 값 저장 / 성공시 201
{
"value": "<Matt의 자동화된 위시리스트>"
}
- response header에
Content-Type: application/json
Work
첫번째 요구사항인 /index.html
을 호스팅하기 위해 static-fileserver
을 사용합니다.
![spin new static-fileserver](https://i.imgur.com/L6O7rLf.png)
생성된 폴더 구조는 아래와 같습니다.
![folder static-fileserver](https://i.imgur.com/NqKwR8m.png)
assets 경로에 있는 파일들을 호스팅 해주는 파일서버 입니다.
두번째 요구사항를 위해 key-value store를 만들어줍니다.
![spin new key-value](https://i.imgur.com/LcPpueb.png)
이 두 프로젝트를 별도의 서버가 아닌 하나의 서버로 작업할 것이기 때문에 두 프로젝트를 하나로 합칩니다.
합치고 난 이후에 이런 구조가 됩니다.
![folder key-value](https://i.imgur.com/18qYKKw.png)
- component는 key-value store인
spin-key-value
와 index.html
을 호스팅해줄 fileserver
총 두가지 컴포넌트가 존재합니다.
/index.html
경로에 호스팅을 해 주어야 하기 때문에 fileserver
의 route 는 /
로 하였습니다.
- 또한
index.html
만 사용하기 때문에 /
를 /index.html
로 변경하였습니다.
- spin-key-value 의 경우
/data
만이 존재하기 때문에 route를 /data
로 하였습니다.
key_value_stores = ["default"]
를 spin-key-value
컴포넌트에 추가하였습니다.
이를 실행해보면 다음과 같이 route가 생성됩니다.
![route](https://i.imgur.com/QC70I7x.png)
이제 구현을 해야합니다.
assets/index.html
경로에 크리스마스 느낌의 이미지가 담긴 html 파일을 만들어줍니다.
<!DOCTYPE html>
<html lang="en">
<body>
<img src="https://www.shutterstock.com/image-vector/flork-meme-christmas-vector-ilustration-600nw-2175960549.jpg" />
</body>
</html>
![index.html](https://i.imgur.com/7oNx4zm.png)
src/lib.rs
에 GET, POST를 처리하기 위한 로직을 구현합니다.
use http::{Method, StatusCode};
use spin_sdk::{
http::{IntoResponse, Response},
http_component,
key_value::Store,
};
#[http_component]
fn handle_request(req: http::Request<Vec<u8>>) -> anyhow::Result<impl IntoResponse> {
let store = Store::open_default()?;
let (status, body) = match *req.method() {
Method::POST => {
store.set(req.uri().path(), req.body().as_slice())?;
println!(
"Storing value in the KV store with {:?} as the key",
req.uri().path()
);
(StatusCode::CREATED, None)
}
Method::GET => match store.get(req.uri().path())? {
Some(value) => {
println!("Found value for the key {:?}", req.uri().path());
(StatusCode::OK, Some(value))
}
None => {
println!("No value found for the key {:?}", req.uri().path());
(StatusCode::NOT_FOUND, None)
}
},
Method::DELETE => {
store.delete(req.uri().path())?;
println!("Delete key {:?}", req.uri().path());
(StatusCode::OK, None)
}
Method::HEAD => {
let code = if store.exists(req.uri().path())? {
println!("{:?} key found", req.uri().path());
StatusCode::OK
} else {
println!("{:?} key not found", req.uri().path());
StatusCode::NOT_FOUND
};
(code, None)
}
_ => (StatusCode::METHOD_NOT_ALLOWED, None),
};
Ok(Response::builder()
.status(status)
.header("content-type", "application/json")
.body(body)
.build())
}