2024년 7월 7일 일요일

vuejs simple clean architecture -now is testing...-

 



### 1. 프로젝트 설정


먼저 프로젝트 디렉토리를 생성하고 필요한 패키지를 설치


```bash

mkdir vue-clean-architecture

cd vue-clean-architecture

npm init -y

npm install vue vuex inversify reflect-metadata jest @vue/test-utils vue-jest

```


### 2. 프로젝트 구조


다음과 같은 프로젝트 구조 생성


```

vue-clean-architecture/

├── src/

│   ├── components/

│   │   └── MyComponent.vue

│   ├── entities/

│   │   └── User.js

│   ├── repositories/

│   │   └── UserRepository.js

│   ├── usecases/

│   │   └── GetUserUseCase.js

│   ├── container.js

│   ├── main.js

├── tests/

│   ├── repositories/

│   │   └── UserRepository.spec.js

│   ├── usecases/

│   │   └── GetUserUseCase.spec.js

│   └── MyComponent.spec.js

├── package.json

└── jest.config.js

```


### 3. 코드 작성


#### `src/entities/User.js`


```javascript

class User {

  constructor(id, name) {

    this.id = id;

    this.name = name;

  }

}


module.exports = User;

```


#### `src/repositories/UserRepository.js`


```javascript

const { injectable } = require('inversify');

const User = require('../entities/User');


@injectable()

class UserRepository {

  getUserById(id) {

    return new User(id, 'John Doe');

  }

}


module.exports = UserRepository;

```


#### `src/usecases/GetUserUseCase.js`


```javascript

const { injectable, inject } = require('inversify');

const UserRepository = require('../repositories/UserRepository');


@injectable()

class GetUserUseCase {

  constructor(@inject(UserRepository) userRepository) {

    this.userRepository = userRepository;

  }


  execute(id) {

    return this.userRepository.getUserById(id);

  }

}


module.exports = GetUserUseCase;

```


#### `src/container.js`


```javascript

require('reflect-metadata');

const { Container } = require('inversify');

const UserRepository = require('./repositories/UserRepository');

const GetUserUseCase = require('./usecases/GetUserUseCase');


const container = new Container();

container.bind(UserRepository).to(UserRepository);

container.bind(GetUserUseCase).to(GetUserUseCase);


module.exports = container;

```


#### `src/components/MyComponent.vue`


```vue

<template>

  <div>

    <p>{{ user.name }}</p>

  </div>

</template>


<script>

import { container } from '../container';

import GetUserUseCase from '../usecases/GetUserUseCase';

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


export default {

  data() {

    return {

      user: new User(0, ''),

    };

  },

  created() {

    const getUserUseCase = container.get(GetUserUseCase);

    this.user = getUserUseCase.execute(1);

  },

};

</script>

```


### 4. Jest 설정 및 테스트 작성


#### `jest.config.js`


```javascript

module.exports = {

  preset: '@vue/cli-plugin-unit-jest',

  moduleFileExtensions: ['js', 'jsx', 'json', 'vue'],

  transform: {

    '^.+\\.vue$': 'vue-jest',

    '^.+\\.(js|jsx)$': 'babel-jest',

  },

  moduleNameMapper: {

    '^@/(.*)$': '<rootDir>/src/$1',

  },

};

```




### 2. 테스트 코드 작성


#### `tests/repositories/UserRepository.spec.js`


```javascript

const { container } = require('../../src/container');

const UserRepository = require('../../src/repositories/UserRepository');


describe('UserRepository', () => {

  it('should return a user by id', () => {

    const userRepository = container.get(UserRepository);

    const user = userRepository.getUserById(1);

    expect(user.id).toBe(1);

    expect(user.name).toBe('John Doe');

  });

});

```


#### `tests/usecases/GetUserUseCase.spec.js`


```javascript

const { container } = require('../../src/container');

const GetUserUseCase = require('../../src/usecases/GetUserUseCase');


describe('GetUserUseCase', () => {

  it('should return a user by id', () => {

    const getUserUseCase = container.get(GetUserUseCase);

    const user = getUserUseCase.execute(1);

    expect(user.id).toBe(1);

    expect(user.name).toBe('John Doe');

  });

});

```


#### `tests/MyComponent.spec.js` (이미 작성된 코드에서 변경 없음)


```javascript

import { shallowMount } from '@vue/test-utils';

import MyComponent from '@/components/MyComponent.vue';

import { container } from '@/container';

import GetUserUseCase from '@/usecases/GetUserUseCase';

import User from '@/entities/User';


test('displays user name', () => {

  const getUserUseCase = container.get(GetUserUseCase);

  jest.spyOn(getUserUseCase, 'execute').mockReturnValue(new User(1, 'John Doe'));


  const wrapper = shallowMount(MyComponent);

  expect(wrapper.text()).toContain('John Doe');

});

```


### 3. 테스트 실행


```bash

npm run test

```

2024년 7월 2일 화요일

Laravel : 정적코드분석 PHPSTAN

Laravel : 정적코드분석 PHPSTAN

정적코드 분석 라이브러리 PHPSTAN ( LaraStan)

Java나 C# 같은 컴파일언어들은 정의안된 함수를 호출하거나 잘못된 수의 인수를 함수에 전달하는 등의 오류를 차컴파일단계에서 찾아준다. 하지만 php 같은 인터프리터 언어에서는 실제로 서비스를 돌려보기 전까지는 어디에서 에러가 나올지 예측할수없다.

이러한 문제점을 해결하고자 정적분석툴이 존재하는데 Laravel에서는 phpstan 을 Laravel에 맞게 수정한 larastan을 따로 제공한다.

정적분석툴을 이용해서 프로그램이 어디서 언제 에러를 뱉어낼지 두려워 하지 않아도 될지도 모르겠다. ( 그렇다고 100% 믿지는 말라. 어디까지나 프로그램입장에서 코드를 분석하는 자동툴이다)

설치는 composer로 간단히 할수 있다. 실제 서비스인 production 환경에서는 사용하지 않도록 --dev 를 이용해 설치하자.

composer require nunomaduro/larastan --dev

설치가 되었다면 neon yaml 이라는 파일을 작성하자.

includes:
    - ./vendor/nunomaduro/larastan/extension.neon
parameters:
    paths:
        - app
    level: 9
    ignoreErrors:
    excludePaths:

첫 번째 옵션을 사용하면 pathsphpstan이 확인할 위치를 정의할 수 있다. 이 경우에는 app애플리케이션 코드가 있는 디렉터리만 검사한다. 원하는 경우 더 많은 영역으로 확장할 수 있지만 상황이 엄격해질 수 있으므로 포함하는 내용에 주의해야한다.
다음으로 levelPHPStan에는 확인할 수준이 다르며 0이 가장 낮고 9가 현재 가장 높다. 조금씩 높여서 9에서도 문제없는 코드를 만들어가자.

./vendor/bin/phpstan analyse
기본 Laravel 애플리케이션에서 얻은 출력은 다음과 같다.

Note: Using configuration file /Users/steve/code/sites/larastan-test/phpstan.neon.
 18/18 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
 ------ ----------------------------------------------------------------------------------------------------------------------------
  Line   Providers/RouteServiceProvider.php
 ------ ----------------------------------------------------------------------------------------------------------------------------
  49     Parameter #1 $key of method Illuminate\Cache\RateLimiting\Limit::by() expects string, int<min, -1>|int<1, max>|string|null
         given.
 ------ ----------------------------------------------------------------------------------------------------------------------------
 [ERROR] Found 1 error

보다시피, 엄격도가 가장 높게 설정되어 있음에도 불구하고 기본 Laravel 애플리케이션에서는 단 하나의 오류만 발생했다. 훌륭한 결과다.

이를 편리하게 실행하려면 작곡가 파일에 스크립트를 추가하여 이 명령을 실행할 수 있다. 이제 이 명령을 더 쉽게 실행할 수 있도록 이 스크립트를 파일에 추가한다.

composer.json

"scripts": {
  "phpstan": [
    "./vendor/bin/phpstan analyse"
  ]
},
"scripts-descriptions": {
  "phpstan": "Run PHPStan static analysis against your application."
},

이제 composer phpstan 라고만 치면된다.

위의 분석 결과에서 지적당했던 내용을 보자.

