2024년 8월 5일 월요일

JDLA Deep Learning for GENERAL 2024#4 (G検定)

 JDLA Deep Learning for GENERAL 2024#4 (G検定)合格した。




2024년 7월 30일 화요일

ReactJS : Redux

ReactJS : Redux

Redux

Redux란 한마디로 "Javascript 상태관리 라이브러리"이다.

여기서 말하는 상태관리란 컴퍼넌트내부에서 사용하는 데이터의 상태를 관리하는 라이브러리이다.
프론트엔드에서는 주로 데이터를 화면에 표시를 하는데, 데이터를 관리함으로서 데이터를 화면에 표시하는 상태를 제어할수 있다.

Redux를 이야기하기 전에 Flux에 대해 간단히 알아보자.
MVC 모델은 View-Model-Controller간의 복잡도가 증가하고 이로인해 데이터흐름의 파악이 힘들어 지는 경우가 있었다.
특히 블록(또는 컴퍼넌트) 구조로 되어 있는 복잡한 화면구성의 웹애플리케이션의 경우 부모컴퍼넌트에서 자식,자식의 자식 컴퍼넌트에 데이터를 전달(prop)한다든지, 이벤트를 발행시켜서 데이터를 갱신할때 “누가 데이터를 보관하고, 어떻게 이벤트를 전파할것인지” 기준을 정하는게 어렵다.

Flux 는 단방향으로 데이터를 흐르도록 하여, View가 Model을 직접 참조하지 않고 단지 상태값만 가지게 함으로서 상태변화에 따른 필요한 부분만 갱신하도록 하는 구조이다.

Action → Dispatcher → Store → View (갱신) → Action → D → S → V…

Flux 패턴의 가장 큰 장점은 개발 흐름이 단방향으로 흐르기 때문에 훨씬 파악하기 쉽고 코드의 흐름이 예측 가능(Predictable)하다는 것이다.

Redux 는 Flux 와 기본개념은 같지만, Redux는 store를 한개로 유지하고 reducer라는 것으로 자료를 갱신(새로운 상태를 생성)하여 view를 업데이트하는 구조이다.

Action → Dispatch → Reducer → Store 순서로

Redux 관련 용어

Store(스토어)

상태가 관리되는 공간이다.
각 Component는 Store의 데이터를 통해 업데이트한다.

Action(액션)

View에서 상태변경을 요청하는 단계이다. 화면을 갱신하기 위해서는 액션을 통해서 주문을 한다.

Reducer(리듀서) 또는 Dispatch(디스패치)

Reducer 는 액션을 스토어에 전달하는 역활을 한다. 즉 액션이라는 주문서를 보고 스토어의 데이터를 갱신한다. Dispatch 를 통해 Reducer 를 호출한다.

Subscribe(구독)

구독은 데이터가 갱신될때 UI를 갱신할수 있다.
useSelector를 사용하면 간편하게으로 subscribe 상태를 만들수 있다.

Redux toolkit

현시점에서 reactjs 에서 redux 를 사용할때에 react-redux + @reduxjs/toolkit 의 조합으로 사용하기를 권장당하고 있다.

따라서 createStore 보다는 createSlice(아…네이밍센스 극혐)을 이용해야 한다.
좋은점은 기존의 createStore, action,dispatcher정의 등 따로따로 정의하던것을 createSlice 를 이용하여 한번에 쉽게 만들면된다.

// createSlice.ts

import { createSlice, PayloadAction } from '@reduxjs/toolkit';

// 상태 타입 정의
export interface AppState {
  number: number;
}

// 초기 상태
const initialState: AppState = { number: 1 };

// 슬라이스 생성
const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state) => {
      state.number++;
    },
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.number += action.payload;
    },
  },
});

// 컴퍼넌트에서 사용하기 위해서 생성된 슬라이스의 액션과 리듀서를 공개(export)한다
export const { increment, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;

store.ts 파일을 만들어서 스토어를 설정하자.

import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';

const store = configureStore({
  reducer: counterReducer,
});

// 루트 상태 타입 정의
export type AppState = ReturnType<typeof store.getState>;

export default store;

컴퍼넌트에서 사용해 보자.
먼저 HomePage.tsx 를 만들어서 Redux 로 연동하자.

import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { AppState } from '../store';
import { increment, incrementByAmount } from '../counterSlice';

const HomePage: React.FC = () => {
  const number = useSelector((state: AppState) => state.number);
  const dispatch = useDispatch();
  return (
    <div>
      <h1>{number}</h1>
      <button onClick={() => dispatch(increment())}>Increment</button>
      <button onClick={() => dispatch(incrementByAmount(5))}>Increment by 5</button>
    </div>
  );
};

export default HomePage;

app.tsx 에서 Homepage를 불러내자.

import React from 'react';
import { Provider } from 'react-redux';
import store from './store';
import HomePage from './components/HomePage';

const App: React.FC = () => {
  return (
    <Provider store={store}>
      <div>
        <h1>Hello StackBlitz!</h1>
        <p>Start editing to see some magic happen :)</p>
        <HomePage />
      </div>
    </Provider>
  );
};

