Typescript Monorepo 탐색(실용적인 실습 모험)

목차



  • The Two Extremes of Code Organization
  • Files & Folders
  • Everything's a Repository


  • Finding the middle ground
  • A note on TypeScript

  • I need your help!
  • So what's the plan?
  • A quick word before we begin
  • The Attempts

  • 나는 코드가 이해할 수 있는 "일"을 수행하는 이해할 수 있고 자체 포함된 덩어리로 패키지되는 "낮은 결합, 높은 응집력"방식의 간단한 코드를 좋아합니다. 그렇게 하면 한 번에 모든 것을 이해할 필요가 없습니다. 대신 높은 수준에서 개요를 파악하고 수행해야 하는 작업과 관련이 있을 때 세부 사항을 자세히 살펴볼 수 있습니다.

    우리 모두는 이미 코드를 이해할 수 있는 추상화로 자릅니다. 함수와 클래스를 별도의 파일과 폴더에 작성합니다. 그러나 프로젝트가 커짐에 따라 코드의 추상화를 계속 구성해야 할 필요성도 커지고 파일 및 폴더만 구성할 수 있는 경우 어느 시점에서 프로젝트가 너무 압도적입니다.

    코드 구성의 두 극단

    This code-organizing dynamic can be thought of as a spectrum, and if we put "files & folders" as the least extreme solution what's the most extreme approach? That's where we split all our code into separate repositories, so our product ends up entirely composed of generic "lego blocks" that snap together and none of the individual parts know about each other. But both these extremes have problems:

      Files & Folders ◄─────────► Everything's a Repository
    

    파일 및 폴더

    This is a great place to start a new project, basically all projects should start here. But there is a scale challenge. Given constant growth it becomes increasingly difficult to keep sub-systems decoupled, because there are no hard separations between systems: Files and folders inevitably degrades into a code-jungle where search-results return too many hits, auto-complete gives too many suggestions, and modules easily end up importing each other in ways that couples concepts together. If you're the original author you might not see that degradation, but newcomers will be increasingly confused and slow to get up to speed. At some point it just becomes too much for newcomers to get an overview, and if you do nothing the code-jungle will spread and suffocate development, and will be a source of countless frustrations and bugs.

    모든 것이 저장소

    On the other side of the spectrum is the Everything's a Repository pattern, where we turn every abstraction into its own separate repository that can be used by possibly many other products. It's like the ultimate open-source dream where all the code lives as independent lego-blocks, and our product just wires together a bunch of separate dependencies and all the details are taken care of by each of those separate projects.

    The end result is complete code isolation: We can open a single repository and really focus on just that one code-concept, there's truly no code-jungle anymore 🎉.

    But this is a dangerous path, it quickly turns into a different jungle: Precisely because each package is so isolated we now have a huge overhead for introducing changes, because each change has to be weaved into the intricate web of sub-projects.

    The challenge is that an individual sub-package has no context of the overall product, so when we dive into one library to make a change we lose sight of the overall product. And it gets very frustrating dealing with the different dependencies and their versions, e.g. if we upgrade one sub-package it becomes a manual process of going through its consumers and make them pull in the new version until we reach our product. And what if we then find the change to the library wasn't quite right for our product? It can be hard to replicate the exact needs of our product inside each library, and this back-and-forth quickly becomes very destructive.

    With just a few separate repositories we'll be spending more time juggling versions and ensuring they all work correctly with each other than we do actually adding valuable changes to our product.

    ℹ️ BTW this "multiple repositories" approach is great for open-source, because that's a low-trust, high-latency work environment where the workflow must be optimized for letting separate groups move at their own individual pace, but it is an extremely poor fit for a team whose value is their product.


    중간 지점 찾기

    This article-series exists because I want to find ways to group code at higher levels than files & folders without suffering the drawbacks of multiple repositories. The Monorepo pattern is the solution, but there are pitfalls and multiple ways of organizing a monorepo that makes this a problem worth exploring.

    This series is all about pragmatism: I expect you and I to be normal "in-the-trenches programmers" who just want to make products, and we don't have time for complex workflows or perfectly divine principles. We want a simple way to organize code into separate projects when and where it makes sense, so code can migrate towards their own apps or shared libraries when their size and complexity warrants it. We want to continuously manage complexity without getting sucked into the jungles of either extremes, and we want to do it in a way that is as straightforward as possible.

    This pragmatism is important because we don't need to find perfection. We just need a straightforward way to extract code. Maybe that code is deep inside the product, maybe it's some hardcoded functions, maybe it's a concept that's been copy-pasted across multiple systems, maybe it lacks tests, whatever the case it's a shared pattern that just needs to be extracted without too much ceremony. It can be improved later, but right now we just want to put a box around it. After all, the whole product can be tested and deployed together, I just want a simple way to continuously refactor so I can avoid the code-jungle.

    Basically we want to find the lowest barrier for grouping pieces of code, with as little technical and workflow overhead as possible to accomplish that.

    ℹ️ BTW the Monorepo pattern is probably more usually seen where each package is versioned and published individually. That's a common pattern for open-source solutions. But that is explicitly not the goal for this series where we focus on a team that just wants to focus on their product, and want a way to organize the code so its easy to understand.

    TypeScript에 대한 참고 사항

    For this guide we're using Nodejs + TypeScript, which unfortunately causes some (or all) of the complexities we're about to encounter. If you're coming from another language you may wonder why these articles exist at all because for you it's easy to extract code into local packages, but for worse or worse it's not that easy in the Nodejs + TypeScript universe… as we're about to see.


    당신의 도움이 필요합니다!

    Spoiler: I don't know what I'm doing! I'm not a Typescript expert, I'm not a Monorepo guru, I can't offer the golden solution for this problem. I need your help to work through ideas and insights to explore the possible solutions. How do you organize your code? Do you have a preferred tool? I'm very interested in exploring what's out there.


    그래서 계획은 무엇입니까?

    First, let's go over the Files & Folders example so we have some starting point to use for exploring the different monorepo solutions. Then we'll move into actually trying various ways of pulling the code-jungle apart.

    ℹ️ BTW, to keep our learnings easy we'll use a simple example to illustrate the complexity of a real product, but as a result it won't actually warrant any code-organizing. So please imagine the code is complex enough that we definitely need to reorganize 😅.

    Let's pretend we're building a web-service called webby, and it's grown to this Files & Folders structure:

    webby
    ├── package.json
    ├── prisma/
    ├── src
    │  ├── analytics.spec.ts
    │  ├── analytics.ts
    │  ├── api.ts
    │  ├── client.tsx
    │  ├── index.ts
    │  ├── logging.ts
    │  ├── pages/
    │  ├── server.tsx
    │  └── types.ts
    ├── tsconfig.json
    └── typings/
    
    1)

    경험 수준에 따라 이 개요에서 제품에 대한 느낌을 얻을 수 있습니다. client.tsx는 프런트엔드와 관련이 있으므로 아마도 server.tsx는 HTML 제공 백엔드일 것입니다. 그러면 api.ts가 백엔드가 되지만 analytics.ts는 무엇에 연결됩니까? 어쩌면 둘 다? 그리고 그 prisma 폴더가 무엇인지 모르십니까? 어떤 영역이 무엇과 연결되어 있는지 어떻게 알 수 있습니까?

    그리고 package.json 파일은 제품에 대한 모든 종속성의 압도적인 상위 집합이기 때문에 개요를 제공하지 않으며 어느 것이 제품의 어떤 부분에 속하는지 알 수 있는 방법이 없습니다.

    이제 막 시작하는 사람의 입장이 되어 보면 개요가 부족하여 제품에 익숙해지기가 어렵습니다. 각 파일이 수백 줄이고 수십 개 이상의 클래스와 함수를 포함하는 경우 모든 파일이 어떻게 조화를 이루는지 이해하기 어려울 것입니다! 이것은 결국 하나의 큰 프로젝트이므로 검색 결과가 너무 많은 유사한 기능을 사용하여 너무 많은 결과를 반환하고 테스트를 실행하는 데 너무 오래 걸리며 정확히 어떻게 파악하기가 너무 어렵다고 상상해보십시오. 모두 잘 맞아서 작업하기 어려운 큰 코드 수프처럼 느껴집니다.

    우리가 모노레포 패턴을 개선하기를 원하는 것은 개요의 부족입니다.

    (이 시점에서 파일 및 폴더를 더 추가하는 것만으로는 해결책이 아니라는 점을 분명히 하고 싶습니다. 그렇게 하면 검색이 더 쉬워지지 않고 테스트를 더 빠르게 실행하는 데 도움이 되지 않으며 도움이 되지 않습니다. 우리의 특정 예가 매우 사소하다는 것을 알고 있지만 이 프로젝트가 너무 복잡해서 주니어 직원이 들어와 폴더, 파일, 클래스, 코드 자체는 잘 구성되어 있지만 더 높은 수준의 추상화가 필요합니다.)


    시작하기 전에 간단한 한마디

    Here's a cheat-sheet dependency graph of how the different modules actually relate to each other:

        ┌─────┐ ┌─────┐
        │ web │ │ api ├─┐
        └────┬┘ └┬────┘ │
             │   │      │
             │   │      │
             │   │      │
           ┌─▼───▼─┐   ┌▼──────────┐
           │ types │   │ analytics │
           └───────┘   └┬──────────┘
                        │
          ┌─────────┐   │
          │ logging ◄───┘
          └─────────┘
    

    These are the "clumps of code" that we'd like to see separated into separate packages. Of course this just reflects my architectural opinions, but let's imagine we've arrived at this diagram together as a result of great collaborative meetings.

    Starting web is straightforward:

    $ npm ci
    $ npm run web:start
    > Started on port 3000
    

    And ditto for api :

    $ npm run api+db:start
    [api] api started at http://localhost:3002
    
    It's not really important what "webby" really is, but just to satisfy anyone curious web is a simple React frontend that queries api for data, and the actual "product" looks like this:


    그것이 무엇을 하는지는 그렇게 중요하지 않습니다. 우리는 단지 그것을 재구성할 필요가 있습니다 😂.


    시도

    Below is the list of attempts, please add suggestions for tools or methodologies I haven't tried, the whole point of this article-series is to learn the different ways of arranging code.

    좋은 웹페이지 즐겨찾기