protected function configureRateLimiting()
{
    RateLimiter::for('api', function (Request $request) {
        return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
    });
}

$request->user()?->id ?: $request->ip()

이 부분에서 정적분석툴이 문제가 있다고 한다. 그런데 애매하게도 이부분은 외부에서 가져오는 거라 이렇게 써야만 헀다.
때문에 HPStan 에게 해당 라인을 무시하라고 해야 하는데 아래와 같다.

protected function configureRateLimiting(): void
{
    RateLimiter::for('api', static function (Request $request): Limit {
        /** @phpstan-ignore-next-line  */
        return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
    });
}

다시 실행하면 문제 없다고 나온다. 이제 적어도 웹사이트나 api 도중에 500 에러는 안 만날것이다.

Note: Using configuration file /Users/steve/code/sites/larastan-test/phpstan.neon.
 18/18 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
 [OK] No errors

artisan을 사용하여 모델을 추가하고 CLI 플래그를 사용하여 마이그레이션과 팩토리를 모두 생성하는 예제도 보자.

php artisan make:model Bookmark -mf

마이그레이션은 다음과 같다.

Schema::create('bookmarks', static function (Blueprint $table): void {
    $table->id();
    $table->string('name');
    $table->string('url');
    $table->boolean('starred')->default(false);
    $table->foreignId('user_id')->index()->constrained()->cascadeOnDelete();
    $table->timestamps();
});

이제 이것을 모델에 추가한다.

class Bookmark extends Model
{
    use HasFactory;
    protected $fillable = [
        'name',
        'url',
        'starred',
        'user_id',
    ];
    protected $casts = [
        'starred' => 'boolean',
    ];
    /**
     * @return BelongsTo
     */
    public function user(): BelongsTo
    {
        return $this->belongsTo(
            related: User::class,
            foreignKey: 'user_id',
        );
    }

}

그렇지만 위처럼 배열을 대충 사용하면 레벨 9의 phpstan 은 화를 낸다. 그래서 아래와 같이 코멘트를 추가한다.

class Bookmark extends Model
{
    use HasFactory;
    /**
     * @var array<int,string>
     */
    protected $fillable = [
        'name',
        'url',
        'starred',
        'user_id',
    ];
    /**
     * @var array<string,string>
     */
    protected $casts = [
        'starred' => 'boolean',
    ];
    /**
     * @return BelongsTo
     */
    public function user(): BelongsTo
    {
        return $this->belongsTo(
            related: User::class,
            foreignKey: 'user_id',
        );
    }
}

이렇게 하면 phpstan 은 화를 내지 않는다.

자 이제, controller 를 만들자

php artisan make:controller Bookmarks/IndexController --invokable

라우터도 만들자.

Route::middleware(['auth'])->prefix('bookmarks')->as('bookmarks:')->group(static function (): void {
    Route::get('/', App\Http\Controllers\Bookmarks\IndexController::class)->name('index');
});

그냥 그대로 phpstan 을 돌려보면,

composer phpstan


Note: Using configuration file /Users/steve/code/sites/larastan-test/phpstan.neon.
 20/20 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
 ------ -------------------------------------------------------------------------------------------------
  Line   Http/Controllers/Bookmarks/IndexController.php
 ------ -------------------------------------------------------------------------------------------------
  15     Method App\Http\Controllers\Bookmarks\IndexController::__invoke() has no return type specified.
 ------ -------------------------------------------------------------------------------------------------
 ------ -----------------------------------------------------------------------------------------------------------------------------
  Line   Models/Bookmark.php
 ------ -----------------------------------------------------------------------------------------------------------------------------
  33     Method App\Models\Bookmark::user() return type with generic class Illuminate\Database\Eloquent\Relations\BelongsTo does not
         specify its types: TRelatedModel, TChildModel
         💡 You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your
         phpstan.neon.
 ------ -----------------------------------------------------------------------------------------------------------------------------
 ------ ----------------------------------------------------------------------------------------------------------------------------
  Line   Models/User.php
 ------ ----------------------------------------------------------------------------------------------------------------------------
  49     Method App\Models\User::bookmarks() return type with generic class Illuminate\Database\Eloquent\Relations\HasMany does not
         specify its types: TRelatedModel
         💡 You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your
         phpstan.neon.
 ------ ----------------------------------------------------------------------------------------------------------------------------
 [ERROR] Found 3 errors

눈에 띄는 첫 번째 문제는 Method App\Models\User::bookmarks() return type with generic class오류다. 우리는 Generic 에 의존하지 않으니까 지시대로 phpstan.neon파일의 checkGenericClassInNonGenericObjectType 에 false 로 해주자.

includes:
    - ./vendor/nunomaduro/larastan/extension.neon
parameters:
    paths:
        - app
    level: 9
    ignoreErrors:
    excludePaths:
    checkGenericClassInNonGenericObjectType: false

IndexController 의 경고를 보자. __invoke 에 return 이 없는거하고 request에서 id 가 null 올수 있다고 한다.

class IndexController extends Controller
{
    public function __invoke(Request $request)
    {
        return View::make(
            view: 'bookmarks.list',
            data: [
                'bookmarks' => Bookmark::query()
                    ->where('user_id', $request->user()->id)
                    ->paginate(),
            ]
        );
    }
}

------ -------------------------------------------------------------------------------------------------
  Line   Http/Controllers/Bookmarks/IndexController.php
 ------ -------------------------------------------------------------------------------------------------
  15     Method App\Http\Controllers\Bookmarks\IndexController::__invoke() has no return type specified.
  21     Cannot access property $id on App\Models\User|null.
 ------ -------------------------------------------------------------------------------------------------

아래와 같이 바꾸자.

public function __invoke(Request $request): \Illuminate\Contracts\View\View

다음으로 Cannot access property $id on App\Models\User|null.은 요청하는 사용자의 ID가 비어 있을 수 있는 기본 Laravel 애플리케이션의 것과 유사하다. 따라서 이 문제를 해결하는 방법은 인증 도우미 기능을 사용하여 인증 가드로부터 직접 ID를 얻는 것이다.
쿼리를 바꿔보자

Bookmark::query()
    ->where('user_id', auth()->id())
    ->paginate()

인증 ID 방법을 사용하면 요청의 ID가 비어 있을 수 있으므로 요청에서 ID를 가져오는 대신 인증 보호에서 직접 ID를 가져온다.
이 작업을 수행할 때 주의할 점은 경로가 인증 미들웨어로 래핑되지 않은 경우 id 메서드가 속성 ID null을 가져오려고 한다고 불평한다는 거다. 따라서 이 경로에 대한 미들웨어를 설정했는지 확인해야한다. 어쨌든 정적분석툴 입장에서는 외부에서 들어오는 request 의 id 값을 그대로 사용하지 않는 다고 생각해서 에러라고 생각 하지 않게된다.

이제 정적 분석을 다시 실행하면 이러한 오류가 제거되었을 것이다.

composer phpstan
Note: Using configuration file /Users/steve/code/sites/larastan-test/phpstan.neon.
 20/20 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
 [OK] No errors

이제 IndexController오류가 없다.

java라면 intellij 나 vscode(java인데 vscode를?) 에서 nullable 정도는 쉽게 잡아줄텐데 php 에서는 phpstan 을 이용하여야 하니 조금 불편하고 마치 잔소리 하는 우리의 누구네 처럼 귀찮게 들리지만 결국 코드 품질이 올라가고 안정된 소프트웨어가 되었다는 것은 큰 가치가 있다고 생각한다.

프로젝트 초기단게라면 어서 빨리 도입하자.

참고로 , SonarQube 라는 유료지만 좋은 툴도 있다.

2024년 7월 1일 월요일

Vuejs 컴퍼넌트 구성간의 적절한 통신 방법

Vuejs 컴퍼넌트 구성간의 적절한 통신 방법

The proper methods for communication between vuejs components

In Vue.js (version 2), there are several methods for communication between components.
like this,

  • props
  • emit/on : on, off, once are removed in Vue 3.0
  • children/parent : children is removed in Vue 3.0
  • attrs/listeners : listeners is removed in Vue 3.0
  • ref
  • root
  • eventbus or (provide/ inject)
  • vuex

