반응형

로버트 마틴의 클린 아키텍처를 읽고 쓴 글입니다.

 

https://lodado.tistory.com/79

 

유연한 컴포넌트 만들기 - 모듈화와 추상화

소프트웨어는 탄생과 동시에 생명 주기를 갖는다. 소프트웨어 개발자는 소프트웨어가 변경될 가능성에 더 신경써야 한다. 소프트웨어는 시간의 흐름에 따라 언젠가 죽음을 맞이하게 될텐데 초

lodado.tistory.com

 

서론

 

이전에 유연한 컴포넌트라는 글을 썼었는데,

 

이전에 제가 쓴 "유연한 컴포넌트" 글에서는 리액트 컴포넌트를 UI, 비즈니스 로직, 그리고 Data Fetching으로 분리했었습니다. 이렇게 분리함으로써 코드의 재사용성과 유지보수성을 높이고자 했죠.

 

하지만 최근 들어 사용하는 프레임워크 & 라이브러리의 deprecated나 Next.js의 급격한 변화(Next.js 13에서 14, 15로 이어지는)를 보며..; 피로감과 불안감을 느끼게 되었습니다.

 

특히 Next.js처럼 프레임워크 & 라이브러리의 큰 변화는 기존 코드 구조에 큰 영향을 미치고, 때로는 기존에 작성한 로직을 재작성해야 하는 상황을 만들기도 합니다. 이러한 변화는 개발자로서 큰 부담으로 다가올 수밖에 없는데, 이를 어떻게 대처할 수 있을지 고민이 많아졌습니다.

 

그래서 클린 아키텍처를 읽게 되었는데요.

보통 백엔트 아키텍처에서 많이 사용하고, 객체 지향을 사용해서 프론트엔드 분야에서는 좀 낯선 분야였지만 읽고 많은 영감을 얻게 되었습니다.

 

문제를 해결하기 위한 WHAT 과 HOW

요약하자면, 어떤 비즈니스의 문제를 해결할때 추상화를 통하여

"WHAT"(문제를 어떻게 해결할지)만 생각해야지 HOW(어떤 기술을 쓰는지)는 중요하지 않고, 분리해서 생각해야한다는 것이였습니다.

 

이를 클린 아키텍처에서는 layered architecture와 DIP(의존성 역전)으로 해결합니다.

예를 들어, 블로그에서 자신이 쓴 비밀 글을 조회하는 상황을 생각해봅시다.

티스토리에서는 비밀 글을 자신만 조회하거나 비밀번호를 알아야 조회할 수 있습니다. (아마도..? 제가 아는 한은 그렇습니다)

 

그럼 가장 높은 추상화 단계로 생각하자면

유저가 로그인을 했고, 정당한 권한을 가지고 있다. (WHAT)만 판별하면 되는 문제입니다.

 

여기서 로그인 판별 방식을 어떻게 할지는(HOW), 세션 인증을 사용하는지, 아님 JWT를 사용하는지는 부차적인 문제고 나중에 언제든지 바뀔 수 있습니다. 

 

그리고 권한을 판별할때도, 지금은 비밀글을 자신밖에 못본다 하더라도 나중 업데이트를 통하여 

네이버 블로그처럼 서로 이웃인 친구는 비밀글을 볼 수 있도록 나중에 바뀔 수 있겠죠. 자세한 디테일은 언제든지 갈아끼우면 됩니다.

 

이처럼 기술과, 비즈니스 로직을 부품화해서 언제든지 갈아끼울 수 있도록 구현하는 방식을 클린 아키텍처는 소개시켜 줍니다.

 

클린 아키텍처 구조

출처: https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

 

 

클린 아키텍처하면 가장 유명한 그림인데요

이 책을 읽기 전에는 이해가 잘 안갔는데 읽고 난 후에는 SOLID의 DIP를 응용한 아키텍처라는것을 알게 되었습니다.

 

각 레이어를 to-do 리스트를 만드는 코드 예시와 간단하게 훑어봅시다.

샘플 코드는 아래 깃허브 주소에 있습니다.

 

https://github.com/lodado/chatgpt-cleanarchitecture-example

 

GitHub - lodado/chatgpt-cleanarchitecture-example

Contribute to lodado/chatgpt-cleanarchitecture-example development by creating an account on GitHub.

github.com

 

 

엔티티 레이어(Entity Layer):

class Task {
  id: string;
  title: string;
  isCompleted: boolean;

  constructor(params: { id: string; title: string; isCompleted?: boolean }) {
    if (!params.id || !params.title) {
      throw new EntityError({ message: "Task must have an id and a title." });
    }

    this.id = params.id;
    this.title = params.title;
    this.isCompleted = params.isCompleted ?? false;
  }

  /**
   * Mark the task as completed
   */
  toggleMark(): void {
    this.isCompleted = !this.isCompleted;
  }

  /**
   * Change the title of the task
   */
  changeTitle(newTitle: string): void {
    if (!newTitle) {
      throw new EntityError({ message: "New title cannot be empty." });
    }
    this.title = newTitle;
  }
}

 

이 레이어는 애플리케이션의 핵심이 되는 비즈니스 엔티티를 포함하고 있습니다. 이러한 엔티티는 비즈니스 규칙을 캡슐화하며 애플리케이션 레이어와는 독립적입니다.

 

집합 개념으로 치면 "연산이 닫혀 있다.(Closure under an operation)" 라는 개념이 생각나는데요.

닫혀있다의 사전적 의미는 특정 연산에 대해 어떤 집합이 닫혀 있다고 말할 때, 이는 그 연산을 집합의 원소들 사이에서 수행한 결과가  항상 동일한 집합의 원소로 남아 있는 성질을 의미합니다.


예를 들어, 엔티티 레이어는 애플리케이션의 다른 레이어와 독립적으로 존재하며, 엔티티 간의 상호작용이나 연산이 일어날 때 그 결과가 항상 엔티티 레이어 내에서 관리되고 유지됩니다. 즉, 엔티티 레이어 내의 비즈니스 규칙이 적용된 연산 결과는 여전히 같은 엔티티 레이어 내에서 처리되며, 외부 레이어로부터의 영향을 받지 않고 독립성을 유지합니다.

이는 수학적 집합에서 특정 연산을 수행한 결과가 항상 그 집합 내에 남아 있는 것과 유사합니다.

 

유스 케이스 레이어(Use Case Layer):

AddTodoListUseCase {
  constructor(private taskRepository: TodoListRepositoryImpl) {}

  async execute(params: { id: string; title: string }): Promise<void> {
    const task = new Task({ id: params.id, title: params.title });
    try {
      return await this.taskRepository.addTask({ task });
    } catch (error) {
      throw mapRepositoryErrorToUseCaseError(error as Error);
    }
  }
}

 

이 레이어는 애플리케이션에 특화된 비즈니스 규칙을 포함하고 있습니다. 이 레이어는 엔티티로부터 데이터를 주고받는 흐름을 조정하고, 필요한 리포지토리를 호출하며 각 유스 케이스에 대한 로직을 관리합니다.

 

비즈니스 로직을 정의하고 있는데요. addTask라는 명령을 수행하는데 해당 코드는 하나의 list를 저장소에 추가하라는 명령을 내리고 있습니다.

그런데 데이터를 "어떻게", "어디에" 저장할지는 주입받은 repository layer에서 정의합니다.

예를 들어 임베디드 시스템에서 저장 명령을 내릴때 "메모리에 저장해"라는 명령어를 내리고 펌웨어에서 "어디"에 저장할지 구체적으로 실행합니다.  

 

 

 

리포지토리 레이어(Repository Layer):

import { Task } from "../../../../entities";
import {
  RepositoryError,
  mapEntityErrorToRepositoryError,
} from "../../../../shared";

import { TodoListRepositoryImpl } from "./interface";

export default class InMemoryTodoListRepository
  implements TodoListRepositoryImpl
{
  private tasks: Map<string, Task> = new Map();

  async getAllTasks() {
    return new Map(this.tasks);
  }

  async addTask(params: { task: Task }): Promise<void> {
    try {
      if (this.tasks.has(params.task.id)) {
        throw new RepositoryError({
          message: "Task with this ID already exists.",
        });
      }
      this.tasks.set(params.task.id, params.task);
    } catch (error) {
      throw mapEntityErrorToRepositoryError(error as Error);
    }
  }

  async getTaskById(params: { id: string }): Promise<Task | null> {
    try {
      return this.tasks.get(params.id) ?? null;
    } catch (error) {
      throw mapEntityErrorToRepositoryError(error as Error);
    }
  }

  async deleteTask(params: { id: string }): Promise<void> {
    try {
      if (!this.tasks.delete(params.id)) {
        throw new RepositoryError({ message: "Task not found for deletion." });
      }
    } catch (error) {
      throw mapEntityErrorToRepositoryError(error as Error);
    }
  }

  async toggleMark(params: { id: string }): Promise<void> {
    try {
      const task = await this.getTaskById({ id: params.id });
      if (!task) {
        throw new RepositoryError({
          message: "Task not found to mark as completed.",
        });
      }
      task.toggleMark();
    } catch (error) {
      throw mapEntityErrorToRepositoryError(error as Error);
    }
  }
}

 

 