export default App;

만일 reducer 를 여러개 만들었다면, combineReducers 로 묶으면 된다.

//store.ts
import { configureStore } from '@reduxjs/toolkit';
import { combineReducers } from 'redux';
import counterReducer from './features/counterSlice';
import inputReducer from './features/inputSlice';

const rootReducer = combineReducers({
  counter: counterReducer,
  input: inputReducer,
});

const store = configureStore({
  reducer: rootReducer,
});

export type AppState = ReturnType<typeof store.getState>;
export default store;

모듈별로 한다면 아래와 같다.

src/
│
├── components/
│   ├── HomePage.tsx
│   └── About.tsx
│
├── features/
│   ├── counter/
│   │   ├── counterSlice.ts
│   │   └── Counter.tsx
│   ├── input/
│   │   ├── inputSlice.ts
│   │   └── Input.tsx
│
├── App.tsx
│
└── store.ts

2024년 7월 29일 월요일

css in js

css in js

CSS IN JS

말그대로, javascript 를 이용해서 css를 정의하는 한다는 것이다.
이런저런 라이브러리가 있지만 실무에서는 emotion+styled 라이브러리를 주로 사용한다.

css를 javascript로 정의해서 사용하면 머가 장점일까?

  1. className 을 변수로 하면 컴파일(빌드)할때 변수값에 해당하는 스타일 값을 중복안되도록 고유의 클래스 이름으로 생성한다.
  2. 스타일을 변수로 지정하면 에디터 등에서 쉽게 찾아볼수 있다.
  3. 사용하지 않는 코드에는 eslint 등에서 에러로 표시되기 때문에 미사용 스타이일은 삭제하기 쉽다.
  4. 단위테스트 작성이 쉽다.

불편한점은?
생성된 스타일로는 디버깅이 불편하다.

아래의 기사에서 css in js 라이브러리에 대해서 비교한것을 볼수있다.
https://techblog.zozo.com/entry/zozotown-css-in-js

2024년 현재 권장하는 선택지는 @emotion/styled이다.

emotion 라이브러리

Emotion 은 JS 로 css 스타일을 작성하도록 제작된 라이브러리다.
@emotion/css 를 설치( npm i @emotion/css )하면 아래와 같이 변수값을 css 기술할때 사용할수 있다.
프레임워크에 의존하지 않으므로 일반 자바스크립트 프레임워크에서도 어떤 자바스크립트 프레임워크에서도 사용가능하다.

import { css } from '@emotion/css'

const color = 'white'