therefore, the proper usage are as follws.

  • parent-child components

    • props/ $emit/ $parent/ ref/$attrs
  • Sibling components (components at the same level)

    • $parent/ $root/ eventbus/vuex
  • Cross-level relationships (multiple hierarchy or complex structures)"

    • eventbus/ vuex/ provide+inject

It’s important to consider both two-component and one-to-many component structures when using these methods.

Vuejs(v2) 에서 컴퍼넌트 구성간의 적절한 통신 방법

vuejs (버젼2) 에서는 컴퍼넌트 간의 통신 방법은 아래와 같이 여러가지가 존재한다.

  • props
  • emit/on : on, off, once 는 Vue 3.0 에서 제거됨.
  • children/parent : children 은 Vue 3.0 에서 제거됨.
  • attrs/listeners : listeners 는 Vue 3.0 에서 제거됨.
  • ref
  • root
  • eventbus or (provide/ inject)
  • vuex

이것들의 적절한 사용은 아래와 같다.

  • 상위-하위 구성요소 ( 부모와 자식)

    • props/ $emit/ $parent/ ref/$attrs
  • 형제 구성요소 (동등한 깊이의 형제)

    • $parent/ $root/ eventbus/vuex
  • 교차 수준 관계 (다중자식 구조나 복잡한 구조)

    • eventbus/ vuex/ provide+inject

이렇게 두 컴퍼넌트 혹은 1 대 다수의 컴퍼넌트 구조를 고려하며 사용하는게 중요하다.

2024년 6월 28일 금요일

Vuejs Provide와Inject 로 컴퍼넌트간 소

Vuejs Provide와Inject 로 컴퍼넌트간 소

Vue.js 제공 및 주입: 구성 요소 간 통신을 위한 고급 방법

소개

Vue.js는 구성 요소 간 통신을 위한 다양한 방법을 제공하는 인기 있는 프런트 엔드 프레임워크이다. 이 중 provide 및 Inject은 prop이나 eventemit를 통해 수동으로 데이터를 전달할 필요 없이 중첩된 구성 요소 트리에 걸쳐 데이터를 전달할 수 있는 한 쌍의 고급 옵션이다.

provide은 ( inject는) 무엇입니까 ?

provide 및 Inject은 구성 요소 간의 종속성 주입을 구현하기 위해 Vue.js 2.2.0+에 도입된 한 쌍의 옵션이다. provide을 사용하면 하위 구성 요소가 얼마나 깊이 중첩되어 있는지에 관계없이 상위 구성 요소에 데이터를 제공하고 Inject을 통해 하위 구성 요소의 해당 데이터에 액세스할 수 있다.

provide그리고 inject의 원리

provide의 원칙 Vue.js 구성 요소 인스턴스의 상위-하위 관계를 기반으로 한다. 구성 요소가 provide를 통해 데이터를 제공하면 해당 데이터가 상위 구성 요소의 개체에 추가된다 provide. 그러면 하위 구성요소는 inject옵션을 통해 이 데이터를 얻을 수 있다.

provide 및 inject 원칙은 Vue.js 구성 요소 인스턴스의 상위-하위 관계를 기반으로 한다. 구성 요소가 제공을 통해 데이터를 제공하면 해당 데이터가 상위 구성 요소의 제공 개체에 추가된다. 그러면 하위 구성 요소는 주입 옵션을 통해 이 데이터를 얻을 수 있다.

예제를 통해 작업을 제공하고 주입하는 방법을 자세히 살펴보겠습니다.

<template>
 <div>
 <p>부모 데이터:{{ ancestorData }}</p>
 <child-component></child-component>
 </div>
</template>
<script>
import ChildComponent from './ChildComponent';
export default {
 provide: {
 ancestorData: '이것은 상위 구성요소의 데이터이다.'
 },
 components: {
 ChildComponent
 }
};
</script>

위의 예에서 조상 구성 요소는 provide 옵션을 통해 조상 데이터라는 데이터를 제공한다. 하위 구성 요소 ChildComponent는 inject 옵션을 통해 이 데이터를 얻을 수 있다.

<template>
 <div>
 <p>자식컴퍼넌트:{{ injectedData }}</p>
 </div>
</template>
<script>
export default {
 inject: ['ancestorData'],
 data() {
 return {
 injectedData: this.ancestorData
 };
 }
};
</script>

하위 구성 요소에서는 주입 옵션을 사용하여 액세스하려는 데이터를 선언한 다음 데이터에 주입된 데이터로 저장한다.

provide 및 inject의 사용법

provide 및 inject은 주로 다음과 같은 상황에서 사용된다.

  1. 전역 구성: 루트 구성 요소에서 제공 옵션을 사용하여 전역 구성 정보를 제공한 다음 삽입을 사용하여 모든 구성 요소에서 이 정보에 액세스할 수 있다.

  2. 교차 레벨 통신: provide 및 inject을 통해 소품이나 이벤트를 통해 수동으로 데이터를 전달할 필요 없이 구성 요소 트리를 통해 데이터를 전달할 수 있다.

  3. 공유 인스턴스: 상위 구성 요소에서 공유 인스턴스를 만든 다음 이 인스턴스를 하위 구성 요소에서 공유할 수 있다.

  4. 플러그인 개발: 플러그인 가능한 플러그인이나 라이브러리를 개발하는 경우 구성이나 서비스를 쉽게 공유할 수 있는 방법을 제공하고 주입한다.

provide및 inject의 고급 사용법

기본값

데이터가 제공되지 않는 경우 주입에 대한 기본값을 제공할 수 있다. 이는 개체의 제공 속성을 래핑하고 기본 속성을 설정하여 달성할 수 있다.

<template>
 <div>
 <p>자식컴퍼넌트:{{ injectedData }}</p>
 </div>
</template>
<script>
export default {
 inject: {
 ancestorData: { default: '기본 데이터' }
 },
 data() {
 return {
 injectedData: this.ancestorData
 };
 }
};
</script>

위의 예에서 상위 구성 요소가 데이터를 제공하지 않으면 ancestorData 하위 구성 요소는 기본값을 사용한다. .

동적 provide

동적 제공 제공 옵션은 데이터를 동적으로 제공할 수 있는 기능을 허용할 수 있다. 이는 사용자의 로그인 상태에 따라 다양한 사용자 데이터를 제공하는 등 특정 시나리오에서 유용하다.

<template>
 <div>
 <p>사용자 데이터:{{ userData }}</p>
 </div>
</template>
<script>
export default {
 provide() {
 return {
 userData:js
 this.loggedIn ? this.user : null
 };
 },
 data() {
 return {
 loggedIn: true,
 user: {
 name: 'ninja_zero',
 email: 'ninja_zero@dx-ninja.com'
 }
 };
 }
};
</script>

위의 예에서 제공 옵션은 userData 속성이 포함된 객체를 반환한다. 사용자가 로그인한 경우 userData 속성에는 사용자 데이터가 포함된다. 그렇지 않으면 null이 된다.

동적 inject

inject 옵션은 함수를 허용하여 동적으로 데이터를 얻을 수도 있다. 이는 조건에 따라 어떤 데이터를 주입할지 결정해야 할 때 유용하다.
코드 복사

<template>
 <div>
 <p>사용자 데이터:{{ userData }}</p>
 </div>
</template>
<script>
export default {
 inject: ['user'],
 computed: {
 userData() {
 if (this.user) {
 return `이름:${this.user.name}, 메일:${this.user.email}`;
 } else {
 return '미등록';
 }
 }
 }
};
</script>

위의 예에서는 inject를 사용하여 사용자 데이터를 주입한다. 그런 다음 계산된 속성 userData를 사용하여 사용자가 로그인했는지 여부에 따라 사용자 데이터를 동적으로 생성한다.