이 레이어는 데이터 접근 로직에 대한 추상화를 제공합니다. 데이터베이스나 웹 서비스와 같은 외부 시스템과 상호작용하며, 외부 포맷과 애플리케이션 엔티티 간의 데이터를 변환하는 역할을 담당합니다.

 

구체적으로 "어디에" 저장할건지를 수행합니다. DB일수도, 메모리일수도, 아니면 네트워크 요청일수도 있죠. 레이어 위에서 아래로 클래스를 주입하면서 의존성 역전을 통하여 핵심적인 로직만 전달합니다.

 

 

어댑터 레이어(Adapter Layer):

 

이 레이어는 애플리케이션과 외부 세계(예: 사용자 인터페이스, 외부 API, 데이터베이스 등) 사이의 인터페이스 역할을 합니다.

백엔드면 URL 요청을 받는 controller,  프론트엔드면 hook 부분일수 있습니다. 

 

객체지향을 쓰지 않더라도 이책에서 말하는 핵심적인 가치, 추상화는 유용할것 같습니다. 이책을 읽고 많이 배운것 같네요.

반응형

'Front-end' 카테고리의 다른 글

쓸만해보이는 태그, 속성들 (메모용)  (0) 2022.07.11
jest가 느린이유  (0) 2022.02.18
반응형

개발하다보면 시간이 지나면서 라이브러리가 deprecated되고, 

라이브러리 버전업 및 마이그레이션을 해야하는 경우가 생기는데요.

 

또한, 디자인 시스템을 만들면서 기능 및 인터페이스가 deprecated 되거나 변경되는 케이스가 존재했는데

 

그때를 위한 좋은 라이브러릴 하나 찾아서 공유해봅니다.

 

1. jscodeshift란 무엇인가?

 

jscodeshift는 Facebook(현 Meta)에서 개발한 JavaScript 코드베이스 변환 도구입니다.

 

이 도구는 Abstract Syntax Tree(AST)를 사용하여 코드의 구조를 이해하고, 필요한 변환을 자동으로 수행할 수 있게 해줍니다.

코드의 AST 변환

 

(위 그림을 보시면 좀 무서울수도 있는데  codemod Studio나 chatgpt를 사용하시면 손쉽게 사용 가능합니다...)

 

이 도구는 특히 대규모 코드베이스에서 일관된 코드 스타일 적용, API 변경에 따른 대규모 리팩토링, 혹은 레거시 코드에서 최신 문법으로의 변환 작업 등에 매우 유용합니다.

 

 

jscodeshift를 찾기 전엔 아래 방법을 사용했었는데요,

 

1. vscode의 전체 검색 및 변경으로 대응

2. file I/O 스크립트+정규식을 통하여 변경

 

다음 케이스는 대응이 어려웠습니다. (실제로 업무중 발생한 케이스)

 

1. 함수/클래스 인터페이스가 변경되는 경우

// 변경전
<Dialog Trigger={TriggerComponent}>
	<Dialog.Header />
	<Dialog.Content />
</Dialog>


// 변경 후
<Dialog>
	<Dialog.Trigger asChild>
		<TriggerComponent />
    </Dialog.Trigger>
	<Dialog.Content>
		<Dialog.Header/>
		<Dialog.Body/>
	</Dialog.Content>
</Dialog>

 

예를 들어서 react에서 props로 넘기던 컴포넌트를

children으로 주입해주게 리펙토링 하는 경우 대응이 어려웠습니다.

 

2. import 경로가 바뀌는 경우, 혹은 일부만 아예 다른 경로로 바뀌는 경우

// 이전
import { useLoginSession, anotherHook } from '@/hooks/login'

// 이후
import { useLoginSession } from '@/entities/auth'
import { anotherHook } from '@/hooks/login'

 

 

그외에도 정규식만으로 대응하기는 어려운 케이스가 수십, 수백 케이스 있겠죠. 특히 라이브러리 migration이 골치아팠었습니다.

 

그럼 쓰는 예시를 코드로 한번 봅시다.

 

jscodeShift 코드 예시

 

2번 케이스가 간단하니 코드로 한번 봅시다. 

 

export default function transformer(file, api) {
  const j = api.jscodeshift
  const root = j(file.source)

  root
    .find(j.ImportDeclaration, {
      source: { value: '@/hooks/login' },
    })
    .forEach((path) => {
      const useLoginSessionSpecifier = path.node.specifiers.find(
        (specifier) => specifier.imported && specifier.imported.name === 'useLoginSession',
      )

      if (useLoginSessionSpecifier) {
        // 경로를 '@/entities/auth'로 변경
        path.node.source.value = '@/entities/auth'

        // 다른 import가 있다면 분리
        const otherSpecifiers = path.node.specifiers.filter((specifier) => specifier !== useLoginSessionSpecifier)

        if (otherSpecifiers.length > 0) {
          // 원래 import 구문에서 useLoginSession을 제거
          path.node.specifiers = [useLoginSessionSpecifier]

          // 새로운 import 구문 추가
          const newImport = j.importDeclaration(otherSpecifiers, j.literal('@/hooks/login'))

          j(path).insertAfter(newImport)
        }
      }
    })

  return root.toSource()
}

 

과정을 글로 설명하면 다음과 같습니다.

 

1. code를 AST 트리로 파싱합니다.

 

2.  AST 트리에서 변경하고 싶은 부분을 찾아냅니다.

 

3. 변경을 적용합니다.

 

4. 적용한 코드를 내보냅니다. (마지막 코드 부분)

 

 