render(
 <div
   className={css`
     padding: 32px;
     background-color: hotpink;
     font-size: 24px;
     border-radius: 4px;
     &:hover {
       color: ${color};
     }
   `}
 >
   Hover to change color.
 </div>

@emotion/react , @emotion/styled

emotion을 reactjs 에서 사용할때 사용성을 좋게 하기 위한 몇가지 기능을 추가한 @Emotion/react가 있다. 그리고 styled 라이브러리와 연동해서 사용하는 styled 라이브러리가 있다.

@emotion/react

emotionreact에서는 css라는 함수를 통해서 css={css({스타일객체})} 또는 css={cssstylesheet선언내용}와 같은 식으로 정의해서 사용한다.

css={css({스타일객체})} 로 사용할경우

/** @jsxImportSource @emotion/react */
import { css } from "@emotion/react";

function MyComponent() {
  return (
    <div
      css={css({
        backgroundColor: "Red",
      })}
    >
      나는 빨강
    </div>
  );
}
export default MyComponent;

{cssstylesheet선언내용} 로 사용할경우

/** @jsxImportSource @emotion/react */
import { css } from "@emotion/react";

function MyComponent() {
  return (
    <div
      css={css`
        background-color: Red;
      `}
    >
      나는 빨강
    </div>
  );
}
export default MyComponent;

권장하는 방식은 css()를 별도의 객체로 작성해서 사용하는 방식이다.

/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react'

const danger = css`
  color: red;
`


function MyComponent() {
  return (
    <div
      css={danger}>
      나는 빨강
    </div>
  );
}
export default MyComponent;

물론 css 를 파일로 공유해서 여러군데에서 사용할수도 있다.

const danger = css`
  color: red;
`
const fontSize15 = css`
  fontSize: '1.5rem';
`

export const errorCss = css`
	${danger}
  	fontWeight : 'bold';
`

//아래 처럼 여러개의 스타일을 배열로 넘겨줄수도 있다.
export const largeErrorCss = css([errorCss, fontSize15, {fontStyle: 'italic'}])

@emotion/styled

emotion에서 styled-components와 비슷한 방식으로 작성할수 있도록 도와주는 라이브러리이다.
emotion/react만 사용할경우

라고 써야하지만, styled 를 사용할경우 const MyStyleDiv=styled.div여러스타일들로 작성하여 처럼 사용자정의 태그형식으로 깔끔하게 작성할수 있다.

import styled from '@emotion/styled'  
const Button = styled.button`  
  color: red;  
`

<Button>Click Me!</Button>

설명서 : https://emotion.sh/docs/styled

파라메터(props)를 이용하여 가변스타일 지정

css를 javascript로 작성하는 가장 큰 장점은 역시 변수에 따라서 스타일을 다르게 적용할수 있다는 것이다.

/** @jsxImportSource @emotion/react */
import styled from '@emotion/styled';

interface ButtonProps {
  children: React.ReactNode;
  color?: string;
  background?: string;
}

const StyledButton = styled.button<ButtonProps>`
  padding: 6px 12px;
  border-radius: 8px;
  border: 1px solid lightgray;

  color: ${(props) => props.color || 'gray'};
  background: ${(props) => props.background || 'white'};
`;

const Button: React.FC<ButtonProps> = (props) => {
  return <StyledButton {...props}>{props.children}</StyledButton>;
};

export default Button;

// 사용 할때는

import Button from './Button'; 
...
<Button color="white" background="blue">My Button</Button>

2024년 7월 26일 금요일

ReactJsとNextJs

ReactJsとNextJs

예전에 잠깐 다뤄본 ReactJs가 지금 많이 바뀐 관계로 잊어버리기 전에 다시한번 정리해 본다.

NextJS 설치

ReactJs와 NextJs를 설치하고 설정하는데 이런저런 귀찮음이 있으니, create-next-app을 설치해서 NextJs기본 설정과 폴더구조(scaffold) 를 쉽게 만들도록 해보자.

npm install -g create-next-app

이때, npm install 로 설치하면 create-next-app 이 시스템의 아무곳에서도 실행되도록 package manager에 포함되는데 단지 이번만 create-next-app을 설치하고 실행하고 싶다면 npx 를 이용하여 해당 명령을 실행(package ecxecute) 할수 있다.
즉, npm 은 두고두고 사용할 라이브러리를 설치하는 명령어, npx는 지금만 사용하고 끝낼 라이브러리설치하는 명령어이다.

위의 npm 명령어로 실행했다면 컴퓨터 어디서든지, create-next-app 명령어를 실행할수 있다. “npm uninstall -g create-next-app” 로 언인스톨한다음 create-next-app 명령어를 치면 그런거 없다고 나온다.
그럴때는 이번만 설치해서 실행하는 npx create-next-app 을 실행하면 된다.

npx create-next-app 

실행해보면 , 일단 create-next-app 을 먼저 실치하고나서, create-next-app 을 실행하는 것을 볼수 있다.

Need to install the following packages:
create-next-app@14.2.5
Ok to proceed? (y) y
✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like to use `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to customize the default import alias (@/*)? … No / Yes
Creating a new Next.js app in /Users/..../...../my-app-test2.

Using npm.

Initializing project with template: app 


Installing dependencies:
- react
- react-dom
- next

Installing devDependencies:
- typescript
- @types/node
- @types/react
- @types/react-dom
- eslint
- eslint-config-next

이렇게 여러가지 질문은 받게 되는데, TypeScript,ESLint,Tailwind,src,App Router,alias 등 선택하면된다.
일일이 선택하는게 귀찮다면 아래처럼 커멘드라인에서 직접 적어도 된다.

npx create-next-app ./my-project-folder --ts --no-tailwind \
      --eslint --app --src-dir --import-alias '@/*'

코딩스타일 검사 라이브러리 설치 (Prettier)

코딩스타일을 통일시켜주는 Prettier를 설치한다.

npm install --save-dev prettier

설정에 대한 공식 설명서를 참고한다.
https://prettier.io/docs/en/options

아래는 샘플이다. prettier.config.mjs

/** @type {import("prettier").Config} */
const config = {
    tabWidth: 4,
    printWidth: 120,
    overrides: [
        {
            files: "*.{json,json5,html,yaml,yml}",
            options: {
                tabWidth: 2,
            },
        },
        {
            files: "*.md",
            options: {
                tabWidth: 2,
                trailingComma: "none",
            },
        },
    ],
};

export default config;

추가로 .prettierignore 파일을 만들고 설정한다.

/pnpm-lock.yaml 
/package-lock.json

검사와 자동수정은 아래의 두 명령어로 한다.

npx prettier --cache  --check  .
npx prettier --cache  --write  .

매번 입력하기 귀찮으니까 package.json 에 포맷검사와 수정을 할수있는 명령어를 추가해보자.
package.json

"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"plint-check": "prettier --cache --check .",
"plint-fix": "prettier --cache --write ."
},

ESLint 관련 추가 라이브라리

eslint 관련 추가 라이브러리를 설치하면 좋다.

eslint-plugin-simple-import-sort : import 자동 정렬
typescript-eslint : ESLintのTypeScript용 룰을 제공

npm install  --save-dev eslint-plugin-simple-import-sort  @typescript-eslint/parser @typescript-eslint/eslint-plugin  

설치된 플러그인을 사용하기 위해서 .eslintrc.json 파일을 아래처럼 수정한다.

{
  "extends": [
    "next/core-web-vitals",
    "prettier"
  ],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "project": true
  },
  "plugins": ["simple-import-sort", "@typescript-eslint"],
  "rules": {
    "simple-import-sort/imports": "error",
    "simple-import-sort/exports": "error",
    "@typescript-eslint/no-misused-promises": [
      "error",
      {
        "checksVoidReturn": false
      }
    ],
    "redos/no-vulnerable": "error"
  }
}

TypeScript 설정 변경

tsconfig.json에 typescript 설정을 확실히 추가해 준다.
tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "noImplicitOverride": true,
    "noImplicitReturns": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    ....
    }
 }

2024년 7월 13일 토요일

6월에 취득한 LPIC-1, 늦었지만 게시함.

 6월에 잠깐 쉬는 동안에 그동안 미뤄왔던 

AWS, AZURE 자격증과 LPIC 자격증을 취득했다.

Linux 는 오래전부터 접해 왔었지만 이번기회에 "아. 이런명령어도 있었지~" 라는 감상이다.





Professional Scrum Master I 취득.

현장에서 Agile 방식으로 개발하고 있어서 어렵지는 않았다.

다만, 스크럼 룰을 실제 현장에서는 명확하게 적용할건지 아니면 유연하게 적용할건지는 팀이나 결정권자의 권한에 맡기는게 좋을것 같다는 감상이다. 



2024년 7월 10일 수요일

Laravel : 바른 친구 php-cs-fixer

Laravel : 바른 친구 php-cs-fixer

바른 친구 php-cs-fixer 를 사용해보자.

각기 다른 팀원들이 자기만의 스타일로 일하는 경우가 있다. 같은 스타일로 코드품질을 높여보자.

php-cs-fixer 라는 php lint 툴이 있다. 설치도 쉽고 사용도 쉽고 내용은 디테일하다.

써보자.

개발때에만 사용해야 되므로 dev옵션을 꼭 붙이자.

composer require --dev friendsofphp/php-cs-fixer

여기에 설치된다.
./vendor/friendsofphp/php-cs-fixer/php-cs-fixer

사용법은 간단하다.
https://github.com/PHP-CS-Fixer/PHP-CS-Fixer?tab=readme-ov-file#installation

php-cs-fixer fix myfileeeees(or mydirecccctory)  

이렇게 하면 기본값으로 파일을 막 고쳐 주는데 고치는건 내가 하려고 할때는 --dry-run 을 붙이고 화면에 뭐가 잘못된건지 보고 싶을때는 --diff 를 붙인다.

php-cs-fixer fix myfileeeees(or mydirecccctory)   --dry-run --diff 

이렇게 하면 이렇단다.

Loaded config default.
Using cache file ".php-cs-fixer.cache".
 1/1 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%

   1) app/Http/Controllers/TotController.php (function_declaration, braces_position)
      ---------- begin diff ----------
--- /var/www/html/app/Http/Controllers/TotController.php
+++ /var/www/html/app/Http/Controllers/TotController.php
@@ -12,7 +12,8 @@
     protected $searchPresenter;
 
 
-    public function __construct(TotTopBannerPresenter $bannerPresenter, TotSearchPresenter $searchPresenter)    {
+    public function __construct(TotTopBannerPresenter $bannerPresenter, TotSearchPresenter $searchPresenter)
+    {
         $this->bannerPresenter = $bannerPresenter;
         $this->searchPresenter = $searchPresenter;
     }

      ----------- end diff -----------

괄호가 이상한 위치라는걸 잡았단다.

그럼, 괄호가 위치가 어떤 기준에서 볼때 잘못된 거냐고 판단하는거야? 라고 묻는다면. --config=.php-cs-fixer.dist.php 옵션을 붙이고 .php-cs-fixer.dist.php 파일을 생성해서 입맛에 맞는 규칙을 추가해 주면된다.
룰은 정말 다양하게 제공된다. if의 앞뒤 공백, 함수인자사이의 공백, array 와 [] 등등 있다.
여기서 찾아보자. https://mlocati.github.io/php-cs-fixer-configurator/#version:3.59

<?php

$finder = (new PhpCsFixer\Finder())
    ->in(__DIR__)
;

return (new PhpCsFixer\Config())
    ->setRules([
        '@PER-CS' => true,
        '@PHP82Migration' => true,
    ])
    ->setFinder($finder)
;

./vendor/friendsofphp/php-cs-fixer/php-cs-fixer fix ./app/Http/Controllers/TotController.php --diff --dry-run --verbose --config=.php-cs-fixer.dist.php

git 에 올릴때 pre-commit 으로 체크해서 오류가 나면 올리지 못하게 하자.

.git/hooks/pre-commit 파일을 만들거나 수정해 보자.

#!/bin/bash

  

# Stash staged changes

git  stash  -q  --keep-index

  

# Run PHP CS Fixer on staged PHP files

files=$(git  diff  --cached  --name-only  --diff-filter=ACM  "*.php")

if [ -n "$files" ]; then

echo  "Running php-cs-fixer..."

docker  exec  procjet-php-1  /var/www/html/vendor/friendsofphp/php-cs-fixer/php-cs-fixer  fix  --dry-run  --diff  --using-cache=no  $files

if [ $? -ne 0 ]; then

echo  "PHP CS Fixer found issues. Please fix them and try again."

git  stash  pop  -q

exit  1

fi

fi

  

# Restore staged changes

git  stash  pop  -q

“docker exec procjet-php-1 /var/www/html/vendor/friendsofphp/php-cs-fixer/php-cs-fixer” 이부분은 내가 docker를 쓰고 있어서 그런거다. 로컬에서 php 깔아서 한다면 필요없으미 삭제하면 된다.

git 으로 커밋해보면

> git -c user.useConfigOnly=true commit --quiet --allow-empty-message --file -
Running php-cs-fixer...
PHP CS Fixer 3.59.3 7th Gear by Fabien Potencier, Dariusz Ruminski and contributors.
PHP runtime: 7.4.1
Running analysis on 1 core sequentially.
You can enable parallel runner and speed up the analysis! Please see https://cs.symfony.com/doc/usage.html for more information.
Loaded config default.
 0/1 [░░░░░░░░░░░░░░░░░░░░░░░░░░░░]   0%
 1/1 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%

   1) app/Http/Controllers/TotController.php
      ---------- begin diff ----------
--- /var/www/html/app/Http/Controllers/TotController.php
+++ /var/www/html/app/Http/Controllers/TotController.php
@@ -12,7 +12,8 @@
     protected $searchPresenter;
 
 
-    public function __construct(TotTopBannerPresenter $bannerPresenter, TotSearchPresenter $searchPresenter)    {
+    public function __construct(TotTopBannerPresenter $bannerPresenter, TotSearchPresenter $searchPresenter)
+    {
         $this->bannerPresenter = $bannerPresenter;
         $this->searchPresenter = $searchPresenter;
     }

      ----------- end diff -----------


Found 1 of 1 files that can be fixed in 0.035 seconds, 20.00 MB memory used
PHP CS Fixer found issues. Please fix them and try again.

응 그래. 잘못된 파일은 올리면 안돼.

vscode 에서 사용하기

php cs fixerv0.3.13
junstyle

이라는 익스텐션을 다운받고, settings.json 에 아래 항목이 있다면 ${workspace}/vendor/bin/php-cs-fixer 를 지정하면된다.
그리고 나서 작업중인 파일에서 F1 을 누르고 php-cs-fixer:diff 혹은 php-cs-fixer:fix를 선택하면 된다.

"php-cs-fixer.executablePath": "${workspaceFolder}/docker-exex-php-cs-fixer.sh",

docker에 php를 실행하고 있다면

docker에 php를 실행하고 있다면 스크립트를 만들어서 스크립트에서 php의 도커에서 실행한다.

"php-cs-fixer.executablePath": "${workspaceFolder}/docker-exex-php-cs-fixer.sh",

스크립트는 아래와 같다.

#!/bin/bash 
# Docker 컨테이너 이름

CONTAINER_NAME="project-php-1"

# PHP-CS-Fixer 경로
PHP_CS_FIXER_PATH="/var/www/html/vendor/bin/php-cs-fixer"

# 컨테이너 내부의 프로젝트 경로
PROJECT_PATH="/var/www/html"

  

for  arg  in  "$@"; do
args+=("${arg/\/Users\/blabla\/pnm_docker\/procjet\/myweb/$PROJECT_PATH}")
done 

# 실행

docker  exec  procjet-php-1  $PHP_CS_FIXER_PATH  --diff  "${args[@]}"  --format=txt

2024년 7월 9일 화요일

Laravel : 내부용 공통모듈은 composer + github

Laravel : 내부용 공통모듈은 composer + github

Composer + Github Repo(private)

내부에서 여러 프로젝트를 동시에 진행하는데, 서비스가 비슷하다 보니 같은 로직을 반복사용할때가 있다. 이럴때 composer 로 관리하고 싶은데 내부용 프로젝트라 packagist.org에는 공개하고 싶지 않다.

Satis라는 툴이 있지만 간단하게 사용하고 싶었는데 github를 이용해서도 된다고 한다.

사용자 라이브러리 생성

  1. 아무 폴더가 가서 my-first-library 처럼 대충 폴더를 만든다.
  2. 폴더로 들어가서 composer init을 하면 여러가지 질문이 나오는데,
    이름 : vendor/package 식이다. ( mytest/my-first-library 처럼 입력한다.)
    require : 이 패키지가 의존하는 다른 패키지를 나열한다. 여러개의 모듈로 되어 있거나, 다른 패키지를 의존한다면 적어주자. (package-name: 버젼값) 형식이다.
    autoload : autoload로 앱구동시 라이브러리를 로딩하는 방법을 정의한다. namespace-prefix:directory 형식으로 namespace-prefix로 네임스페이스를 use선언하면 directory에서 파일을 찾는다는 의미이다
    (“MyLibrary\”:“src”) 라고 선언한다면 MyLibrary\MyClass는 해당패키지폴더의 src\MyClass.php을 찾는다
{
    "name": "mytest/my-first-library",
    "description": "A simple library",
    "type": "library",
    "require": {},
    "autoload": {
        "psr-4": {
            "MyLibrary\\": "src/"
        }
    }
}

  1. 위에서 src폴더를 지정했으니 src폴더를 만들고 간단한 소스를 만들어 보자.
    MyFirstLibrary.php 이다.
<?php

namespace MyLibrary;

class MyFirstLibrary {
    public function sayHello() {
        return "Hello, World!";
    }
}
  1. github에 현재 작업중인 폴더내용을 master에 올리자.

사용해보기

  1. 앞에서 github의 적당한 주소(https://github.com/my-repository-account/my-first-library)에 올렸으니 현재 프로젝트의 composer.json에 repository를 추가하자.
    변경될 부분은 아래와 같다.
{
....
    "require": {
....
        "mytest/my-first-library": "master"
    },
....
    "repositories": [
        {
            "type": "vcs",
            "url": "https://github.com/my-repository-account/my-first-library"
        }
    ]
}

2.Private Repository를 이용할것이기 때문에 접속시 Token이 필요하다. 프로젝트 폴더에서 auth.json 을 만들고 github의 개인프로필에서 개발용 token 을 생성하여 토큰값을 적어보자


{
    "github-oauth": {
        "github.com": "ghp_D12345678asdfgh"
    }
}
  1. 이제 composer insatll 혹은 composer update(실제 운영중인 프로젝트라면 update에 조심하자)를 하자
    아래 처럼 패키지가 추가되고 vendor/mytest/ 하위에 파일들이 설치된것을 볼수있다.
.....
  - Installing mytest/my-first-library (dev-master 010203c): Extracting archive
....

설치가 안되었다면 composer clear-cache 를 한에 다시 해보거나 composer install -vvv 로 로그를 확인해보자.

  1. 라라벨 controller나 기타 다른 곳에서 사용해보자.

use MyLibrary\MyFirstLibrary;

$lib = new MyFirstLibrary();
echo $lib->sayHello();  // 출력: Hello, World!

  1. 잘된다. 라이브러리 팀에서 라이브러리를 갱신하면 다시 composer update 하여 최신 버젼을 가져오도록 한다.
<?php

namespace MyLibrary;

class MyLibrary {
    public function sayHello() {
        return "Hello, World!";
    }
    public function toUpperCase(string $userData): string {
        return strtoupper($userData);
    }
}

composer update

  1. 사용할 곳에서 추가된 함수를 사용하면 된다.
$lib = new  MyLibrary();

echo  $lib->sayHello(); // 출력: Hello, World!

echo  $lib->toUpperCase("dx-ninja");

담엔 Satis 를 이용해서 전용 서버에서 패키지 관리가 될수있도록 해보자.

2024년 7월 8일 월요일

Laravel : Laravel8 + Vuejs2 + Clean Architecture

Laravel : Laravel8 + Vuejs2 + Clean Architecture

Laravel8 + Vuejs2 + Clean Architecture

1. 먼저 간단히 php 에서 전형적이고 심플한 형태로 제작해 보자.

라라벨 type hine 로 자동으로 객체주입이 가능하다. 놀랍다.

Entities - User.php
Repositories - UserRepository.php
UseCases -GetUserUseCase.php
폴더를 만들자.

각각의 파일들은 다음과 같을 것이다.

// app/Entities/User.php
namespace App\Entities;

class User {
    public $id;
    public $name;
    public $email;
    public function __construct($id, $name, $email) {
        $this->id = $id;
        $this->name = $name;
        $this->email = $email;
    }
}
// app/Repositories/UserRepository.php
namespace App\Repositories;

use App\Entities\User;

class UserRepository {
    public function find($id) {
        // 데이터베이스에서 사용자 찾기 로직
        return new User(1,"dxninja","dxninja@email.com");
    }

    public function save(User $user) {
        // 데이터베이스에 사용자 저장 로직
    }
}

// app/UseCases/GetUser.php
namespace App\GetUserUseCase;

use App\Repositories\UserRepository;

class GetUserUseCase {
    protected $userRepository;

    public function __construct(UserRepository $userRepository) {
        $this->userRepository = $userRepository;
    }

    public function execute($id) {
        return $this->userRepository->find($id);
    }
}

이제, Controller 에서 사용해보자

$user = $this->getUser->execute("userid");

Koin보다 간단하다. 역시나 막 사용하는 php !!

2. Service Provider 로 좀더 유연하고 클린하게 만들어보자.

위의 1에서는 간단하게 의존성 주입이 되었지만 직접개체를 주입하는 방식이라 유연하지 못했다. 좀더 클린하고 결합도를 낮추고 인터페이스 분리를 하기 위해서 인터페이스를 주입하여 객체의 변경과 확장에도 대응하도록 하자.

IUserRepository와 구현체를 만들자.

namespace App\Repositories;

use App\Entities\User;

interface IUserRepository {
    public function find($id): ?User;
    public function save(User $user): void;
}


namespace App\Repositories;

use App\Entities\User;

class UserRepository implements IUserRepository {
    public function find($id): ?User {
        // 데이터베이스에서 사용자 찾기 로직
        return new User(1, "dxninja", "dxninja@email.com");
    }

    public function save(User $user): void {
        // 데이터베이스에 사용자 저장 로직
    }
}

IUseCase와 구현체를 만들자.


namespace App\UseCases;

interface IUseCase {
    public function execute($id);
}


namespace App\UseCases;

use App\Repositories\IUserRepository;

class GetUserUseCase implements IUseCase {
    protected $userRepository;

    public function __construct(IUserRepository $userRepository) {
        $this->userRepository = $userRepository;
    }

    public function execute($id) {
        return $this->userRepository->find($id);
    }
}

Repository와 UseCase의 서비스 프로바이더 만들자.


namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Repositories\IUserRepository;
use App\Repositories\UserRepository;

class RepositoryServiceProvider extends ServiceProvider {
    public function register() {
        $this->app->singleton(IUserRepository::class, UserRepository::class);
    }

    또는 defer가능하도록.

     public function register() {
        $this->app->singleton(IUseCase::class, function ($app) {
            return new GetUserUseCase($app->make(IUserRepository::class));
        });
    }

    public function provides() {
        return [IUseCase::class];
    }
}

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\UseCases\IUseCase;
use App\UseCases\GetUserUseCase;
use App\Repositories\IUserRepository;

class UseCaseServiceProvider extends ServiceProvider {
    public function register() {
        $this->app->singleton(IUseCase::class, function ($app) {
            return new GetUserUseCase($app->make(IUserRepository::class));
        });
    }

    또는 defer가능하도록

    public function register() {
        $this->app->singleton(IUserRepository::class, UserRepository::class);
    }

    public function provides() {
        return [IUserRepository::class];
    }
}

서비스 프로바이더를 라라벨에 등록하자.


// config/app.php

'providers' => [
    // Other Service Providers

    App\Providers\RepositoryServiceProvider::class,
    App\Providers\UseCaseServiceProvider::class,
],

Controller에서 IUseCase를 주입받도록 하자.

namespace App\Http\Controllers;

use App\UseCases\IUseCase;
use Illuminate\Http\Request;

class UserController extends Controller {
    protected $getUser;

    public function __construct(IUseCase $getUser) {
        $this->getUser = $getUser;
    }

    public function show($id) {
        $user = $this->getUser->execute($id);
        return view('user.show', compact('user'));
    }
}

라우터가 아래처럼 되어있다고 치자.


use App\Http\Controllers\UserController;

Route::get('/users/{id}', [UserController::class, 'show']);

내친김에 테스트 코드도 만들자

namespace Tests\Unit\UseCases;

use Tests\TestCase;
use App\UseCases\GetUserUseCase;
use App\Repositories\IUserRepository;
use App\Entities\User;
use Mockery;

class GetUserUseCaseTest extends TestCase {
    public function testExecute() {
        $userRepositoryMock = Mockery::mock(IUserRepository::class);
        $userRepositoryMock->shouldReceive('find')
            ->with(1)
            ->andReturn(new User(1, 'dxninja', 'dxninja@email.com'));

        $getUserUseCase = new GetUserUseCase($userRepositoryMock);
        $user = $getUserUseCase->execute(1);

        $this->assertInstanceOf(User::class, $user);
        $this->assertEquals(1, $user->id);
        $this->assertEquals('dxninja', $user->name);
        $this->assertEquals('dxninja@email.com', $user->email);
    }

    protected function tearDown(): void {
        Mockery::close();
        parent::tearDown();
    }
}



namespace Tests\Feature\Http\Controllers;

use Tests\TestCase;
use App\Http\Controllers\UserController;
use App\UseCases\IUseCase;
use App\Entities\User;
use Mockery;

class UserControllerTest extends TestCase {
    public function testShow() {
        $user = new User(1, 'dxninja', 'dxninja@email.com');
        
        $useCaseMock = Mockery::mock(IUseCase::class);
        $useCaseMock->shouldReceive('execute')
            ->with(1)
            ->andReturn($user);

        $controller = new UserController($useCaseMock);
        $response = $controller->show(1);

        $this->assertEquals('user.show', $response->name());
        $this->assertEquals($user, $response->getData()['user']);
    }

    protected function tearDown(): void {
        Mockery::close();
        parent::tearDown();
    }
}

namespace Tests\Feature\Api;

use Tests\TestCase;
use App\Entities\User;
use Mockery;
use App\UseCases\IUseCase;

class UserApiTest extends TestCase {
    public function testGetUser() {
        $user = new User(1, 'dxninja', 'dxninja@email.com');

        $useCaseMock = Mockery::mock(IUseCase::class);
        $useCaseMock->shouldReceive('execute')
            ->with(1)
            ->andReturn($user);

        $this->app->instance(IUseCase::class, $useCaseMock);

        $response = $this->getJson('/api/users/1');

        $response->assertStatus(200)
                 ->assertJson([
                     'id' => $user->id,
                     'name' => $user->name,
                     'email' => $user->email,
                 ]);
    }

    protected function tearDown(): void {
        Mockery::close();
        parent::tearDown();
    }
}

3. Vuejs에서도 구현하자. (개선중)

src/
├── components/
│   └── UserComponent.vue
├── entities/
│   └── User.js
├── repositories/
│   ├── IUserRepository.js
│   └── UserRepository.js
├── usecases/
│   └── GetUserUseCase.js
├── services/
│   └── container.js
├── views/
│   └── UserView.vue
└── main.js

// container.js inversify 

import { Container } from "inversify";
import "reflect-metadata";
import IUserRepository from '../repositories/IUserRepository';
import UserRepository from '../repositories/UserRepository';
import GetUserUseCase from '../usecases/GetUserUseCase';

const container = new Container();
container.bind(IUserRepository).to(UserRepository);
container.bind(GetUserUseCase).toSelf();

export default container;

// Entity

export default class User {
    constructor(id, name, email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }
}



// Repository

export default class IUserRepository {
    find(id) {
        throw new Error("Method not implemented.");
    }
}

import IUserRepository from './IUserRepository';
import User from '../entities/User';

export default class UserRepository extends IUserRepository {
    find(id) {
        // 여기서 실제 데이터베이스 호출을 구현합니다.
        return new User(1, "dxninja", "dxninja@email.com");
    }
}

// UseCase

export default class GetUserUseCase {
    constructor(userRepository) {
        this.userRepository = userRepository;
    }

    execute(id) {
        return this.userRepository.find(id);
    }
}

// Presenter

<template>
  <div>
    <p>User ID: {{ user.id }}</p>
    <p>User Name: {{ user.name }}</p>
    <p>User Email: {{ user.email }}</p>
  </div>
</template>

<script>
import { inject } from 'inversify-vue';
import GetUserUseCase from '../usecases/GetUserUseCase';

export default {
  name: 'UserComponent',
  data() {
    return {
      user: {}
    };
  },
  async created() {
    const getUserUseCase = inject(GetUserUseCase);
    this.user = await getUserUseCase.execute(1);
  }
};
</script>


// View

<template>
  <div>
    <UserComponent />
  </div>
</template>

<script>
import UserComponent from '../components/UserComponent.vue';

export default {
  name: 'UserView',
  components: {
    UserComponent
  }
};
</script>


// main.js

import Vue from 'vue';
import App from './App.vue';
import container from './services/container';
import { Provider } from 'inversify-vue';

Vue.config.productionTip = false;

new Vue({
  render: h => h(App),
  provide: {
    container
  }
}).$mount('#app');



// testcode


import 'reflect-metadata';
import { Container } from 'inversify';
import IUserRepository from '../../src/repositories/IUserRepository';
import UserRepository from '../../src/repositories/UserRepository';
import GetUserUseCase from '../../src/usecases/GetUserUseCase';
import User from '../../src/entities/User';

test('GetUserUseCase executes correctly', () => {
  const container = new Container();
  container.bind(IUserRepository).to(UserRepository);
  container.bind(GetUserUseCase).toSelf();

  const getUserUseCase = container.get(GetUserUseCase);
  const user = getUserUseCase.execute(1);

  expect(user).toBeInstanceOf(User);
  expect(user.id).toBe(1);
  expect(user.name).toBe("dxninja");
  expect(user.email).toBe("dxninja@email.com");
});