사용 시나리오

  1. 여러 구성 요소가 일부 공통 데이터나 상태를 공유해야 하는 경우 Provide 및 inject를 사용하여 이 데이터를 공유할 수 있다. 최상위 컴포넌트에 데이터를 제공한 후 주입을 통해 모든 하위 컴포넌트에서 사용한다. 이러한 방식으로 데이터를 편리한 방식으로 공유할 수 있으며 구성 요소 간에 데이터가 여러 번 전송되는 것을 방지할 수 있다. 플러그인 또는 타사 라이브러리 통합: 제공 및 주입은 일부 플러그인이나 타사 라이브러리의 기능을 구성 요소에 캡슐화하고 다른 구성 요소가 해당 기능을 공유하도록 해야 할 때 사용할 수 있다.
  2. 크로스레벨 구성 요소 통신: 구성 요소 계층 구조가 깊고 서로 다른 수준의 구성 요소 간에 데이터를 전송해야 하는 경우 제공 및 주입을 사용할 수 있다.
    상위 구성 요소는 제공을 통해 데이터를 제공하고 하위 구성 요소는 주입 주입을 사용한다. 이러한 방식으로 레이어별로 소품을 전달하거나 통신을 위해 이벤트 버스를 사용하는 것을 피할 수 있다.
  3. 플러그인 또는 타사 라이브러리 통합 :일부 플러그인이나 타사 라이브러리의 기능을 구성 요소로 캡슐화하고 다른 구성 요소가 해당 기능을 공유하도록 해야 할 때 provide및 inject 를 사용할 수 있다.

단점

데이터의 소스는 추적할 수 없다 . 모든 수준에서 액세스할 수 있으므로 데이터 추적이 어렵습니다. 어느 레이어에서 이를 선언하는지, 어느 레이어에서 이를 사용하는지 알 수 없다.
구성 요소 간 결합을 유발한다 . 구성 요소 간 결합을 유발하여 구성 요소 재사용성에 영향을 줍니다.

요약

provide및 inject은 Vue.js에서 구성 요소 간의 여러 계층에 걸쳐 데이터를 전달하는 고급 방법이다. 이는 글로벌 구성, 교차 레벨 통신, 공유 인스턴스 및 플러그인 개발과 같은 다양한 시나리오에 적합한다. 제공 및 주입의 원리와 사용법을 깊이 이해하면 Vue.js의 기능을 더 잘 활용하고 보다 대화형이고 반응성이 뛰어난 프런트 엔드 애플리케이션을 구축하는 데 도움이 된다.

추가로 컴퍼넌트간 통신하는 방법이 더 있는데 간단히 소개해본다.

방법 1. props/$emit : 부모-자식이 끈끈하게 연결되어 있는구조로서 다층복합으로 구성된 경우에는 prop과 emit 지옥이 펼쳐진다.
방법 2, emit/emit/on : 한쪽이 emit과 함께 데이터를 던지면 on 으로 받는쪽에서 받은 자료를 처리하는구조이다. emit과 on 의 개수가 많아지면 관리가 어렵다.
방법 3. vuex : vue에서 전역 혹은 모듈별로 저장소를 통해서 데이터 전달이 가능하다. dispatch 와 commit, getter 로 관리되며, 별도의 저장소를 통해 데이터를 관리하기 때문에 단순히 부모자식 이 일대일의 간단한 구조에서 컴퍼넌트간에 데이터를 전달할때는 다소 과잉된 방법이기도 하다.
방법 4. attrs/attrs/listeners : Vue2.4에 등장한 속성-리스너 방식으로 여러 대상에 대한 데이터전달할경우에 유용하다.
방법 5, provide/inject : 본 글에서 설명한 내용이다.
방법 6, $parent/ $children그리고ref : 구성요소를 직접 참고하는 것으로 깊은 커플링으로 인해 사용을권하지 않는다.
방법 7, v-model : v-model은 Vue.js에서 제공하는 Syntactic Sugar를 기반으로 한 컴포넌트 통신 방법이며, v-model이 지정된 컴퍼넌트는 v-model에 정의된 값을 동시에 변경가능한다.

2024년 6월 26일 수요일

Vuejs 테스트코드작성

Vuejs 테스트코드작성

Vue.js 에서 테스트 코드를 작성해보자.(jest)

– 작성중 … –
Vue 는 html 화면을 구성시켜주는 프레임워크이다. 따라서 화면을 구성하는 각 컴퍼넌트들을 어떻게 테스트할지에 대해 고민하게 되는데 다행히 test-util 이라는 것을 제공해 주어 화면에 구성요소 확인, 클릭, 함수 실행 이벤트수행여부 등을 시뮬레이션하여 테스트 해 볼수 있게 해준다.

테스트 라이브러리 설치는 어렵지 않다.
먼저 javascript 테스트 프로그램인 jest 를 설치하고, jest를 vue에서 연동해서 컴퍼넌트등을 좀더 쉽게 테스트 할수 있도록 해주는 vue-jest, test-utils 를 설치하면된다.

테스트 라이브러리 설치

Vue CLI 로 Vue2나 Vue3 을 설치했다면 test 관련 모듈이 설치가 안되어있을수 있으므로 아래의 라이브러리를 추가로 설치하자.

설명

@vue/cli-plugin-unit-jest@^4.5.14 : Vue CLI에서 제공하는 Jest 플러그인입니다. 이 플러그인을 사용하면 Vue.js 프로젝트에서 Jest를 통해 유닛 테스트를 쉽게 설정하고 실행할 수 있다. 의존하는 관련 모듈인 jest가 같이 설치된다.

@vue/test-utils@1.3.6 : Vue컴퍼넌트에서 발생하는 이벤트나 데이터등을 시뮬레이션 할수 있게 도와주는 라이브러리이다.

vue와 jest 를 연동하는 라이브러리등이 버젼에 따라 vue2 또는 vue3 에 적합하도록 되어 있으므로 아래의 package.json 을 참고하여 자신의 package.json 을 수정한후 npm install 해서 라이브러리를 다시 설치하자.

  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "test": "vue-cli-service test:unit"
  },
  "dependencies": {
    "@vue/cli-plugin-unit-jest": "^4.5.14",
    "core-js": "^3.6.5",
    "vue": "^2.6.11"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "~4.5.0",
    "@vue/cli-plugin-eslint": "~4.5.0",
     "@vue/test-utils": "1.3.6",
    "@vue/cli-service": "~4.5.0",
    "babel-eslint": "^10.1.0",
    "eslint": "^6.7.2",
    "eslint-plugin-vue": "^6.2.2",
    "vue-template-compiler": "^2.6.11"
  },
  
  "jest": {
    "preset": "@vue/cli-plugin-unit-jest"
  }

테스트 1


import HelloWorld from '@/components/HelloWorld.vue'
import Vue from 'vue'

let Constructor
let vm
let abc

test('HelloWorld Test', () => {
  Constructor = Vue.extend(HelloWorld)
  vm = new Constructor().$mount()
  abc = vm._data.abc
  expect(abc).toBe(100);
});

2024년 6월 24일 월요일

Vuejs + Lodash

Vuejs + Lodash

Vuejs + Lodash

Rx계열의 프로그램을 접해본 사람들은 데이터 스크림을 효과적으로 다루는 map, reduce, filter, debounce 등의 함수를 통해 원하는 데이터를 효과적으로 취득하는 방법을 알고 있을 것이다.
Lodash 는 Rx 프로그래밍을 할때처럼 데이터(배열,컬렉션등)을 효과적으로 다룰수 있도록 도와주는 라이브러리이다.

Lodash는 일관된 모듈식 고성능 JavaScript 유틸리티 라이브러리이다. 대부분의 개발 요구 사항을 충족할 수 있는 풍부한 기능과 방법을 제공한다. 스캐폴딩을 통해 구축된 Vue 프로젝트에는 Lodash 도구 라이브러리가 설치되어 있어 직접 사용할 수 있다.
Lodash 공식 웹사이트: lodash.com/

lodash 에는 정말 많은 기능들이 탑재되어 있어서, array, collection, data 등의 데이터를 쉽고 간편하게 다룰수있도록 해준다.
Arry관련 함수로는 findIdnex, flatten,remove등과 Collection관련 함수로는 every, find,filter,map, reduce 등을 지원한다.

Array: 데이터 채우기, 요소 찾기, 배열 슬라이싱 및 기타 작업과 같은 배열 유형에 적합
Collection: 배열 및 객체 유형에 적용 가능하며 그룹화, 검색, 필터링 및 기타 작업과 같은 문자열에 부분적으로 적용 가능
Function: 조절, 지연, 캐싱, 후크 설정 등과 같은 기능 유형에 적합
Lang: 다양한 유형에 보편적으로 적용 가능하며 유형 판단 및 유형 변환을 수행하는 데 자주 사용
Math: 숫자 유형에 적합하며 종종 수학 연산을 수행하는 데 사용
Number: 난수를 생성하고 숫자 값과 숫자 간격의 관계를 비교하는 데 적합
Object: 객체 유형에 적용 가능하며 객체 생성, 확장, 유형 변환, 검색, 수집 및 기타 작업에 자주 사용.
Seq: 체인 호출을 생성하고 실행 성능을 향상시키는 데 자주 사용(지연 계산).
String: 문자열 유형에 적합