1,2,3번이 좀 어려울 수 있는데 codemod(https://codemod.com/studio) 를 사용하거나,

ChatGPT 같은 AI의 도움을 받으면 빠르고 손쉽게 변경 가능합니다.

 

ai를 사용한다면 내가 하고 싶은 과정을 step by step으로 잘 풀어내는 게 중요하겠네요.

 

결론

 

codemod 와 jscodeshift를 사용하면 자동화된 스크립트를 통하여 손쉽게 바꿀 수 있습니다.

저는 요즘 디자인 시스템 개발 & 기존 코드 migration을 하고 있었는데,  여러 팀이 동시에 작업을 하다보니

 

기존 개발 되었던 컴포넌트의 기본 인터페이스가 잘못되었다고 판단되어도

다른 팀의 프로젝트에 이미 쓰고 있으니 변경이 매우 골치아팠었는데 리펙토링 후 migration 스크립트를 같이 제공하는것으로 변경과 확장에 매우 유연해질 수 있었습니다.

 

좋은 라이브러리 찾아서 공유해봅니다 ㅎㅎ

반응형

'Front-end > JavaScript' 카테고리의 다른 글

commonjs와 ESM의 차이  (0) 2023.07.25
pnpm 도입기 (feat: 모노 레포)  (0) 2023.07.15
자바스크립트의 async/await에 대해서  (0) 2022.03.31
JS로 Single Page Application 구현  (0) 2022.03.12
Generator  (0) 2022.02.02
반응형

사내 디자인 시스템을 만들고 있었는데

배포 및 개발 환경 관련해서 여러가지 피드백 및 요청이 왔습니다..!

 

1. 배포 환경을 갖추기 어려운데 자동화 스크립트가 있으면 좋을거 같다.

2. 초심자도 쉽게 배포하고 빌드하면 좋을 것 같다.

 

특히, 각 컴포넌트를 하나의 모노 레포지토리로 관리하고 있고 내부에서 
turbo repo, rollup, jest 등 많은 개발 라이브러리를 사용하고 있는데 시행착오를 많이 겪으시는거 같아서 

boilerplate 생성 스크립트를 하나 작성했고, 블로그에도 공유해 봅니다. 

 

생성 스크립트

 

더보기
const fs = require('fs')
const path = require('path')
const readline = require('readline')
const { exec } = require('child_process')

const validatePackageName = (name) => {
  const regex = /^@TDS\/[a-zA-Z0-9-_]+$/
  if (!regex.test(name)) {
    throw new Error(`Invalid package name format. It should be in the format @TDS/package's name`)
  }

  const hasUppercase = /[A-Z]/.test(name.split('/')[1])
  if (hasUppercase) {
    console.warn('Warning: Package name should not contain uppercase letters.')
  }
}

const generatePackageJson = (packageName, sourcePath) => {
  // 무엇인지 인지한 상태에서 바꿀것
  const [namespace, name] = packageName.split('/')
  
  const packageJson = {
    name: `${namespace}/${name.toLowerCase()}`,
    version: '0.0.0',
    sideEffects: ['./index.scss'],
    license: 'MIT',
    exports: {
      '.': {
        require: sourcePath,
        import: sourcePath,
      },
      './index.scss': './index.scss',
      './package.json': './package.json',
    },
    source: sourcePath,
    main: sourcePath,
    module: sourcePath,
    types: sourcePath,
    files: ['dist/**'],
    scripts: {
      test: 'jest --passWithNoTests',
      build: 'rollup -c && tsc --emitDeclarationOnly --declarationMap false --declaration --declarationDir dist/types',
      lint: `eslint "src/**/*.ts*"`,
      clean: `rm -rf .turbo && rm -rf node_modules && rm -rf dist`,
      'manual-release': 'pnpm run build && pnpm publish --no-git-checks',
    },
    devDependencies: {
      '@types/node': '^20.12.7',
      autoprefixer: '^10.4.19',
      'babel-jest': '29.5.0',
      eslint: '^8.57.0',
      'jest-config': 'workspace:*',
      'rollup-config': 'workspace:*',
      'eslint-config-acme': 'workspace:*',
      postcss: '^8.4.38',
      sass: '^1.75.0',
      'ts-jest': '29.0.5',
      tsconfig: 'workspace:*',
    },
    dependencies: {},
    peerDependencies: {
      '@types/react': '^18.2.37',
      '@types/react-dom': '^18.2.25',
      react: '^16.8 || ^17.0 || ^18.0',
    },
    peerDependenciesMeta: {
      '@types/react': {
        optional: true,
      },
      '@types/react-dom': {
        optional: true,
      },
    },

    publishConfig: {
      access: 'restricted',
      registry: 'http://##addressissecret##/api/v4/projects/311/packages/npm/',
      exports: {
        '.': {
          import: {
            types: `./dist/types/index.d.ts`,
            default: `./dist/es/client/${path.basename(sourcePath).replace('tsx', 'mjs')}`,
          },
          require: {
            types: `./dist/types/index.d.ts`,
            default: `./dist/cjs/client/${path.basename(sourcePath).replace('tsx', 'cjs')}`,
          },
        },
        './index.css': `./dist/es/client/index.css`,
      },
      source: `./src/index.ts`,
      main: `./dist/cjs/client/${path.basename(sourcePath).replace('tsx', 'cjs')}`,
      module: `./dist/es/client/${path.basename(sourcePath).replace('tsx', 'mjs')}`,
      types: `./dist/types/index.d.ts`,
    },
  }

  return JSON.stringify(packageJson, null, 2)
}
const createSrcIndexTsx = (srcDir) => {
  const srcIndexTsxContent = `export {};`
  fs.writeFileSync(path.join(srcDir, 'index.tsx'), srcIndexTsxContent)
}

const createRootIndexTsx = (projectDir) => {
  const rootIndexTsxContent = `import './index.scss';

// export * from './src';
`
  fs.writeFileSync(path.join(projectDir, 'index.tsx'), rootIndexTsxContent)
}

const createEslintConfig = (projectDir) => {
  const rootIndexTsxContent = `module.exports = {
    root: true,
    extends: ["acme"],
  };  
`
  fs.writeFileSync(path.join(projectDir, '.eslintrc.js'), rootIndexTsxContent)
}

const createIndexScss = (projectDir) => {
  const indexScssContent = ` 
`
  fs.writeFileSync(path.join(projectDir, 'index.scss'), indexScssContent)
}

const createJestConfig = (projectDir) => {
  const jestConfigContent = `const jestConfig = require('jest-config/jest.config.js')

const customJestConfig = {
  ...jestConfig,
  // 패키지별 설정을 여기에 추가
}

module.exports = customJestConfig
`
  fs.writeFileSync(path.join(projectDir, 'jest.config.js'), jestConfigContent)
}

const createPostcssConfig = (projectDir) => {
  const postcssConfigContent = `module.exports = {
  plugins: [require('autoprefixer')()],
}
`
  fs.writeFileSync(path.join(projectDir, 'postcss.config.js'), postcssConfigContent)
}

const createRollupConfig = (projectDir) => {
  const rollupConfigContent = `import { defaultConfig } from 'rollup-config/rollup.config.mjs'

const config = defaultConfig()
export default config
`
  fs.writeFileSync(path.join(projectDir, 'rollup.config.mjs'), rollupConfigContent)
}

const createSetupTests = (projectDir) => {
  const setupTestsContent = `import '@testing-library/jest-dom'
`
  fs.writeFileSync(path.join(projectDir, 'setupTests.ts'), setupTestsContent)
}

const createTsconfig = (projectDir) => {
  const tsconfigContent = `{
  "extends": "tsconfig/react-library.json",
  "compilerOptions": {
    "baseUrl": "./",

    "paths": {}
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["dist", "build", "node_modules", "**/*.test.*"]
}
`
  fs.writeFileSync(path.join(projectDir, 'tsconfig.json'), tsconfigContent)
}

const createProjectStructure = (packageName, projectDir, sourcePath) => {
  if (!fs.existsSync(projectDir)) {
    fs.mkdirSync(projectDir, { recursive: true })
  }

  const srcDir = path.join(projectDir, 'src')
  if (!fs.existsSync(srcDir)) {
    fs.mkdirSync(srcDir)
  }

  // Write package.json
  fs.writeFileSync(path.join(projectDir, 'package.json'), generatePackageJson(packageName, sourcePath))

  // Create other files
  createSrcIndexTsx(srcDir)
  createRootIndexTsx(projectDir)
  createIndexScss(projectDir)
  createEslintConfig(projectDir)
  createJestConfig(projectDir)
  createPostcssConfig(projectDir)
  createRollupConfig(projectDir)
  createSetupTests(projectDir)
  createTsconfig(projectDir)

  console.log(`Project structure for ${path.basename(projectDir)} created successfully.`)
}

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
})

const question = (text, defaultValue = '') => {
  return new Promise((resolve) => {
    rl.question(`${text}${defaultValue ? ` (${defaultValue})` : ''}: `, (answer) => resolve(answer || defaultValue))
  })
}

const runPnpmInstall = (projectDir) => {
  return new Promise((resolve, reject) => {
    exec('pnpm install', { cwd: './' }, (error, stdout, stderr) => {
      if (error) {
        console.error(`Error running pnpm install: ${error}`)
        reject(error)
        return
      }
      console.log(stdout)
      console.error(stderr)
      resolve()
    })
  })
}

;(async () => {
  const packageName = await question('Enter the package name (ex- @TDS/button)')
  validatePackageName(packageName)

  const folderPath = path.join('./apps/', packageName.replace(/\//g, '-'))

  console.log(`folders create in ${folderPath}`)
  const sourcePath = './index.tsx'
  const projectDir = path.join(folderPath)

  console.log(`entryPoint is ${sourcePath}`)

  createProjectStructure(packageName, projectDir, sourcePath)

  rl.close()
  console.log(`package.json generated and project folder created at ${projectDir}.`)

  console.log('start installing packages....')

  await runPnpmInstall(projectDir)

  console.log('done.')
})()

 

코드 전체는 아래에서 구경하실 수 있습니다. 


https://gist.github.com/lodado/4beb14f44d304fe65772809778e0fb35

 

generatePacakges.js

GitHub Gist: instantly share code, notes, and snippets.

gist.github.com

 

사용 예시 스크린샷)

 

 

기본 사용 방법은

라이브러리의 이름을 입력하면 지정된 위치에 폴더 +  라이브러리 명으로 하나의 레포지토리 및 설정 파일이 생깁니다.

혹시 쓰실 분은 config 관련 함수에 자기가 원하는 코드를 작성하는 식으로 커스텀하면 되실 것 같습니다. 

 

반응형
반응형
{
   // 예시 코드!
  "exports": {
    ".": {
      "require": "./src/index.tsx",
      "import": "./src/index.tsx"
    },
    "./index.scss": "./src/index.scss",
  },
  "source": "src/index.tsx",
  "main": "dist/index.js",
  "files": [
    "dist/**"
  ],
  "devDependencies": {
...
  },
  "dependencies": {
....
  },
  "publishConfig": {
    "access": "restricted",
    "registry": "http://####/api/v4/projects/311/packages/npm/",
    "exports": {
      ".": {
        "require": "./dist/index.js",
        "import": "./dist/index.mjs",
        "types": "./dist/index.d.ts"
      },
      "./index.css": "./dist/index.css"
    },
    "import": "./dist/index.mjs",
    "main": "./dist/index.js",
    "module": "./dist/index.mjs",
    "types": "./dist/index.d.ts"
  }
}

 

모노레포에서 개발 경험 향상을 위해서 레포지토리의 package.json의 exports에 원본 코드 파일을(./src/index.ts) 명시하고, 배포할때는 publishConfig 옵션의 exports에 빌드 결과물 파일을 명시해서 사용하고 있다.

 

*export에 빌드 결과물(./dist/index.js)을 적으면 테스트코드를 돌리기 위해서 반드시 모든 레포지토리를 빌드해줘야해서 위 방법을 사용했음

 

RootDir 문제점..?

이랬더니 빌드하면 의존성 있는 레포지토리 내용이 번들링 결과에 포함되는 결과물이 생겼었는데..

 

dist (번들링된 결과물)
└── 기타 파일 etc...
└── packages (다른 레포지토리 결과물)
    ├── @다른 모노레포 1 
    │   ├── designFiles
    │   └── src
    │       ├── index.css
    │       └── index.js
    ├── @다른 모노레포 2
    │   └── src
    │       ├── .eslintrc.js
    │       ├── .eslintcache
    │       ├── .gitignore
    │       └── .npmrc

 

빌드 과정에서 tsconfig.jsoncompilerOptions.rootDir 설정이 다른 타입스크립트 파일을 포함하기 위해서 모노레포의 최상위 레포지토리 루트(./)로 자동 확장되고, 빌드 결과물에 다른 레포지토리의 내용이 함께 번들링되는 문제가 발생한걸로 추측됐다. 

 

이 타입 관련 문제는 typescript 자체적인 문제로 tsup, rollup등 다른 번들러를 써도 동일한 결과가 나올것이라 추측되고 실제로 tsup, rollup 동일하게 발생했다.

 

해결 방법  

해결방법은 생각보다 간단했는데.. 

 

1. peer dependencies

 

이미 다른 레포지토리를 배포했다면 해당 라이브러리를 참조해서 사용하면 된다.

그래서, peerDependencies에 다른 모노레포 레포지토리 명을 적어주면 개발할때는 export에 적어놓은 타입스크립트 원본 파일을 참조하고, 빌드할때는 peerdependencies이니까 자동적으로 다른 레포지토리의 내용이 빌드 결과물에 빠지게 된다.

 

참고로 rollup external 등 여러 옵션을 써봐도 다른 레포지토리의 빌드 결과는 자동적으로 빠지지 않았다. 번들러 입장에서 다른 레포지토리가 정말로 배포되어 있어서 내가 빌드 안해도 되는지 알 방법이 없으니 peerDependendicies로 명시해줘야한다.

 

2. 빌드할때 내 폴더의 타입은 tsc로 따로 빌드함

 

다른 사람들이 쓴 rollup 코드들을 보면 타입은 tsc 등으로 따로 빌드하던데 왜 그런가 했더니.. 이 rootDir문제 때문이 아닐까?

typescript({
          tsconfig: './tsconfig.json',
          tsconfigOverride: {},
          useTsconfigDeclarationDir,
        }),

 

나는 rollup과 rolllup-plugin-typescript2를 사용하고 있는데 useTsconfigDeclarationDir로 타입만 빼고 자바스크립트 파일만 따로 빌드하게 만들었다. 

해당 옵션을 빼면 peerDependencies를 사용해도 동일한 문제가 발생했다.

 

  "scripts": {
    "build": "rollup -c ./rollup.config.mjs",
    "build:types": "tsc --emitDeclarationOnly --declarationMap false --declaration --declarationDir dist/types/client",
  }

 

그리고 tsc로 레포지토리의 타입만 따로 추출한다.


해당 방법이 완벽한 정답이 아닐수도 있긴 하지만.. 일단 pnpm, turbo repo, typescript, rollup을 사용하는 환경에서는 잘 동작하는것 같다. 

반응형
반응형

사내에서 프로젝트 별 코드의 파편화를 줄이고 디자인 시스템을 만들려고 하고 있다.

인수인계 겸 정리한 문서를 공개해도 상관 없을꺼 같아서 블로그에 간단 정리해본다.

 

사용 툴

  • 🏎 Turborepo — 모노레포 라이브러리로 여러 패키지나 프로젝트 간의 의존성 관리를 효율화하고 Monorepo 내에서 빌드 프로세스를 간소화
  • 🚀 React — 프론트엔드 사용 라이브러리
  • 🛠 Parcel — 번들러로 설정 없이 간편하게 쓸 수 있어서 사용, 추후 rollup으로 변경될 가능성도 있음
  • 📖 Storybook — UI 컴포넌트 문서화 및 공유 & 협업용
  • TypeScript는 정적 타입 검사를 위한 도구
  • ESLint는 코드 린팅을 위한 도구
  • Prettier는 코드 포맷팅을 위한 도구
  • Changesets는 버전 관리와 변경 로그를 관리하는 도구
  •  

라이브러리를 구현할때는 radix-ui를 사용합니다.

자체 구현은 최대한 지양합니다. (개발자 나가면 유지보수가 안됨 ㅠㅠ)

 

커밋 컨벤션

build: 빌드 시스템이나 외부 의존성에 관련된 변경사항 (예: gulp, broccoli, npm의 패키지 변경 등)
chore: 기타 변경사항 (코드 수정 없이 설정을 변경하는 경우)
ci: 지속적 통합(CI) 설정과 스크립트 변경사항 (예: Travis, Circle, BrowserStack, SauceLabs의 설정 변경 등)
docs: 문서에만 영향을 미치는 변경사항
feat: 새로운 기능에 대한 커밋
fix: 버그 수정에 대한 커밋
perf: 성능을 개선하는 코드 변경사항
refactor: 버그를 수정하거나 기능을 추가하지 않는 코드 변경사항
revert: 이전 커밋을 되돌리는 작업
style: 코드 의미에 영향을 주지 않는 변경사항 (공백, 포맷팅, 누락된 세미콜론 등)
test: 테스트 추가, 리팩토링 테스트 코드, 누락된 테스트 추가 등
echo "foo: some message" # fails
echo "fix: some message" # passes

 

conventional commit을 사용합니다. 허용되지 않은 양식은 husky와 commitlint library가 막습니다.

https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional

 

모노 레포 구성

 

모노레포는 여러 개의 프로젝트나 라이브러리를 단일 저장소에서 관리하는 방식을 말합니다. 이러한 접근 방식은 큰 프로젝트나 여러 팀이 협업할 때 많은 이점을 제공합니다. 주요 이점으로는 코드의 재사용성 증가, 의존성 관리의 단순화, 통합된 버전 관리 등이 있습니다.

turbo repo를 이용한 모노 레포 구조를 가집니다. 각 폴더는 하나의 레포지토리를 구성합니다.

 

- apps
    ㄴ docs -> 문서용 storybook
    ㄴ RadixComponents -> 사용하는 공통 컴포넌트 (현재는 사용하고 있지 않음)
    ㄴ TTTable  -> 테이블 구현체

- packages
    ㄴ shared  -> 공통 변수
    ㄴ @TDS/utils -> 공통 함수
    ㄴ designFiles -> designTokens, icons들 저장소

- configs
    ㄴ eslint -> eslint 설정
    ㄴ tsconifg -> typescript 설정
    ㄴ jestconfig -> jest 설정
  • apps: 실제 사용자에게 제공되는 애플리케이션들이 위치합니다. 여기에는 문서를 위한 Storybook(docs), 공통 컴포넌트(RadixComponents), 테이블 구현체(TTTable) 등이 포함될 수 있습니다.
  • packages: 재사용 가능한 코드, 공통 함수, 디자인 토큰, 아이콘 등을 포함하는 패키지들이 위치합니다. 이러한 패키지들은 apps 내의 프로젝트들 및 배포 후 자사 프로젝트에 의해 사용될 수 있습니다.
  • configs: 프로젝트 전체에 걸쳐 공유되는 설정 파일들이 위치합니다. ESLint, TypeScript, Jest 등의 설정이 포함될 수 있습니다.

script 실행 방법

최상위 package.json에 정의된 스크립트는 모노레포 내의 여러 프로젝트나 패키지에 대한 작업을 조정합니다. 예를 들어, 다음과 같은 스크립트가 있을 수 있습니다:

  • build: 애플리케이션과 패키지를 빌드합니다.
  • test: 애플리케이션과 패키지의 테스트를 실행합니다.
  • lint: 코드 스타일과 문법 검사를 전체 프로젝트에 걸쳐 실행합니다.
  • start: 개발 모드에서 특정 애플리케이션을 실행합니다.

최상위 폴더에서 pnpm run build시 하위 레포지토리의 build 명령어가 있는 레포지토리는 병렬적으로 해당 명령어를 전부 실행합니다.

다른 자세한 기능들은 turbo repo docs를 참고하시기 바랍니다.

레포지토리 배포 프로세스

1. pnpm run changeset
2. 패치 버젼, summary 등 작성
3. pnpm run changeset version (version up)
4. commit
5. PR 및 리뷰
6. master branch에 반영
7. gitlab runner에서 자동 배포

1. pnpm run changeset

  • 이 명령은 변경 사항을 추가하기 위해 Changesets CLI를 실행합니다.

2. 패치 버전, 요약 등 작성

  • 선택한 패키지와 업데이트 유형(예: 패치)에 대한 설명을 작성합니다. 이 설명은 추후 changelog에 반영됩니다.
  • 이 과정은 변경 사항에 대한 명확한 기록을 남기며, 팀원들이나 사용자들이 어떤 변경 사항이 있었는지 쉽게 파악할 수 있도록 합니다.

3. pnpm run changeset version (버전 업)

  • 이 명령은 Changesets가 생성한 변경 사항 파일을 기반으로 패키지의 버전을 실제로 업데이트하고, 각 패키지의 changelog를 업데이트합니다.
  • 변경 사항에 대한 모든 정보는 패키지별로 관리되며, 이 단계에서 자동으로 패키지 버전이 업데이트됩니다.

4. 커밋

  • 버전 업데이트와 changelog 변경 사항을 Git 저장소에 커밋합니다. 이 커밋에는 버전이 업데이트된 패키지와 변경된 changelog 내용이 포함됩니다.

5. PR(풀 리퀘스트) 및 리뷰

  • 변경 사항을 메인 브랜치(예: master)에 병합하기 전에, 풀 리퀘스트를 생성합니다. 이는 코드 리뷰 과정을 통해 변경 사항을 검토하고 팀 내에서 합의를 이루기 위함입니다.
  • 팀원들이 변경 사항을 리뷰하고, 필요한 피드백을 제공합니다. 리뷰 과정을 통해 코드 품질을 유지하고, 오류를 사전에 방지할 수 있습니다.

6. master 브랜치에 반영

  • 리뷰를 통과한 후, 풀 리퀘스트를 메인 브랜치에 병합합니다. 이는 변경 사항이 공식적으로 프로젝트에 반영되었음을 의미합니다.

7. GitLab Runner에서 자동 배포

  • 메인 브랜치에 병합된 후, GitLab CI/CD 파이프라인이 실행됩니다. GitLab Runner는 이 파이프라인을 통해 정의된 배포 작업을 자동으로 수행합니다.
  • 배포 과정은 테스트, 빌드, 배포 단계를 포함할 수 있으며, 이 모든 과정은 자동으로 실행됩니다. 성공적으로 배포가 완료되면, 변경된 사항이 실제 환경에 적용됩니다.

** package.json에 명시한 build 결과물 및 publish config의 exports 파일만 배포하므로 꼼꼼히 확인해주세요.

private(사내망) 배포 방법

 

1. 최상위 .npmrc 에 registry를 설정합니다. (구현되어 있음)

@TDS:registry=http://#######/api/v4/projects/311/packages/npm/

위 설정의 뜻은 @TDS 라는 이름을 앞에 붙인 라이브러리를 사내에(private registry) 배포 및 사용하겠단 뜻입니다.

사용하는 프로젝트의 .npmrc에도 동일하게 작성합니다

 

2. 배포하고 싶은 라이브러리의 package.json을 설정합니다. (중요!)

{
  "name": "@TDS/radix_components",
  "version": "0.0.0",
  "sideEffects": ["./src/index.scss"],
  "license": "MIT",
  "exports": {
    ".": {
      "require": "./src/index.tsx",
      "import": "./src/index.tsx"
    },
    "./index.scss": "./src/index.scss",
    "./package.json": "./package.json"
  },
  "source": "src/index.tsx",
  "main": "dist/index.js",
  "files": [
    "dist/**"
  ],
  "scripts": {
    "test": "jest --passWithNoTests",
    "build": "tsup src/index.tsx --format esm,cjs --dts --external react",
    "parcel:build": "parcel build",
       .... 
  },
  "devDependencies": {
...
  },
  "dependencies": {
....
  },
  "resolutions": {
   ....
  },
  "publishConfig": {
    "access": "restricted",
    "registry": "http://####/api/v4/projects/311/packages/npm/",
    "exports": {
      ".": {
        "require": "./dist/index.js",
        "import": "./dist/index.mjs",
        "types": "./dist/index.d.ts"
      },
      "./package.json": "./package.json",
      "./index.css": "./dist/index.css"
    },
    "import": "./dist/index.mjs",
    "main": "./dist/index.js",
    "module": "./dist/index.mjs",
    "types": "./dist/index.d.ts"
  }
}

 

여기서 중요하게 볼 것은 exports와 publish config, private, sideEffects, name, version 입니다.

name

  • 라이브러리의 고유한 이름을 설정합니다. 여기서는 @TDS/radix_components로, @TDS는 범위(scope)를 나타내며, 같은 범위 내에서 유니크한 이름을 가지게 합니다. npm 등의 패키지 매니저를 통해 이 이름으로 라이브러리를 찾고 설치할 수 있습니다.

version

  • 라이브러리의 현재 버전을 나타냅니다. 세마틱 버저닝(semantic versioning) 원칙에 따라 major.minor.patch 형식으로 관리됩니다. 초기 상태인 0.0.0에서 시작하여, 라이브러리가 업데이트될 때마다 적절한 버전 업을 합니다.

sideEffects

  • 패키지가 부수 효과(side effects)를 가지는지 여부를 웹팩(Webpack) 같은 모듈 번들러에 알립니다. 여기서는 ./src/index.scss 파일이 부수 효과를 가진다고 명시되어 있어, 이 파일을 제외하고 트리 쉐이킹(tree shaking)을 적용할 수 있습니다.

exports

  • 패키지의 내보내기(entry points)를 정의합니다. 이는 패키지를 사용하는 소비자가 접근할 수 있는 모듈의 경로를 명시합니다. 예를 들어, require 또는 import를 통해 메인 모듈을 가져오거나, 추가적인 파일(index.scss)에 대한 접근 방법을 제공합니다.
  • 개발자 경험 향상을 위해서 모노레포 내에서는 코드를 참조합니다. 해당 방식을 사용하지 않으면 merge 및 테스트때마다 강제로 빌드를 한번씩 실행해줘야 합니다.

publishConfig

  • 패키지를 배포할 때 사용할 설정을 정의합니다. access 필드로 접근성을 설정하며, registry 필드는 패키지가 배포될 npm 레지스트리의 URL을 지정합니다. 또한, exports 섹션은 배포된 패키지의 내보내기 설정을 다시 정의하여, 사용자가 패키지의 빌드된(dist) 버전을 사용할 수 있게 합니다.
  • 개발자 경험을 향상하기 위해서 모노레포 빌드시 해당 옵션을 사용하도록 합니다.

private

  • (이 설정은 예시에 명시되어 있지 않지만 중요합니다) 이 필드가 true로 설정되면, 패키지가 비공개로 설정되어 npm 등의 공개 레지스트리에 배포되지 않습니다. 주로 개인 프로젝트나 팀 내부에서 사용할 패키지에 적용됩니다.
  • configs 레포지토리들은 private로 적용합니다.

 

gitlab의 package registry에서 현재 배포된 파일들을 확인 가능합니다

 

사내에 배포시 gitlab의 package registry에서 현재 배포된 파일들을 확인 가능합니다.

라이브러리 사용 방법

  "dependencies": {
    "@DB3/designFiles": "^0.0.0",
    "@DB3/tttable": "1.0.2",
       ...
   }

“내부망에 접속된 컴퓨터”에서 package.json에 명시해서 사용하면 됩니다.

 

CSS 관리법 - Design Variable

Figma Variable은 Figma 디자인 도구에서 사용되는 기능으로, 사용자가 디자인 내에서 텍스트나 색상과

같은 속성을 변수로 설정하여 재사용할 수 있게 해주는 기능입니다.

 

design variable에 대한 간단한 설명

 

개발의 "variable(변수)"처럼 dark mode, theme 등 여러 상황에 알맞게 값을 바꿔주는 기능
color, text 등 여러 값을 유연하게 변경 가능

반영 방법

enterprise 계정에서만 figma web api를 통한 auto import 가능 (우리 회사는 해당 없음)

그래서, figma plugin을 활용해서 design variable은 variable2CSS plugin으로 받아오고

typography 관련은 global style to code plugin으로 수동으로 받아오는 식으로 구현해야합니다.

피그마 기준으로 plugin을 통해 컬러를 뽑아내어 사용합니다.

절대로 임의의 컬러를 추가해서 사용하면 안됩니다.

해당 파일은 packages/designFiles 라이브러리 내부에 구현하고, 공유해서 사용합니다.

ex)