사용법

Lodash의 첫 번째 소개 방법을 사용 import _ from 'lodash'하거나 import { xx } from 'lodash'. 그런 다음 해당 메서드를 사용하려면 직접 _.xx호출 하면 된다 xx(). 아래 debounce 예를 들어보겠다 `

<template>
 <div 
 class="edit-input" 
 :style="{'font-size':fontSize}"
 v-html="innerText"
 :contenteditable="isEdit"
 @focus="isLock = true"
 @blur="onBlur"
 @input="changeText"
 >
 </div>
</template>
<script>
import _ from 'lodash'
//  import { debounce } from 'lodash'
/*
  
props: {  
  value: { 
    type: String, 
   },  
 },  
 data() {  
   return {  handleInput: null,  }  
 },
mounted() { 
   this.handleInput = debounce(function (e) {     
      console.log('value', e.target.value) this.$emit('input', e.target.value)
      }, 1000) 
},


  


*/

export default {
 methods: { 

 changeText:_.debounce(function(e){
 this.$emit('input',e.target.innerText); 
 }, 500)
 }
}
  
</script>

또는

2024년 6월 23일 일요일

Vuex 모듈 방식개발

Vuex 모듈 방식개발

Vuex 모듈 방식으로 개발

1. 모듈을 사용하는 이유

단일 상태 트리를 사용하기 때문에 애플리케이션의 모든 상태가 상대적으로 큰 개체에 집중된다. 애플리케이션이 매우 복잡해지면 저장소 개체가 상당히 커질 가능성이 있다.

위의 문제를 해결하기 위해 Vuex를 사용하면 저장소를 모듈로 분할할 수 있다. 각 모듈에는 고유한 상태, 변형, 작업 및 getter가 있다.

네임스페이스 기본적으로 모듈 내의 작업, 변형 및 getter는 전역 네임스페이스에 등록된다. 이를 통해 여러 모듈이 동일한 변형 또는 작업에 응답할 수 있다.

모듈이 더 높은 수준의 캡슐화 및 재사용성을 갖기를 원하는 경우, 네임스페이스 모듈을 추가하여 네임스페이스 모듈로 만들 수 있다. 모듈이 등록되면 모듈의 모든 getter, 작업 및 변형 이름이 모듈의 등록된 경로에 따라 자동으로 지정된다.

2. vuex 다운로드

일단 vuex를 설치한다.

npm i vuex yarn add vuex // yarn

3. store.js 수정

기본적으로 전역인 store.js만 있는데 아래처럼 모듈폴더를 추가하고 각 모듈별 저장할 store 파일들을 만든다.

  • src 폴더에 store 폴더 추가
  • store.js를 store 폴더에 넣고 store.js의 이름을 index.js로 바꿉니다.
  • 모듈 js를 저장하려면 store 폴더에 모듈 폴더를 추가한다…

  • type.js 에서 상수를 정의하고 , 상수를 사용하여 mutation을 대체한다. 이벤트 유형 member.js 에서 사용자 구성 요소 에 사용되는 , state를 작성 하고 마지막으로 균일하게 내보낸다(이전 예의 store.js 와 유사) . getters.js 에서는 프로세스 판단 및 비동기 요청과 같이 실행될 작업인 actions.js 에 작성된 원래 속성을 가져오는 데 사용 된다 .

4. main.js 에 index.js(스토어) 등록수정

코드 복사

import Vue from 'vue'
import App from './App.vue'
import store from './store/index.js'
new Vue({
 store,
 el: '#app',
 render: h => h(App)
})

5.types.js에서 상수를 정의하고 내보낸다. 기본적으로 모두 대문자이다.

const INCREMENT = 'INCREMENT'
const DECREMENT = 'DECREMENT'
export default {
 INCREMENT,
 DECREMENT
}

참고 : 이러한 상수를 별도의 파일에 넣으면 코드 공동 작업자가 전체 앱에 포함된 변형을 한눈에 볼 수 있다. 상수를 사용할지 여부는 사용자에게 달려 있다. 이는 여러 사람이 함께 작업하는 대규모 프로젝트에 도움이 될 수 있다.

6. member.js의 사용자 구성 요소에 사용되는 상태, getter, 작업 및 변형을 작성한다.

import types from "./../types";

// state로 관리
const state ={
 count:5
}
// getters
var getters ={
 count(state){
 return state.count
 }
}
// actions : store에 가해질 액션을 정의,commit 을 통해서 액션을 받는다. commit(types.INCREMENT)
const actions ={
 increment({ commit, state }){
 commit(types.INCREMENT)
 },
 decrement({commit,state}){
 if (state.count>10) {
 commit(types.DECREMENT)
 }
 }
}
const mutations ={
 // 액션을 통해 commit(types.INCREMENT) 가 발동할때 실행. 단 상수를 안쓴다면 보통 이렇게 호출한다.  increment(state)
 [types.INCREMENT](state){
 state.count++
 },
 [types.DECREMENT](state){
 state.count--
 }
}
// 외부에서 사용할수 있도록 내보내자.
export default {
 namespaced: true, // 네임스페이스 설정 추가해서 상태,게터,액션,뮤테이션이 모듈 내부에서만 접근하도록 하자.
 state,
 getters,
 actions,
 mutations
}

7. 추가로 모듈의 값을 이용하여 전역 getters.js에 홀수와 짝수를 결정하는 기능을 작성한다.


import members from './modules/member'
const getters = {
 isEvenOrOdd(state){
 return members.state.count % 2 == 0 ? "짝수" : "홀수"
 }
}
export default getters;

8. index.js에 actions.js, getters.js, user.js를 모아서 균일하게 내보낸다.

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
import getters from './getters'
import actions from './actions'
import members from './modules/member'

export default new Vuex.Store({
 getters,
 actions,
 modules:{
 members
 }
})

9. AppMain.vue 파일을 수정.

코드 복사

<template>
 <div id="app">
 <button @click="increment">증가</button>
 <button @click="decrement">감소</button>
 <p>{{count}}</p>
 <p>{{isEvenOrOdd}}</p>
 </div>
</template>
<script>
import { mapGetters, mapActions } from "vuex";
export default {
 name: 'app',
 data () {
 return {
 msg: 'Welcome to Your Vue.js App'
 }
 },
 computed:mapGetters([
 'count',
 'isEvenOrOdd'
 ]),
 methods:mapActions([
 'increment',
 'decrement'
 ])
}
</script>

2024년 6월 18일 화요일

Laravel : MVC아키텍쳐에 대해

Laravel : MVC아키텍쳐에 대해

Laravel : 라라벨의 MVC 아키텍쳐

라라벨의 기본 아키텍쳐는 MVC이다.
Controller : 사용자로부터의 요청을 받아 , View와 Model을 연결
Model : 데이터 처리- 실제로는 Eloquent 에서 담당한다.
View : 화면처리

우리가 알고 있는 MVC 는 범용적인 경우에 적용하는것이고, 라라빌에서의 MVC 는 조금 다르다.

특히 비지니스 로직을 담당하는 Model 을 artisan 으로 생성해 보면 데이터베이스에서 데이터를 꺼내거나 처리하는 Eloquent 가 use 에 추가된다.

use Illuminate\Database\Eloquent\Factories\HasFactory;  
use Illuminate\Database\Eloquent\Model;

app/Http 폴더는 라라벨 프레임워크를 이용하는 것을 전제로 작업을 View , Controller 이나 Middleware 를 만들때 주로 사용된다.

Model 은 직접적으로 View와 연결하지 않고 Controller에서 참조한다.
이렇게 되면 Controller가 요청받은 리퀘스트처리, View의 처리와 Model 에서 전달받은 데이터를 가지고 비지니스 로직까지 품어야 되는데 Controller 가 비대해지는 경향이 있다.

규모가 작은 홈페이라면 라라벨의 기본 구조만으로도 충분하지만, 좀 규모가 있는 개발이라면 기능에 따라 모듈 또는 디렉토리 별로 관리하는것이 좋다.

문제는 서비스가 늘어날수록 View나 Controller가 늘어나는것은 어쩔수없다.

해결 방법은 여러가지가 있겠지만
기능(feature)별로 폴더를 만들어서 그 안에 Domain layer, Data layer 그리고 Presenter까지 세트로 묶는 방식이 기능확장 또는 추가/삭제 시에 좋을거라 생각한다.

즉,
app폴더와 같은 레벨의 폴더인 package 등과 같은 상위 폴더를 만들고 하위로 아래와 구조로 만든다.

Features
  |-StoreDetail
      |-Domains
      |-Models
      |-resources
      |-Presenters
  |-StoreList
      |-Domains
      |-Models
      |-resources
      |-Presenters
Infrastructures
  |-Databases
  |-Apis
  |-FileSystems
  |-Messages
  |-Logs
Shared 를 만들어 
  |-Utils
  |-Constants
  |-Settings

파일과 폴더가 많아 지지만 이렇게 함으로서 각 기능별로 추가/수정/삭제가 용이하고 Presenter를 통해 Controller 가 데이터를 삽입/취득 하게하여 controller의 부담을 경감시킬 수있다.

2024년 6월 17일 월요일

Laravel : Test 하기

Laravel : Test 하기

Laravel 테스트 작성

Laravel에서는 UnitTest와 FeatureTest 의 두가지가 있다.

UnitTest 는 laravel 프레임워크에 의존하지 않는 순수한 php 소스에 대해 테스트한다.
FeatureTest 는 Laravel 프레임워크를 사용한 기능 테스트가 가능하다.

테스트 환경설정

테스트관련 설정값을 phpunit.xml 에서 설정한다.
기본적으로 아래와 같은 형태이다.

    <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
      xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"  
      bootstrap="vendor/autoload.php"  
      colors="true"  
    >  
     <testsuites> <testsuite name="Unit">  
     <directory suffix="Test.php">./tests/Unit</directory>  
     </testsuite> <testsuite name="Feature">  
     <directory suffix="Test.php">./tests/Feature</directory>  
     </testsuite> </testsuites> <coverage processUncoveredFiles="true">  
     <include> <directory suffix=".php">./app</directory>  
     </include> </coverage> <php> <server name="APP_ENV" value="testing"/>  
     <server name="BCRYPT_ROUNDS" value="4"/>  
     <server name="CACHE_DRIVER" value="array"/>  
      <!-- <server name="DB_CONNECTION" value="sqlite"/> -->  
     <!-- <server name="DB_DATABASE" value=":memory:"/> -->  <server name="MAIL_MAILER" value="array"/>  
     <server name="QUEUE_CONNECTION" value="sync"/>  
     <server name="SESSION_DRIVER" value="array"/>  
     <server name="TELESCOPE_ENABLED" value="false"/>  
     </php></phpunit>

testsuites 항목에 보면 Unit와 Feature 항목이 있고 각각 디렉토리와 테스트파일로 인식해서 테스트를 실행할 접미사suffix 가 있다.
이곳에 항목을 추가해서 자신만의 디렉토리에서도 테스트가 실행되도록 할수도 있다.
server name=“APP_ENV” value=“testing” 항목이 있는데 .env 파일을 테스트용으로 별도로 만들어 테스트할때는 다른 환경에서 실행되도록 할수도있다.

UnitTest

UnitTest, 단위테스트는 함수레벨에서 프로그램의 특정 부분을 검증하는것이다. 가능한 모든 클래스와 함수는 테스트가 가능해야 되며 테스트가 불가능 하거나 테스트하기 어려운 함수를 만들었다는 것은 잘못 코딩했다는 반증이기도하다.

PHP 에서는 PHPUnit 이라는 PHP 단위 테스트 패키지 를 제공한다. 이는 주로 단위 테스트에 사용된다. 즉, 가능한 가장 작은 구성 요소로 코드를 테스트할 수 있지만 매우 유연하며 단순한 단위 테스트 이상의 용도로 사용할 수 있다.

Laravel에서는 PHPUnit 테스트를 확장한 TestCase라는 클래스를 제공하여 사용자의 테스트 클래스에서 상속받아 테스트를 진행한다.

class ExampleTest extends TestCase  { 
	public function test_example()  
	{  
	    $this->assertTrue(true);  
	}
}

Laravel 설치시에 위와같은 테스트 샘플이 있으므로 한번 테스트를실행해보자.

    php artisan test 

결과
   PASS  Tests\Unit\ExampleTest
  ✓ example
   PASS  Tests\Feature\ExampleTest
  ✓ example

  Tests:  2 passed
  Time:   0.03s

artisan 의 test 명령을 이용하면 Unit 테스트 와 Feature 테스트 전부 실행된다.
Unit 만 실행하고 싶을때는 아래와 같이 한다.

php artisan test --testsuite=Unit

또는 하나의 파일만 테스트 할수도 있다.

php artisan test tests/Unit/ExampleTest.php

FeatureTest ( 애플리케이션 테스트)

여러 개의 코드 유닛이 함께 작동하여 시스템의 특정 기능이 예상대로 동작하는지 검증하는 테스트이다. 주로 컨트롤러, 라우팅, 데이터베이스 쿼리 등을 포함한 여러 시스템 요소를 통합적으로 테스트한다.

Http 나 데이터 베이스 접속 등을 이용한 테스트가 가능하다.

use RefreshDatabase; // use RefreshDatabase 는 테스트 할때 저장한 데이터를 테스트 종료와 함께 삭제해준다.

public function testUser()
{
    $user = User::factory()->create(); //$user 데이터 생성

    $response = $this->actingAs($user)  //actingAs 함수을 사용하여 사용자 인증을 처리
                     ->getJson('/api/users');  //getJson 함수을 사용하여 JSON 응답받음

    $response->assertStatus(200)
             ->assertJson(['data' => true]); //assertJson  추가 검증
}

Feature 만 실행하고 싶을때는 아래와 같이 한다.

php artisan test --testsuite=Feature

Mock 테스트

모의 테스트(Mock Testing) 은 테스트 대상이 되는 클래스나 함수에 영향을 줄만한 다른 함수의 결과치를 모의(Mocking) 해서 지정된 결과치가 나오도록 하여 실제로 테스트 대상 함수가 해당 결과치를 이용한 수행결과가 테스트결과 예상치와 같은 지 확인하기 위해서 사용한다.

즉, foo() 함수를 실행하는데 foo 함수내부에서는 bar 를 사용한 결과를 가지고 추가 처리를 하여 최종 결과를 리턴하는 기능을 할때, foo 함수를 테스트 할때 bar 결과치에 따라 달라지거나, 실패가 발생할때 원인이 bar 에 의한 건지 foo 에 의한 건지 모호할때가 있다.

이때 bar 함수를 테스트에 필요한 지정된 값만 출력되도록 모의(mocking) 하여 foo 함수가 해당값을 이용하여 처리한 결과가 성공인지 판단 할수 있도록 해준다.

샘플을 보자

namespace App\Services;

class FooBarService
{
    public function bar()
    {
        // 복잡한 로직이나 외부 서비스 호출 등
        return 'real result';
    }

    public function foo()
    {
        $barResult = $this->bar();
        // bar의 결과에 따라 foo의 동작이 달라짐
        if ($barResult === 'real result') {
            return 'foo with real result';
        }
        
        return 'foo with different result';
    }
}

테스트 해보자

    class FooBarServiceTest extends TestCase
{
    public function testFooWithMockedBar()
    {
        // FooBarService의 Mock 객체 생성
        $fooBarServiceMock = $this->getMockBuilder(FooBarService::class)
            ->onlyMethods(['bar'])
            ->getMock();

        // bar 메서드가 호출되면 'mocked result'를 반환하도록 설정
        $fooBarServiceMock->method('bar')->willReturn('mocked result');

        // foo 메서드 실행
        $result = $fooBarServiceMock->foo();

        // foo 메서드가 예상하는 결과를 반환하는지 확인
        $this->assertEquals('foo with different result', $result);
    }
}

실행해 보면 아래 처럼 테스트가 성공한다.

   PASS  Tests\Unit\FooBarServiceTest
  ✓ foo with mocked bar

  Tests:  1 passed
  Time:   0.35s

getMockBuilder 로 FooBarService 클래스의 bar() 함수만 모의로 생성한다음 willReturn 으로 ‘mock result’ 라는 고정된 결과가 나오도록 하여 우리가 테스트할 foo 에서 해당 값을 가지고 테스트 하다록 하였다.
foo 에서는 ‘mock result’ 라는 인수가 들어올때 'foo with different result’라는 결과를 출력하므로 참이 되어 테스트가 성공했다.

createMock & getMockBuilder

테스트대상 이외의 클래스나 함수에 대해 지정된 모의(mock)결과를 리턴하도록 도와주는 함수들이다.

createMock : 지정한 클래스에 대해 클래스내의 public 함수에 대해 무조건 null을 리턴하는 모의 class를 만들어준다.

$stub = $this->createMock(SomeClass::class);

getMockBuilder 는 여러가지 설정사항을 직접 선택하여 모의객체를 생성할수있다.

        $mock = $this->getMockBuilder(SomeClass::class)
                 ->onlyMethods(['method1'])
                 ->disableOriginalConstructor() 
                 ->setConstructorArgs(['arg1', 'arg2'])
                 ->disableOriginalClone()
                 ->disableAutoload()
                 ->enableArgumentCloning()
                 ->getMock();

생성자실행안함, 특정함수만 이용,인수지정 등을 사용할수있다.

will

모의객체나 함수에 대해 리턴값을 지정할수있다. 리턴값을 지정함으로서 실제로 테스트 대상이 되는 함수에 대해 신뢰성있는 테스트 결과를 얻을수있다.

$mock->method('fuga')->will($this->returnValue('foo'));

wil 은
will( this−>returnValue(this->returnValue(value)) : 반환값을 설정
will( this−>returnArgument(this->returnArgument(num)) : 메서드에 전달된 $num 번째 인수를 반환 값으로 설정
will( $this->returnSelf()) : 반환 값에 모의객체 자체를 설정
will( $this->throwException(예외)) : 반환값에 예외값를 설정
등으로 사용하는데 좀더 사용하기 편리하도록 함수화 한것들도 있다.

willReturn : 특정 값을 반환하도록 설정

$mock->method('methodName')->willReturn('value');

willReturnMap :인자 값에 따라 다른 반환 값을 설정

$map = [
    ['param1', 'result1'],
    ['param2', 'result2']
];
$mock->method('methodName')->willReturnMap($map);

willReturnArgument : 특정 인자 값을 그대로 반환하도록 설정

$mock->method('methodName')->willReturnArgument(0); // 첫 번째 인자 값을 반환

willReturnCallback : 콜백 함수를 사용하여 반환 값을 동적으로 설정

$mock->method('methodName')->willReturnCallback(function ($arg) {
    return $arg . ' processed';
});

willReturnSelf
설명: 호출된 객체 자체를 반환하도록 설정. 메서드 체이닝에 유용.

$mock->method('methodName')->willReturnSelf();

willReturnOnConsecutiveCalls 설명: 메서드가 연속적으로 호출될 때 각각 다른 값을 반환하도록 설정

$mock->method('methodName')
     ->willReturnOnConsecutiveCalls('first', 'second', 'third');

Assert

테스트 대상 함수가 예상결과와 같은지 판단하는 함수가 Assert 함수이다. 테스트 대상 함수의 결과치가 여러가지 인 만큼 Assert 도 다양하다.

  • assertEquals() : 값이 같은지 확인. assertEquals(5, $actual);
  • assertSame() : 값과 형식도 같은지 확인. assertSame(5, $actual);
  • assertNotSame() : 값과 형식도 다른지 확인. assertNotSame(5, $actual);
  • assertTrue() / assertFalse() : 불린값이 같은지 확인. assertTrue($actual);
  • assertInstanceOf() : 인스턴스가 같은지 확인. assertInstanceOf(Foo::class, $actual);
  • assertNull(): 값이 null 인지 확인 assertNull($actual);
  • assertNotNull(): 값이 null 이 아닌지 확인 assertNotNull($actual);
  • assertStringStartsWith() / assertStringEndsWith() / assertStringContainsString() : 값의 문자열중에 시작문자, 종료문자, 포함문자가 있는지 확인. assertStringStartsWith(‘foo’, $actual);
  • assertCount() : 배열인수의 개수가 같은지 확인. assertCount(1, $actual)
  • assertEmpty() : 배열이 빈 배열인지 확인. assertEmpty($actual)
  • assertContains() : 배열에 해당 아이템이 포함되었는지 확인. assertContains(‘foo’, $actual)

Laravel에서는 아래와 같은 Assert 도 추가되어 있다.

  • assertPageLoaded($uri, $message = null) :최신 페이지가 로드되었는지 확인한다… URL/메시지가 존재하지 않으면 오류가 보고된다.
  • assertResponseOk() : 페이지 응답이 정상인지 확인
  • assertReponseStatus($code): 지정된 코드에 응답한지 여부
  • assertViewHas($key, $value = null): 지정된 데이터가 뷰에 존재하는지 여부
  • assertViewHasAll($bindings):지정된 데이터 계열이 뷰에 존재하는지 여부
  • assertViewMissing($key):이 데이터가 뷰에 존재하지 않는지 여부를 지정
  • assertRedirectedTo($uri, $with = []):지정된 URI로 리디렉션할지 확인
  • assertRedirectedToRoute($name, $parameters = [], $with = []):클라이언트가 지정된 경로로 리디렉션되는지 여부
  • assertRedirectedToAction($name, $parameters = [], $with = []):작업으로 리디렉션할지 여부
  • assertSessionHas($key, $value = null):세션에 키/값이 존재하는지 여부
  • assertSessionHasAll($bindings):지정된 kv가 세션에 존재하는지 여부
  • assertSessionHasErrors($bindings = []):세션에 오류가 있는 걸까요?
  • assertHasOldInput():세션에 이전 데이터가 있는지 여부
    등등 많이 있다.

테스트 함수의 맨 마지막에 적어준다.
실패의 경우에는 예상치과 실제값이 어떻게 다른가 표시해준다.

  $this->assertEquals('foo with different result', $result);

어노테이션 기능

PHPUnit 에서는 여러가지 어노테이션을 제공해서 테스트 함수에 주석으로 표기하면 PHPUnit이 읽어들여 적절한 액션을 취해준다.

@depends : 현재 테스트가 다른 테스트의 결과를 받아서 처리해야할 필요가 있을때 지정한다.

class ExampleTest extends TestCase
{
   public function testEmpty()
   {
       $stack = array();
       $this->assertEmpty($stack);
       return $stack;
   }
   /**
    * @depends testEmpty
    */
   public function testPush(array $stack)
   {
       array_push($stack, 'foo');
       $this->assertEquals('foo', $stack[count($stack)-1]);
       $this->assertNotEmpty($stack);
       return $stack;
   }
   /**
    * @depends testPush
    */
   public function testPop(array $stack)
   {
       $this->assertEquals('foo', array_pop($stack));
       $this->assertEmpty($stack);
   }
}

@depends 의 대상이 되는 함수에는 리턴값이 있고 ,이를 depends하는 함수가 인수로서 받아서 테스트에 이용하는 방식이다.

@dataProvider : 테스트 대상 함수에 인수를 전달 하는 것이 가능하다. 테스트 함수에 인수를 다양하게 지정하여 테스트 대상함수가 해당 인수로 성공하는 지 알아 볼때 편리하다.

class ExampleTest extends TestCase
{
    /**
     * @dataProvider additionProvider
     */
    public function testAppend($a, $b, $expected)
    {
        $result = $a . $b;
        $this->assertEquals($expected, $result);
    }

    public function additionProvider()
    {
        return [
            ['Hello, ', 'World!', 'Hello, World!'],
            ['This ', 'is a ', 'This is a '],
            ['PHP ', 'unit test', 'PHP unit test'],
        ];
    }
}

@testWith : dataProvider와 비슷하지만 좀더 간단하게 지정할수 있다.

class ExampleTest extends TestCase
{
    /**
     * @testWith ["Hello", "World", "HelloWorld"]
     *           ["This", " is a ", "This is a "]
     *           ["PHP", " unit test", "PHP unit test"]
     */
    public function testConcatenateStrings($a, $b, $expected)
    {
        $result = $a . $b;
        $this->assertEquals($expected, $result);
    }
}

@group : 그룹명을 지정하여 테스트 시에 선택적으로 해당 그룹만 실행할수있다.

 /**  
 * @group mytest  
 */
 public function test_1(){}
 
 public function test_2(){}
 
 /**  
 * @group mytest  
 */
 public function test_3(){}

php artisan test --group mytest 을 실행하면 test_1(), test_3() 만 실행된다.

Laravel : ServiceProvider & Service Container

Laravel : ServiceProvider & Service Container

Laravel : Service Provider & Container

공식문서에서 정의한 내용을 보자.
https://laravel.kr/docs/8.x/providers

쉽게 가보자.
서비스 컨테이너와 서비스 프로바이더에서 서비스라는 단어를 빼고 생각해 보자.

컨테이너와 프로바이더의 관계가 된다.
컨테이너는 말 그대로 뭔가 들어있는 우리가 항만에서 볼수있는 그 거대한 컨테이너박스를 의미한다.
프로바이더는 제공자라는 의미로 뭔가를 공급하는 사람을 뜻한다.

우리가 만드는 어플리케이션을 하나의 생산품이라고 생각해보자. 이때 우리는 자주 사용하는 부품이 여러 공정에서 필요한데 이를 매번, 매공정마다 담당자가 제조사에 전화해서 달라고 하면 담당자마다 다른 곳에 전화하는 실수를 한다든지 잘못 주문한다든지 하게된다.
이럴때 우리는 프로바이더에게 해당 부품의 공급을 의뢰해서 컨테이너 단위로 물건을 요청할수있다. 이렇게 하면 프로바이더에게 요청하는 담당자를 통해서 부품을 공급받게되어 빠르고 안정적으로 제품을 생산할수 있는 환경이 된다.

서비스 컨테이너와 서비스 프로바이더도 같은 원리이다.

Laravel에서 Service Provider 는 애플리케이션이 기동되는 순간(이를 라라벨에서는 부트스트래핑이라고 부른다.) 필요한 프로바이더들을 등록하고 프로바이더가 무엇을 제공할건지 서비스 컨테이너 내용을 등록하면 우리가 개발하는 프로그램의 어디에서든지 자유롭게 컨테이너의 내용을 꺼내서 활용할수 있도록 해준다.

Service Provider

서비스 프로바이더(서비스 공급자) 는 위에서 설명했듯이 제품을 공급해줄 담당자를 지정한다.

config/app.php 에 보면 providers 항목이 있고, Laravel 프레임워크에서 필요한 프로바이더와 애플리케이션에서 필요한 프로바이더를 미리 등록해 놓은 것이 보인다.
물론 우리가 만든 프로바이더도 등록이 가능하다.

추가하는 provider 는 ServiceProvider 를 상속받는데 register 와 boot 함수를 오버라이드 한다.

register : 객체를 서비스 컨테이너에 바인딩(연결하다) 한다.
boot : laravel이 부트스트래핑할때 호출된다.

여기서 착각하면 안되는 것이, 각 프로바이더마다 register->boot 순으로 실행되는게 아니라, 모든 프로바이더의 register가 종료된후에 모든 프로바이더의 boot 가 실행된다는 것이다.

register()

register 함수에서는 외부에서 프로바이더에게 요청시에 대답해줄 컨테이너정보를 등록한다. 즉, 컨테이너를 가지는게 아니라 보내줄 컨테이너를 지정한다.
컨테이너를 연결할때, singleton 과 bind, instance, when, tag 등의 연결방법이 있다.

singleton : 말그대로 외부에서 여러번 요청해도 하나의 똑같은 컨테이너만 돌려준다.
bind : 컨테이너 요청때마다 새로운 컨테이너 만들어서 돌려준다.
instance : 간단하게 컨테이너를 등록할수있다. 테스트코드 작성시에 Mock용 컨테이너 등록시에 유용하다.
when : 의존관계에서 다른 의존관계의 컨테이너가 필요할때 사용한다.
tag : 여러개를 그룹화 해서 하나의 컨테이너 키이름으로 불릴수있게 해준다.

1.make 로 나만의 프로바이더를 만든다.

php artisan make:provider MyProductServiceProvider

2.생성된 /app/Providers/MyProductServiceProvider.php 가 생성되었고 register()/boot() 빈함수 두개가 있다.

public function register()  
{  
    $this->app->singleton('myutility', function($app){  
       return new MyUtility();   
    });  
}

3.MyUtility 클래스가 없으므로 app/MyUtils/MyUtility.php 를 폴더생성과 함께 만들자.

class MyUtility  
{  
    public function exampleMethod($value): string  
  {  
        return strtoupper($value);  
    }        
}

4.config/app.php 파일에 우리가 만든 프로바이더도 등록해보자.

confit/app.php 
'providers' => [
....
App\Providers\MyProductServiceProvider::class,
]

5.프로바이더를 통해서 컨테이너가 등록이 되었고 어디서든지app(‘myutility’)라는 호출명령어로 MyUtility가 불려지게 된다. 컨트롤러에서 사용해보자.

class ExampleController extends Controller  
{  
    protected MyUtility $myUtility;  
  
    public function __construct()  
    {  
        $this->myUtility = app('myutility');  
    }  
  
    public function index()  
    {  
        $value = 'hello world';  
        return $this->myUtility->exampleMethod($value); // "HELLO WORLD" 반환  
  }  
}

6.라우터에 등록해서 확인해 보면 대문자로 HELLO WORLD 가 출력되는것을 확인할수 있다.

boot()

boot 는 다른 프로바이더의 register가 모두 끝난후에 호출된다.

1.위에서 만들었던 MyproductServiceProvider.php 의 boot 함수에 내용을 추가해보자.

public function boot()  
{  
    //  
  view()->composer('welcome', function($view){  
        return $view->with('helloworld', app('myutility')->exampleMethod('hey! there'));  
    });       
}

2.welcome.blade.php 에서 helloworld 변수를 사용해보자 .

<h1> {{ $helloworld }} </h1>

3.뷰화면에 hey! there 가 출력된다.

Service Provider implements DeferrableProvider

deferred. 말그대로 지연 시킨다는 건데 무엇을 지연시키느냐면 register 를 지연 시킨다는 말이다.
부트스트래핑과저에서 모든 프로바이더에 대해 register에서 사용할 컨테이너를 만들 준비를 해둔다. 이렇게 하면 프로바이더가 많아질수록 애플리케이션 초기기동이 늦어질수있는데, 프로바이더 에 요청할때 비로소 컨테이너만들 준비를 하도록 일단 지연 시키는게 deferrer 이다.

기존 서비스 프로바이더 선언에 DeferrableProvider 인터페이스를 추가하고 provides 함수를 오버라이드해서 사용할 컨테이너를 적으면 된다.

class MyProductServiceProvider extends ServiceProvider  implements  DeferrableProvider


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

사용할때는 동일하다.

Facade

서비스 프로바이더를 사용해는 것도 좋지만 app()함수를 통해 만드는것은 더 직관적이라 어떤 서비스 프로바이더인지 알아보기 어려울수도 있다.
Facade(겉으로, 표면적인) 으로 등록하면 서비스 프로바이더 호출 이름만으로도 쉽게 알아볼수 있게 해준다.

1.Facade파일을 생성하자. getFacadeAccessor 함수에 위의 과정에서 등록했던 ‘myutility’ 키 이름을 지정하면 자동으로 서비스 프로바이더를 불러준다.

class MyUtilityFacade extends Facade  
    {  
    protected static function getFacadeAccessor()  
    {  
        return 'myutility'; // 서비스 컨테이너에 등록된 키  
  }  
}

2.config/app.php 에 aliases 로 등록 하자.

'MyUtilityFacade' => App\Providers\MyProductServiceProvider::class,

3.컨트롤러에서 적었던 서비스 프로바이드 관련 정보를 삭제하고, MyUtirilityFacade 를 이용해 보자.

use App\Facades\MyUtilityFacade;  
  
class ExampleController extends Controller  
{  
  
    public function index()  
    {  
        $value = 'Hello World';  
        return MyUtilityFacade::exampleMethod($value); // "HELLO WORLD" 반환  
  }  
}