[data-theme="light"] {
  /* colors */
  --adaptive-blanket-layout-split: var(--adaptive-grey-opacity-grey-opacity300, #001d3a2e);
  --adaptive-blanket-modal-background: var(--adaptive-grey-opacity-grey-opacity500, #03183275);
  --adaptive-blue-blue100: #cae8ff;
  --adaptive-blue-blue1000: #004491;
  --adaptive-blue-blue1100: #003571;
  ....(생략)
  }

  [data-theme="dark"] {
  /* colors */
  --adaptive-blanket-layout-split: var(--adaptive-grey-opacity-grey-opacity300, #001d3a2e);
  --adaptive-blanket-modal-background: #13151799;
  --adaptive-blue-blue100: #00326a;
  --adaptive-blue-blue1000: #98cefd;
  --adaptive-blue-blue1100: #b3defe;
  ...(생략)
  }

darkMode, lightMode 두가지 테마를 variable을 토글되게 해서 사용하고, semantic한 네임을 사용하도록 ux연구원분들께 요청합니다.

어떻게 모드를 바꾸는지는 designFiles/setDarkMode.ts 코드를 참고합니다.

다크모드를 적용할 생각이 없어도 해당 방식으로 구현해야 편할껍니다...

*프로젝트에서 실제로 어떻게 사용할껀지는 논의가 필요함.

가장 쉬운 방법은 import ‘designFiles/index.css’ 이다.

테스트(JEST) 사용법

예시 config 파일

const { dirname, join } = require('path')
const path = require('path')

const jestConfig = require('jest-config/jest.config.js')

const customJestConfig = {
  ...jestConfig,

  testEnvironment: 'jsdom',

  moduleNameMapper: {
    ...jestConfig.moduleNameMapper,

    ...{ '@table/(.*)$': `${__dirname}/../@TDS-TTTable/src/$1` },
  },
}

module.exports = customJestConfig

jest-config 레포지토리에서 jest.config.js와 setupTests.ts를 가져옵니다.

describe('Circle class', function() {
  describe('when area is calculated', function() {
    it('it should sets the radius', function() { ... });
    it('it should sets the diameter', function() { ... });
    it('it should sets the circumference', function() { ... });
  });
});

 

테스트는 describe-it pattern 으로 가독성 있게 작성합니다.

https://woonjangahn.gitbook.io/logs/typescript/testing/jest-describe-it

내부는 when then given pattern에 맞게 작성합니다.

https://brunch.co.kr/@springboot/292

프론트엔드 특성상 스타일과 사용 라이브러리가 자주 바뀔 일이 많습니다.

스타일은 테스트 하지 말고 로직(hook)과 컴포넌트 단위로 테스트를 작성합니다.

코드 한줄 한줄을 검증하는 화이트박스가 아니라

블랙박스 테스트, 유저의 동작을 end-to-end로 검증하는 테스트 위주로 작성합니다.

유용한 명령어들

최상위 package.json 참고

  • pnpm build - Storybook 를 포함한 모든 패키지 빌드
  • pnpm dev - 모든 패키지를 로컬에서 실행하고 Storybook으로 미리보기
  • pnpm lint - 모든 패키지에 대해 lint 진행
  • pnpm changeset - changeset 생성
  • pnpm clean - 모든 node_modulesdist 폴더 정리 (각 패키지의 clean 스크립트 실행)
반응형
반응형

next14로 사이드 프로젝트를 진행중인데, next14에서 빠른 로딩 및 UX를 위해서 treeshaking을 어떻게 하면 잘 할수 있는지 찾아봤고 요약해 공유한다.

 

1. next는 페이지 기반 code split을 자동으로 해준다.
production 모드에서 확인 가능

 

2. next는 webpack 기반이다. next에서 babel 대신 swc 기반으로 동작한다.

 

3.  전역 변수 등도 쓰이지 않는다면 webpack이 자동으로 트리쉐이킹 해준다. next 및 webpack version이 높아지면서 성능이 좋아졌다.

 

4.  package.json 에서 sideEffects: false 옵션을 키면 코드들이 side Effect 가 없다고 가정하고, 휴리스틱하게 트리쉐이킹 해준다. 혹은 아래와 같이 특정 폴더만 side Effect 가 있다고 지정할 수도 있다.

{
  "name": "your-project",
  "sideEffects": ["./src/some-side-effectful-file.js"]
}

 

혹은 PURE 옵션을 이용 가능하다.

const Button$1 = /*#__PURE__*/ withAppProvider()(Button);​

 

공식 문서에서 위와 같이 pure 옵션을 사용해서 side-effect-free인 코드라고 명시 가능하다고 설명한다만 라이브러리에서 실제로 쓰는건 별로 못본듯..?

 

테스트 결과 전역 클로저, 클래스 등도 모두 트리쉐이킹 된다.

다만, 아래와 같은 케이스를 조심해야한다.

 

1. 함수 내부에서 외부의 값에 영향을 준다든지(side Effect) 예측 불가능한 동작을 한다면 트리쉐이킹이 정상적으로 동작하지 않을 수도 있다.

순수함수를 쓰는게 코드 관리에도 좋고 성능에도 좋으니 순수함수적으로 짜도록 더욱 노력하면 좋을것 같다. 
 

2. ES6 문법(imprt, export)등을 사용해야한다. module.exports 등 es5 문법을 사용하면 트리쉐이킹이 안될 수도 있다.
특히, babel을 쓴다면 주의

{
  "presets": [
    ["env", {
      "modules": false
    }]
  ]
}​

 
babel-preset-env를 쓴다면 .babelrc에서 ES6 모듈만 남도록 옵션을 설정한다.

 

3. 라이브러리 등을 import시

import * as lodash from  'lodash-es'

 

위 방법을 쓴다면 무엇을 원하는지 알기 쉽지 않아서 트리쉐이킹 최적화가 힘듬

 

import { flow } from 'lodash-es'
import { Box } from '@mui/material'
 

필요한 부분만 import해서 사용!

 

export * from '~~' (barrel pattern)등도 과거에는 트리쉐이킹이 안되었으나 최신 버전은 지원한다.

 

참고)

 

https://webpack.js.org/guides/tree-shaking/

 

vercel/next.js#49742

 

^ 과거에 올라온 이슈인데 아직도 글이 써지는걸 보니까 관련 이슈가 핫한 모양..

반응형
반응형

책의 내용을 요약하자면 "세상의 모든 일은 우연이다."로 요약할 수 있다. 

 

보통 사람들은 A를 했으면 B가 일어난다는 인과론적 사고로 생각하지만 실제로 세상은 복잡계이기 때문에 A 만으로 B가 일어난다고 말할 순 없고, A가 있으면 B가 일어날 확률이 높아질 수 있다는 확률론적 사고로 세상을 바라봐야 한다고 저자는 말한다.  

 

이를 설명하기 위한 예시로 전쟁을 예시로 드는데,  명장은 적은 병력으로 많은 적을 격파하는 장군이 명장이 아니고 

항상 이길 수 있는 싸움, 이길 확률을 높이는 싸움을 만드는 사람을 명장이라고 저자는 설명한다.

 

예를 들어서 오다 노부나가는 오케하자마 전투에서 2000명의 적은 병력으로 25000명의 적군중 5000명의 적 장군 본대를 우연히 찾아 기습해서 이긴적이 있었다. 25000명의 적군이 동시에 달려들었으면 오다 노부나가는 전멸했을텐데 우연히 전장에 폭우가 쳐서 적군이 일사분란하게 움직일 수 없었고, 첩자가 우연히 적의 본대를 발견했기 때문에 온 힘을 쥐어짜서 적의 지휘계통을 무력화할 수 있었다.

 

저자는 오다 노부나가가 이때의 운을 믿고 또 무모한 도전을 반복했으면 오다 노부나가는 패망했을것이라고 설명한다. 하지만 이후 노부나가는 신중하게 이길 수 있는 싸움만 했고, 역사에 한 획을 긋는 위대한 명장으로 남게 되었다고 설명한다.

 

이처럼 모든 일은 확률에 따라 일어나고 일어나지 않을 수 있다. 그래서 옛말의 진인사대천명(盡人事而待天命)이라는 말처럼 자신이 할 수 있는 일을 모두 진행했으면 그 다음은 하늘에 맡겨야 할 것 같다.

 

흔히 운7기3이라는 말이 있는데, 운 7을 살리기 위해서 기3를 잘 닦아놔야 할 것 같다. 

 

반응형

'독서 > 독후감' 카테고리의 다른 글

독후감 - 결국 해내는 사람들의 원칙  (0) 2024.02.18
독후감 - 부의 추월차선  (1) 2024.02.17
반응형

 

결국 해내는 사람들의 원칙 - 밀리의 서재

 

사람의 뇌에는 RAS(Reticular Activating System, 망상 신경계)라는 기관이 있다.

Reticular Activating System, 그림 출처 https://integratedlistening.com/blog/meet-the-reticular-activating-system-ras/

 

우리 뇌는 1초에 4억 비트 이상의 정보를 처리 가능하다. 하지만 과부하를 방지하기 위해서 이중 2000비트 정도만 의식에 들어오고 나머지는 무의식적으로 처리된다.

 

즉, 사람은 자신이 생각하고 의식하는 정보만 선별해서 처리하려고 노력하는 식인데

예시로 중국어를 새로 공부하기 시작하면 그 전에는 신경쓰지 않았던, 지나가는 중국인의 말소리가 들리는 식이다.

 

그래서 저자는 긍정적인 생각을 하는 사람은 낙관적인 정보만 들려오니 이를 이루기 위해 행동할 것이라고 주장하고

반대로 부정적인 사람은 안될것이라는 자기파괴적인 정보만 취사 선택해서 들으니 될 것도 안되게 만들 것이라고 주장하고 있다. 

 

서커스에서 코끼리를 어릴때부터 쇠사슬로 묶어놓는다면, 코끼리가 커서 쇠사슬을 간단히 파괴 가능한 힘을 가지게 되어도 탈출 시도를 하지 않는다고 한다. 이처럼 불가능한 일처럼 보여도 시도를 했으면 일단 성공할 확률이 생길텐데, 시작조차 안하면 아예 성공 확률이 0%일 것이다.

 

저자는 불가능해 보이는 일이라도 계속 도전하면 RAS가 성공을 위해 길을 이끌것이고, 언젠가는 성취한다는 믿음을 갖고 있다. "어떻게" 보다는 "무엇을" 원하는지 정한다면 RAS가 성공을 위해 길을 이끌 것이다.

 

예를 들자면, 해리포터의 저자인 J.K 롤링이 10개 정도의 출판 시도후 낙담했다면 해리포터는 탄생하지 않았을 것이고, 월트 디즈니가 299번째의 테마파크 구상에서 포기했다면 디즈니랜드는 탄생할 수 없었다고 한다.

 

결론적으로 내 인생은 나의 것이니, 남의 평가에 휘둘려 포기하지 말라는 메세지를 던지고 있다. 

 

결론

다만 "도박을 해서 부자가 된다."라는 목표를 세우게 되면 어떻게 될지?

저자의 의견을 맹목적으로 수용하진 말자.

물고기가 새처럼 날고 싶다는 목표를 세운다면 날 수 있을까?

도전도 좋지만 성공 확률을 객관적으로 판단 가능하게 메타인지를 키우는 것도 중요할 것 같다.

 

이 책에서 취할 수 있는 것은 다음과 같다고 생각된다.

 

1.  RAS 시스템상 긍정적으로 생각해야 인생을 즐겁게 보낼 수 있다.

2. 내 인생의 주인공은 나니까 남에게 휘둘리지 않기 

3. 도전도 좋지만 성공 확률을 객관적으로 판단 가능하게 메타인지를 키우자! 실현 가능하게 능력도 키우자!

반응형

'독서 > 독후감' 카테고리의 다른 글

확률적 사고의 힘 리뷰  (0) 2024.02.18
독후감 - 부의 추월차선  (1) 2024.02.17
반응형

몇일 전 밀리의 서재를 결제했다.

 

AI 기술의 발전으로 인류는 그 어느 시대보다도 정보에 가장 쉽게 접근할 수 있지만, 그 중에서 양질의 정보를 찾아내기 가장 어려운 시대에 도래했다고 생각된다. 그래서 디지털 리터러시를 키우기 위해 앞으로 퇴근 후 짬짬이 책을 한두 권씩 볼 예정..!

벌써 4일간 4권이나 읽었다.

 

독후감 - 부의 추월차선 후기

 

일단 이 책에 대해 찾아보니 불쏘시개에 비유할 정도로 논란이 많은데 많은 사람들의 인생 방식이 틀렸다고 하는 자극적인 내용과 "나는 이렇게 하니 되었다."라는 성공 스토리가 대다수라 좀 거부감이 들 수 있다고 생각된다. 책을 읽으며 얻을 건 얻고 필요 없는 부분은 버리는 식으로 생각하자.

 

책의 저자는 가난한 어린 시절을 보냈는데, 어느 날 람보르기니를 몰던 자수성가한 사람을 발견하고 큰 깨달음을 얻었다고 한다. 그래서 젋었을때 백만장자가 되는 방법을 연구했고, 바로 행동으로 옮겨서 큰 부를 얻었다.

 

이 과정에서 깨달은 부자가 되는 공식을 제시한다.

 

부자가 되는 공식

 

1. 지도 (나아가야 할 방향)

2. 차량 (자기 자신)

3. 속도 (생각을 행동으로 옮기는 추진력)

 

여기서 가장 중요한것은 부자까지 가는 지도이다.

사람들은 크게 3가지 방식을 이용한다고 제시한다.

 

1. 인도로 가는 지도

돈을 잘 벌든 말든 상관 없이 재무적 목적지가 존재하지 않는 사람으로, 하루 벌어 하루 쓰는 사람들을 뜻한다. 

어떤 팝스타의 한 달 소득이 40만 달러였지만 수입이 바로 끊기자 파산하는 케이스를 예시로 들었다. 

 

2. 서행차선으로 가는 지도

 

재무적 지식은 있지만 늙어서 부자가 되는 사람들을 뜻한다.

대부분의 제태크 책들은 부동산, 주식 등에 30~40년 투자해서 여유로운 은퇴생활을 즐기도록 부추기는데 저자는 늙어서 돈을 벌면 무슨 소용이 있냐고 젋은 나이에 돈을 불려야한다고 주장한다. (이 내용은 3번에 후술)

 

또한, 절약으로는 절대로 부자가 될 수 없다고 말하면서 지출보다는 소득을 늘려야 한다고 주장한다.

 

3. 추월 차선으로 가는 지도

 

직장은 시간을 팔아 돈을 얻는 방법이라고 소개하면서 저자는 부자는 시간으로부터 자유롭고, 돈으로부터 자유로운 사람을 뜻한다고 한다. 

 

이를 위해 저자는 창업을 추천하는데, 내가 일하는게 아니라 나를 위해 일하는 사람을 고용하고, 내가 일을 하지 않아도 시스템이 자동으로 돈을 벌어오는 현금 흐름을 만들어야 한다고 주장한다.

 

예시로 저자는 웹 페이지를 만들어 자동적으로 나오는 광고 수입으로 큰 소득을 올렸고, 끝내는 다른 회사에 팔아 큰 수익을 챙겼다고 한다.

혹은 창업 외에도 발명, 개발 등 현금 흐름을 기하급수적으로 발생시키는 어려가지 방법을 제시한다.

 

결론

 

보통 재테크 책이라고 하면 2번인 서행차선 방식을 추천하고, 부동산 주식 채권 등 금융 자산으로 현금 흐름을 늘리라는 식의 추천을 하는데

이 책은 하이리스크 하이리턴인 창업을 제시하다니.. 부자가 되는 방식은 쉽지 않구나 생각이 든다.

 

이 책에서 취할 수 있는 것은 다음과 같다고 생각된다.

 

- 일을 안해도 자동으로 현금 흐름이 생기는 시스템을 생성해라.

- 많은 일 => 많은 소득이 아니라 많은 일 => 더 많은 일이다.

 

또한,  저자가 부자가 된 방식인 코딩을 나는 운좋게도 전공으로 삼았고, 할 수 있다. 창업 이외에도 사이드 프로젝트등 여러 방법을 사용 할 수 있지 않을까?

반응형

'독서 > 독후감' 카테고리의 다른 글

확률적 사고의 힘 리뷰  (0) 2024.02.18
독후감 - 결국 해내는 사람들의 원칙  (0) 2024.02.18
반응형

사이드 프로젝트를 위해 chatgpt api를 사용해 보던 중 최근 신기한 기능을 찾아서 간단히 요약해본다.

 

gpt에게 어떤 '상황'을 부여하고, 문맥을 기억할 수 있게 하는 assistant api가 새로 생겼다.

이를 통해 chatgpt를 프롬프트 엔지니어링을 통해 자신이 원하는대로 커스텀 가능하게 되었는데 

국내엔 관련 자료가 많지는 않은거 같아서 기록겸 요약해본다.

 

gpt store에 올리는 custom gpt가 이 assistant와 비슷한 개념일듯?

 

사전 지식

 

chatgpt에게 api로 명령을 보낼때는 message role로 "user", "assistant", "system"을 보내어 원하는 명령을 수행시킨다.

 

'user'는 유저가 입력한 명령을 의미한다.

'system'은 chatgpt에게 입력할 제약 사항, 요구 사항 등을 의미한다.

'assistant'는 명령을 수행하기 위한 앞뒤 문맥, 사전 지식등을 뜻한다.

 

api example)

curl https://api.openai.com/v1/chat/completions \
 -H "Authorization: Bearer $OPENAI_API_KEY" \
 -H "Content-Type: application/json" \
 -d '{
 "model": "gpt-3.5-turbo",
 "messages": [
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "user", "content": "Who won the world series in 2020?"},
    {"role": "assistant", "content": "The Los Angeles Dodgers won the World Series in 2020."},
    {"role": "user", "content": "Where was it played?"}
    ]
 }'

 

 

기존 api는 독립적으로 작동하기 때문에 앞뒤 문맥을 파악하지 못했다.

이를 위해서 assistant role로 이전의 대화 내용을 전부 보내주거나 요약해줘야 했고, 이는 api의 과다 사용 = 비용의 상승으로 이어졌다. 

 

assistant api는 이 문제를 해결하기 위해서 미리 학습시킨 system, assistant 값을 이용해 유저의 api 명령을 처리한다.

이 api를 사용하기 위해서 몇가지의 개념 학습이 더 필요하다.

 

https://platform.openai.com/docs/assistants/how-it-works/objects

 

1. Assistant

명령을 받아 처리하는 chatgpt 자체를 뜻한다.

role system, assistant 들로 사전 입력을 할 수 있다.

이를 위해서 assistant는 text 파일 같은 file 입력도 가능하며 model 의 튜닝도 가능하다. 

 

2. Thread 

명령이 이루어지는 "context", 대화 맥락을 뜻한다.

채팅방이라고 생각하면 될 듯하다.

Thread간의 context는 독립적이다. 이 말은 즉, 카카오톡 1:1 채팅방처럼 여러 thread가 있을 수 있다.

 

3. Message

기존의 chatgpt api message와 동일하다. 

assistant api에선 thread 단위로 message간의 대화 맥락, context를 파악해서 ai가 대화를 이어나가준다.

 

4. RUN

run의 결과값

message를 입력한 이후의 결과 객체이다. 

코드 레벨에서 의미 있는 객체인데 뭔 역할인지 코드 예시에서 후술함

 

 

여기서 api를 사용하기 위해서 assistant, thread, message를 어떻게 코드 레벨에서 동작시키는지 좀 혼란을 겪었는데 사이드 프로젝트의 코드 예시로 설명하겠다.

 

개인적으로는 상당히 복잡하고, chat gpt한테 보낸 명령이 완료되었는지 확인하기 위해서

pooling을 써야하는 부분이 있어서 비동기 처리 측면에서 상당히 헷갈렸던듯..?

 

지금 next14에서 실험중이니 next 코드로 설명함 

전체 코드는 아래와 같다.

/* eslint-disable no-await-in-loop */
import { NextRequest, NextResponse } from 'next/server'
import { OpenAI } from 'openai'

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
})

async function handler(req, res) {
  try {
    const body = await req.json()
    
    const assistant = await openai.beta.assistants.retrieve(' assistant api id 추가! ') // TO DO - tutor 별로 assistant 생성
	const thread = await openai.beta.threads.retrieve('thread api id 추가 !') // TO DO - 유저 대화창별로 thread 생성

    const { excelJSON, prompt } = body

    const message = await openai.beta.threads.messages.create(thread.id, {
      role: 'user',
      content: prompt,
    })

    const run = await openai.beta.threads.runs.create(thread.id, {
      assistant_id: assistant.id,
    })

    let cnt = 0

    // TO DO - polling logic 업그레이드
    while (cnt < 1000) {
      const { status } = await openai.beta.threads.runs.retrieve(thread.id, run.id)

      if (status === 'completed') break
      await new Promise((resolve) => {
        setTimeout(() => {
          resolve(1)
        }, 500)
      })
      cnt += 10
    }

    const messages = await openai.beta.threads.messages.list(thread.id)
    // @ts-ignore
    const responseText = messages.data[0].content[0].text.value
    console.log(JSON.stringify(responseText), responseText)
	
	... 생략(뒤는 중요하지 않은 파트임)
}

export { handler as POST }

 

 

1. assistant와 thread 불러오기

 

/* eslint-disable no-await-in-loop */
import { NextRequest, NextResponse } from 'next/server'
import { OpenAI } from 'openai'

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
})

async function handler(req: NextRequest, res: any) {
  try {
    const body = await req.json()
    
    const assistant = await openai.beta.assistants.retrieve(' assistant api id 추가! ') // TO DO - tutor 별로 assistant 생성
	const thread = await openai.beta.threads.retrieve('thread api id 추가 !') // TO DO - 유저 대화창별로 thread 생성

    const { excelJSON, prompt } = body

 

앞 부분은 미리 생성된 assistant를 불러오고, 기존에 채팅방(thread)를 불러오는 api이다.

어시스턴트와 thread는 코드 레벨에서도 구현이 가능하고 혹은 playground에서도 생성이 가능하다.

 

숨김 처리한 부분이 각각 assistant id, thread id이다.

 

2. message 전달 후 chatgpt 실행

    const message = await openai.beta.threads.messages.create(thread.id, {
      role: 'user',
      content: prompt,
    })

    const run = await openai.beta.threads.runs.create(thread.id, {
      assistant_id: assistant.id,
    })

  // TO DO - polling logic 업그레이드
    while (cnt < 1000) {
      const { status } = await openai.beta.threads.runs.retrieve(thread.id, run.id)

      if (status === 'completed') break
      await new Promise((resolve) => {
        setTimeout(() => {
          resolve(1)
        }, 500)
      })
      cnt += 10
    }

 

message를 생성하고 chatgpt에게 실행시킨다. 

만약 message 결과값을 확인하고 싶다면 pooling api를 통해 대화가 끝났는지 체크해줘야 한다.. 

run의 결과는 아까 보았던 completed, failed, canceled 등등이 있다.

 

    const messages = await openai.beta.threads.messages.list(thread.id)
    // @ts-ignore
    const responseText = messages.data[0].content[0].text.value
    console.log(JSON.stringify(responseText), responseText)

 

message의 결과값은 커서 페이지네이션의 형태로 message들을 최신순 20개를 받아오는듯하다. 

아직 안써봤지만 message id를 통해 페이지네이션을 조작할 수 있을것이라고 추측 된다.

 

 

아직 베타 버전이라 많은 기능은 없지만

chatgpt의 발전 속도가 매우 무섭다..

 

월급 들어오면

마소 주식이나 좀 사야지 가즈아!!

 

 

reference 

 

https://platform.openai.com/docs/assistants/how-it-works/runs-and-run-steps?lang=curl

 

 

반응형

'인공지능' 카테고리의 다른 글

resnet->densenet  (0) 2022.03.08

+ Recent posts