오늘날 최고의 모노레포 솔루션의 장단점을 살펴봅시다.
아래 링크를 통해 전체 시리즈를 살펴보세요.
이 글은 Nx
, PNPM
, Turborepo
의 기능, 성능, 프로젝트 적합성을 비교하는 시리즈의 일부입니다.
모노레포 관리 및 빌드 시스템의 기본 사항을 살펴본 후, 이번 글에서는 이 분야의 강력한 도구인 NX
에 대해 자세히 알아보겠습니다. ✨
다시 한번 말씀드리지만, 우리의 목표는 개발 워크플로우를 간소화하고 코드 베이스 관리를 개선할 수 있는 최적의 솔루션을 찾는 것입니다.
최고의 모노레포가 빛나길 바랍니다! 우리가 함께 정복할 도전은 다음과 같습니다.
다음 단계가 궁금하신가요? 함께 알아보시죠! 🚀 🌟
NX의 전체 기능은 다음과 같습니다.
그리고 NX 워크플로우의 대략적인 개요는 다음과 같습니다.
+-------------------+ +------------------+ +----------------+
| Workspace Config | -------> | Project Graph | ----------->| Task Graph |
| (workspace.json, | | (DAG: projects | | (DAG: tasks) |
| project.json) | | & deps) | | |
+-------------------+ +------------------+ +----------------+
|
|
v
+------------+
| Task |
| Execution |
| & Caching |
+------------+
이 섹션에서는 주요 구성 요소를 분석하여 작동 방식을 이해하고 장점과 한계를 평가할 수 있도록 합니다.
NX는 모노레포 개발 및 관리를 간소화하도록 설계된 정교한 툴킷입니다.
지능형 프로젝트 그래프 분석, 최적화된 작업 실행, 효율적인 캐싱 및 유연한 플러그인 시스템에 대해 더 자세히 살펴봅시다!
먼저 Nx를 설치하면 일반적으로 콘솔에 다음과 같은 출력이 표시됩니다.
nx % pnpm i -D nx
Packages: +109
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Progress: resolved 118, reused 0, downloaded 109, added 109, done
node_modules/.pnpm/nx@19.3.2/node_modules/nx: Running postinstall script, done in 526ms
devDependencies:
+ nx 19.3.2
Done in 9.1s
🔳 중요한 스크립트인 post-installation script를 살펴보면, packages/nx/bin/post-install.ts
가 실행되어 몇 가지 필수 설정 작업을 수행합니다.
// https://github.com/nrwl/nx/blob/master/packages/nx/bin/post-install.ts
...
(async () => {
const start = new Date();
try {
setupWorkspaceContext(workspaceRoot);
if (isMainNxPackage() && fileExists(join(workspaceRoot, 'nx.json'))) {
assertSupportedPlatform();
try {
await daemonClient.stop();
} catch (e) {}
const tasks: Array<Promise<any>> = [
buildProjectGraphAndSourceMapsWithoutDaemon(),
];
...
이 스크립트에서 두 가지 핵심 함수는 setupWorkspaceContext
와 buildProjectGraphAndSourceMapsWithoutDaemon
입니다.
🔳 setupWorkspaceContext
: 타입스크립트와 러스트 연결하기
이 함수는 Nx의 타입스크립트 코드와 기본 Rust 구현 사이의 다리 역할을 합니다. 프로젝트 그래프 관리를 위한 핵심 로직이 포함된 컴파일된 Rust 모듈을 동적으로 로드합니다.
// https://github.com/nrwl/nx/blob/master/packages/nx/src/utils/workspace-context.ts#L9
export function setupWorkspaceContext(workspaceRoot: string) {
const { WorkspaceContext } =
require("../native") as typeof import("../native");
performance.mark("workspace-context");
// https://github.com/nrwl/nx/blob/master/packages/nx/src/native/workspace/context.rs#L163
workspaceContext = new WorkspaceContext(
workspaceRoot,
cacheDirectoryForWorkspace(workspaceRoot)
);
performance.mark("workspace-context:end");
performance.measure(
"workspace context init",
"workspace-context",
"workspace-context:end"
);
}
여기서 핵심 작업은 Rust 레이어에 WorkspaceContext
인스턴스를 생성하는 것입니다. 이 인스턴스는 파일 작업 및 프로젝트 그래프 구성 등 작업 공간과 상호작용을 하기 위한 중앙 인터페이스가 됩니다.
// https://github.com/nrwl/nx/blob/master/packages/nx/src/native/workspace/context.rs#L163
#[napi]
impl WorkspaceContext {
#[napi(constructor)]
pub fn new(workspace_root: String, cache_dir: String) -> Self {
enable_logger();
trace!(?workspace_root);
let workspace_root_path = PathBuf::from(&workspace_root);
WorkspaceContext {
files_worker: FilesWorker::gather_files(&workspace_root_path, cache_dir),
workspace_root,
workspace_root_path,
}
}
🔳 gather_files
: 비동기 파일 수집(Rust)
Rust 구현(context.rs
)에서 gather_files
함수(WorkspaceContext
생성자에서 호출)는 워크스페이스에서 파일을 효율적으로 수집하고 해싱하는 일을 담당합니다.
// https://github.com/nrwl/nx/blob/master/packages/nx/src/native/workspace/context.rs#L36
// https://github.com/nrwl/nx/blob/master/packages/nx/src/native/workspace/context.rs
impl FilesWorker {
fn gather_files(workspace_root: &Path, cache_dir: String) -> Self {
if !workspace_root.exists() {
warn!(
"workspace root does not exist: {}",
workspace_root.display()
);
return FilesWorker(None);
}
// ... (로깅 및 설정)
let archived_files = read_files_archive(&cache_dir);
// ... (스레드 동기화 설정)
thread::spawn(move || {
// ... (잠금 설정)
let file_hashes = if let Some(archived_files) = archived_files {
selective_files_hash(&workspace_root, archived_files)
} else {
full_files_hash(&workspace_root)
};
// ... (파일 데이터 처리 및 정렬)
*workspace_files = files; // 공유된 벡터에 파일 데이터 저장
// ... (메인 스레드에 알림 및 캐시 적용)
cvar.notify_all();
....
write_files_archive(&cache_dir, file_hashes);
});
FilesWorker(Some(files_lock)) // FilesWorker 인스턴스를 반환
}
}
🔵 주요 단계는 다음과 같습니다.
✔️ 워크스페이스 루트 확인: 워크스페이스 디렉터리가 존재하는지 확인합니다.
✔️ 캐시 된 파일 읽기(선택 사항): 이전에 캐시 된 파일 정보를 읽어 증분 업데이트 속도를 높입니다.
✔️ 백그라운드 스레드 생성: 파일 해싱을 비동기적으로 처리하기 위해 별도의 스레드를 생성합니다(thread::spawn
).
✔️ 파일 해싱.
selective_files_hash
, 그렇지 않은 경우에는 full_files_hash
사용).✔️ 데이터 저장소: 정렬된 파일 데이터를 공유 작업공간 파일 벡터에 저장합니다.
✔️ 알림 및 캐싱: 데이터가 준비되었음을 메인 스레드(cvar.notify_all()
)에 알리고 파일 데이터를 캐시에 적용합니다(write_files_archive
).
https://nx.dev/concepts/how-caching-works
🔳 buildProjectGraphAndSourceMapsWithoutDaemon
: 프로젝트 그래프 생성 오케스트레이션
buildProjectGraphAndSourceMapsWithoutDaemon
함수는 Nx 데몬을 사용하지 않을 때 Nx 프로젝트 그래프 및 관련 소스 맵을 빌드하는 전체 프로세스를 오케스트레이션합니다.
// https://github.com/nrwl/nx/blob/master/packages/nx/src/project-graph/build-project-graph.ts#L1
// https://github.com/nrwl/nx/blob/master/packages/nx/src/project-graph/project-graph.ts#L92
...
export async function buildProjectGraphAndSourceMapsWithoutDaemon() {
global.NX_GRAPH_CREATION = true;
const nxJson = readNxJson();
...
let configurationResult: ConfigurationResult;
let projectConfigurationsError: ProjectConfigurationsError;
const [plugins, cleanup] = await loadNxPlugins(nxJson.plugins);
try {
configurationResult = await retrieveProjectConfigurations(
plugins,
workspaceRoot,
nxJson
);
} catch (e) {
if (e instanceof ProjectConfigurationsError) {
projectConfigurationsError = e;
configurationResult = e.partialProjectConfigurationsResult;
} else {
throw e;
}
}
const { projects, externalNodes, sourceMaps, projectRootMap } =
configurationResult;
....
const { allWorkspaceFiles, fileMap, rustReferences } =
await retrieveWorkspaceFiles(workspaceRoot, projectRootMap);
...
const cacheEnabled = process.env.NX_CACHE_PROJECT_GRAPH !== 'false';
...
let projectGraphError: AggregateProjectGraphError;
let projectGraphResult: Awaited<
ReturnType<typeof buildProjectGraphUsingProjectFileMap>
>;
try {
projectGraphResult = await buildProjectGraphUsingProjectFileMap(
projects,
externalNodes,
fileMap,
allWorkspaceFiles,
rustReferences,
cacheEnabled ? readFileMapCache() : null,
plugins,
sourceMaps
);
} catch (e) {
if (isAggregateProjectGraphError(e)) {
projectGraphResult = {
projectGraph: e.partialProjectGraph,
projectFileMapCache: null,
};
projectGraphError = e;
} else {
throw e;
}
} finally {
// 플러그인이 격리된 경우 CLI를 한번 실행하는 동안에는 cleanup 하지 않습니다.
// CLI 프로세스가 종료될 때 cleanup 됩니다.
// 여기서 cleanup 하면 보류 중인 promise가 해결되지 않을시 문제가 발생할 수 있습니다.
if (process.env.NX_ISOLATE_PLUGINS !== 'true') {
cleanup();
}
}
...
🔵 주요 단계는 다음과 같습니다.
✔️ 구성(Configuration) 및 플러그인 로드하기: nx.json
구성을 읽고 관련 플러그인을 로드합니다.
const nxJson = readNxJson();
....
// https://github.com/nrwl/nx/blob/master/packages/nx/src/config/nx-json.ts#L459
export function readNxJson(root: string = workspaceRoot): NxJsonConfiguration {
const nxJson = join(root, 'nx.json');
if (existsSync(nxJson)) {
const nxJsonConfiguration = readJsonFile<NxJsonConfiguration>(nxJson);
if (nxJsonConfiguration.extends) {
const extendedNxJsonPath = require.resolve(nxJsonConfiguration.extends, {
paths: [dirname(nxJson)],
});
const baseNxJson = readJsonFile<NxJsonConfiguration>(extendedNxJsonPath);
return {
...baseNxJson,
...nxJsonConfiguration,
};
} else {
return nxJsonConfiguration;
}
✔️ 프로젝트 구성을 검색하고 구문 분석하여 프로젝트 정보를 수집합니다.
...
// https://github.com/nrwl/nx/blob/master/packages/nx/src/project-graph/utils/retrieve-workspace-files.ts#L63
export async function retrieveProjectConfigurations(
plugins: LoadedNxPlugin[],
workspaceRoot: string,
nxJson: NxJsonConfiguration
): Promise<ConfigurationResult> {
const globPatterns = configurationGlobs(plugins);
const workspaceFiles = await globWithWorkspaceContext(
workspaceRoot,
globPatterns
);
return createProjectConfigurations(
workspaceRoot,
nxJson,
workspaceFiles,
plugins
);
}
...
// https://github.com/nrwl/nx/blob/master/packages/nx/src/project-graph/utils/retrieve-workspace-files.ts#L82
export async function retrieveProjectConfigurationsWithAngularProjects(
workspaceRoot: string,
nxJson: NxJsonConfiguration
): Promise<ConfigurationResult> {
const pluginsToLoad = nxJson?.plugins ?? [];
if (
shouldMergeAngularProjects(workspaceRoot, true) &&
!pluginsToLoad.some(
(p) =>
p === NX_ANGULAR_JSON_PLUGIN_NAME ||
(typeof p === 'object' && p.plugin === NX_ANGULAR_JSON_PLUGIN_NAME)
)
) {
pluginsToLoad.push(join(__dirname, '../../adapter/angular-json'));
}
...
✔️ 워크스페이스 파일을 검색하여 워크스페이스의 모든 관련 파일에 대한 정보를 수집합니다.
// https://github.com/nrwl/nx/blob/master/packages/nx/src/project-graph/utils/retrieve-workspace-files.ts#L26
/**
* 워크스페이스 디렉토리를 탐색하여 `projectFileMap`, `ProjectConfigurations` 및 `allWorkspaceFiles`를 생성합니다.
* @throws
* @param workspaceRoot
* @param nxJson
*/
export async function retrieveWorkspaceFiles(
workspaceRoot: string,
projectRootMap: Record<string, string>
) {
...
const { projectFileMap, globalFiles, externalReferences } =
await getNxWorkspaceFilesFromContext(workspaceRoot, projectRootMap);
...
return {
allWorkspaceFiles: buildAllWorkspaceFiles(projectFileMap, globalFiles),
fileMap: {
projectFileMap,
nonProjectFiles: globalFiles,
},
rustReferences: externalReferences,
};
}
✔️ buildProjectGraphUsingProjectFileMap
을 사용하는 함수는 수집된 정보를 기반으로 프로젝트 그래프를 구성합니다.
export async function buildProjectGraphUsingProjectFileMap(
projectRootMap: Record<string, ProjectConfiguration>,
externalNodes: Record<string, ProjectGraphExternalNode>,
fileMap: FileMap,
allWorkspaceFiles: FileData[],
rustReferences: NxWorkspaceFilesExternals,
fileMapCache: FileMapCache | null,
plugins: LoadedNxPlugin[],
sourceMap: ConfigurationSourceMaps
): Promise<{
projectGraph: ProjectGraph;
projectFileMapCache: FileMapCache;
}> {
...
return {
projectGraph,
projectFileMapCache,
};
}
🔳 buildProjectGraphUsingProjectFileMap
, ProjectGraphBuilder
, and buildProjectGraphUsingContext
: 이러한 함수는 프로젝트 그래프를 점진적으로 구축하고, 외부 노드를 추가하고, 프로젝트 노드를 정규화하고, 암시적 종속성을 적용하기 위해 사용합니다.
🔳 ProjectGraphBuilder 클래스는 그래프에 노드와 종속성을 추가하고 제거하는 프로세스를 용이하게 합니다:
export class ProjectGraphBuilder {
// TODO(FrozenPandaz): 이것을 비공개로 설정합니다.
readonly graph: ProjectGraph;
...
/**
* 프로젝트 그래프에 프로젝트 노드를 추가합니다.
*/
addNode(node: ProjectGraphProjectNode): void {
// 같은 이름의 프로젝트가 이미 존재하는지 확인합니다.
if (this.graph.nodes[node.name]) {
// 기존 프로젝트가 다른 유형인 경우 Throw 합니다.
if (this.graph.nodes[node.name].type !== node.type) {
throw new Error(
`Multiple projects are named "${node.name}". One is of type "${
node.type
}" and the other is of type "${
this.graph.nodes[node.name].type
}". Please resolve the conflicting project names.`
);
}
}
this.graph.nodes[node.name] = node;
}
/**
* 그래프에서 노드와 모든 의존성 에지를 제거합니다.
*/
removeNode(name: string) {
if (!this.graph.nodes[name] && !this.graph.externalNodes[name]) {
throw new Error(`There is no node named: "${name}"`);
}
...
🔳 ProjectGraph
(DAG(유향 비순환 그래프)
)는 Nx가 효율적인 작업 스케줄링, 종속성 분석 및 증분 빌드를 수행할 수 있도록 하는 중요한 데이터 구조입니다.
/**
* 작업 공간의 프로젝트와 프로젝트 간의 종속성 그래프
*/
export interface ProjectGraph {
nodes: Record<string, ProjectGraphProjectNode>;
externalNodes?: Record<string, ProjectGraphExternalNode>;
dependencies: Record<string, ProjectGraphDependency[]>;
version?: string;
}
'nx test myapp'와 같이 대상을 직접 호출하거나 'nx affected:test'와 같이 영향을 받는 명령을 실행할 때마다 Nx는 먼저 작업 공간 내의 모든 다른 프로젝트와 파일이 서로 어떻게 맞는지 파악하기 위해 프로젝트 그래프를 생성해야 합니다. 당연히 작업 공간이 커질수록 이 프로젝트 그래프 생성에 더 큰 비용이 듭니다. - https://github.com/nrwl/nx/blob/master/docs/shared/concepts/daemon.md?plain=1
🔳 작업 공간을 DAG(일반적인 그래프)로 올바르게 모델링하려면 잘 정의된 응집력 있는 단위(모듈)가 있는 명확한 경계를 갖는 것이 중요합니다.
코드를 잘 정의된 응집력 있는 단위로 분할하면 소규모 조직이라도 수십 개의 앱과 수십 또는 수백 개의 라이브러리를 보유하게 됩니다. 이 모든 것이 서로 자유롭게 의존할 수 있다면 혼란이 뒤따르고 작업 공간은 관리가 불가능해질 것입니다. - https://nx.dev/features/enforce-module-boundaries
🔳 기본적으로 NX는 이러한 긴밀성을 보장하지만, 이 규칙을 강화할 수도 있습니다.
이를 위해 NX는 코드 분석을 사용하여 프로젝트가 서로의 잘 정의된 공용 API에만 의존할 수 있도록 합니다. 또한 프로젝트가 서로 의존할 수 있는 방식에 대해 선언적으로 제약을 가할 수 있습니다. - https://nx.dev/features/enforce-module-boundaries
🔳 작업 오케스트레이션 및 실행: 프로젝트 그래프는 실행할 작업과 그 종속성을 나타내는 작업 그래프(createTaskGraphAndValidateCycles
)를 생성하는 데 사용됩니다.
...
export async function runCommand(
projectsToRun: ProjectGraphProjectNode[],
projectGraph: ProjectGraph,
{ nxJson }: { nxJson: NxJsonConfiguration },
nxArgs: NxArgs,
overrides: any,
initiatingProject: string | null,
extraTargetDependencies: Record<string, (TargetDependencyConfig | string)[]>,
extraOptions: { excludeTaskDependencies: boolean; loadDotEnvFiles: boolean }
): Promise<NodeJS.Process['exitCode']> {
const status = await handleErrors(
process.env.NX_VERBOSE_LOGGING === 'true',
async () => {
const projectNames = projectsToRun.map((t) => t.name);
const taskGraph = createTaskGraphAndValidateCycles(
projectGraph,
extraTargetDependencies ?? {},
projectNames,
nxArgs,
overrides,
extraOptions
);
const tasks = Object.values(taskGraph.tasks);
const { lifeCycle, renderIsDone } = await getTerminalOutputLifeCycle(
initiatingProject,
projectNames,
tasks,
nxArgs,
nxJson,
overrides
);
const status = await invokeTasksRunner({
tasks,
projectGraph,
taskGraph,
lifeCycle,
nxJson,
nxArgs,
loadDotEnvFiles: extraOptions.loadDotEnvFiles,
initiatingProject,
});
await renderIsDone;
return status;
}
);
return status;
}
...
createTaskGraphAndValidateCycles
함수는 작업 그래프 생성을 담당하는 함수입니다. 이 함수는 projectGraph
, extraTargetDependencies
및 기타 매개변수를 입력으로 받고 유효성 검사 후 task graph
를 반환합니다.
task graph
가 비순환적인지(순환 종속성이 없는지) 확인하기 위해 유효성을 검사합니다. 순환이 감지되면 Nx는 구성 설정에 따라 오류를 발생시키거나 사용자에게 경고합니다.
const cycle = findCycle(taskGraph);
if (cycle) {
if (process.env.NX_IGNORE_CYCLES === "true" || nxArgs.nxIgnoreCycles) {
output.warn({
title: `The task graph has a circular dependency`,
bodyLines: [`${cycle.join(" --> ")}`],
});
makeAcyclic(taskGraph);
} else {
output.error({
title: `Could not execute command because the task graph has a circular dependency`,
bodyLines: [`${cycle.join(" --> ")}`],
});
process.exit(1);
}
}
🔳 Nx는 실제로 아키텍처에서 DAG(유향 비순환 그래프)와 작업 그래프를 모두 활용하며, 각각 다른 용도로 사용됩니다.
✔️ 프로젝트 그래프(DAG)
app
프로젝트가 ui-lib
라이브러리에 종속된 경우, 프로젝트 그래프에서 app
노드에서 ui-lib
노드로 향하는 에지가 있습니다.✔️ 작업 그래프(DAG)
app
프로젝트를 빌드할 때 build ui-lib
, lint app
, test app
과 같은 작업이 포함될 수 있습니다. 작업 그래프는 이러한 작업이 실행되어야 하는 순서를 정의하여 build app
전에 build ui-lib
가 완료되도록 합니다.
https://nx.dev/concepts/mental-model
https://nx.dev/concepts/mental-model
💡 작업은 단독으로 실행되지 않습니다. 작업은 작업 간의 관계와 종속성을 나타내는 더 큰 작업 그래프의 일부입니다. 이 그래프는 병렬 실행을 최적화하면서 종속성을 존중하여 작업이 올바른 순서로 실행되도록 합니다.
💡 Nx는 작업 그래프에서 작업을 실제로 실행하는 다양한 작업 러너(예: defaultTasksRunner
, nxCloudTasksRunnerShell
)를 제공합니다. 앞서 살펴본 invokeTasksRunner
함수는 구성 및 컨텍스트에 따라 적절한 태스크 러너를 선택하는 역할을 합니다.
🔳 최단 경로를 찾기 위해 NX는 Dijkstra 알고리즘을 사용합니다.
// https://github.com/nrwl/nx/blob/master/graph/ui-graph/src/lib/graph.ts#L164
...
case 'notifyGraphTracing':
if (event.start && event.end) {
if (event.algorithm === 'shortest') {
elementsToSendToRender = this.projectTraversalGraph.traceProjects(
event.start,
event.end
);
} else {
elementsToSendToRender =
this.projectTraversalGraph.traceAllProjects(
event.start,
event.end
);
}
}
break;
}
...
// https://github.com/nrwl/nx/blob/master/graph/ui-graph/src/lib/util-cytoscape/project-traversal-graph.ts#L146
traceProjects(start: string, end: string) {
const dijkstra = this.cy
.elements()
.dijkstra({ root: `[id = "${start}"]`, directed: true });
const path = dijkstra.pathTo(this.cy.$(`[id = "${end}"]`));
return path.union(path.ancestors());
}
Nx의 의존성 그래프와 몇 가지 기본 어빌리티에 대해 배웠다면, 이제 또 다른 중요한 어빌리티를 알아볼 차례로 데몬을 알아봅시다. 👻
🔵 로컬 머신에서 실행할 때는 기본적으로 Nx 데몬이 활성화됩니다. 활성화를 끄고 싶다면 아래와 같은 방법으로 비활성화 할 수 있습니다.
- `nx.json`의 러너 옵션에서 `useDaemonProcess: false`를 설정하거나
- 환경 변수 `NX_DAEMON`을 `false`로 설정합니다.
🔵 데몬은 여러 가지 방식으로 Nx 명령의 성능과 응답성을 향상시키기 위해 설계된 장기 실행 백그라운드 프로세스입니다.
✔️ 프로젝트 그래프 캐시: 프로젝트 그래프의 캐시 버전을 메모리에 유지하여 모든 명령에 대해 다시 빌드할 필요가 없도록 합니다.
✔️ 파일 감시: 작업 공간의 파일 변경 사항을 능동적으로 모니터링하고 필요할 때 프로젝트 그래프를 점진적으로 업데이트합니다.
Nx 데몬은 작업 공간의 파일을 감시하고 프로젝트 그래프를 즉시 업데이트하기 때문에 재계산을 최소화하기 위해 지능적으로 스로틀링하므로 프로젝트 그래프를 더 효율적으로 재계산할 수 있습니다(재계산을 최소화하도록 지능적으로 스로틀링합니다). 또한 모든 것을 메모리에 보관하므로 응답 속도가 훨씬 빨라지는 경향이 있습니다. - https://github.com/nrwl/nx/blob/master/docs/shared/concepts/daemon.md?plain=1
✔️ 병렬 작업: 여러 작업을 동시에 실행할 수 있으며, 여러 코어 또는 머신(Nx Cloud 사용 시)을 활용하여 빌드 속도를 높일 수 있습니다.
✔️ 작업 캐시: 로컬 캐시와 마찬가지로 데몬은 작업 결과에 대한 캐시를 유지하여 변경되지 않은 작업의 재실행을 방지함으로써 성능을 더욱 향상시킵니다.
효율성을 극대화하기 위해 Nx 데몬에는 필요하지 않을 때 자동으로 종료(모든 파일 감시자 제거 포함)하는 몇 가지 메커니즘이 내장되어 있습니다. 여기에는 3시간 동안 활동이 없는 경우(해당 시간 동안 작업 공간의 Nx 데몬이 어떤 요청도 받지 않았거나 파일 변경을 감지하지 못한 경우)와 Nx 설치가 변경되는 경우가 포함됩니다. - https://github.com/nrwl/nx/blob/master/docs/shared/concepts/daemon.md?plain=1
🔴 Nx 작업 공간당 하나의 고유한 Nx 데몬이 있습니다.
Nx 데몬은 로컬 컴퓨터의 백그라운드에서 실행되는 프로세스입니다. Nx 작업 공간당 하나의 고유한 Nx 데몬이 있다는 것은 컴퓨터에 여러 개의 Nx 작업 공간이 동시에 활성화되어 있는 경우 해당 Nx 데몬 인스턴스가 서로 독립적으로 작동하며 다른 버전의 Nx에 있을 수 있음을 의미합니다. - https://github.com/nrwl/nx/blob/master/docs/shared/concepts/daemon.md?plain=1
이 수준에서 로컬 운영에 대한 모든 것을 해부했다고 생각하는데, 이제 Nx Cloud로 이동하는 것에 대해 어떻게 생각하시나요? 시작하겠습니다! ☁️
Nx Cloud는 분산 캐싱 및 작업 실행 기능을 활용하여 모노레포 개발을 강화합니다. 레고 블록처럼 빌드 아티팩트를 공유 및 재사용하고 빌더 팀처럼 작업을 병렬화하여 빌드 시간을 크게 단축하고 리소스를 절약하며 팀 협업을 강화하여 가장 복잡한 프로젝트도 더 쉽게 관리할 수 있습니다.
🔴 NX Cloud는 유료 서비스이며 다양한 요금제가 있습니다.
Nx Replay는 분산 캐싱을 가능하게 하는 Nx Cloud의 핵심 기능입니다. 작업 실행 결과를 원격 캐시에 저장하여 모든 팀원과 CI 머신이 이를 공유할 수 있도록 합니다.
✔️ 수정된 PR을 위한 더 빠른 CI: PR에 대한 후속 커밋은 이전 CI 실행에서 캐시 된 결과를 재사용할 수 있으므로 불필요한 작업 재실행이 줄어듭니다.
https://nx.dev/ci/features/remote-cache
✔️ 개발자 기기에서 CI 결과 재사용: 개발자는 CI 작업의 결과를 즉시 활용하여 로컬 빌드 및 테스트 속도를 높일 수 있습니다.
https://nx.dev/concepts/how-caching-works
Nx 에이전트는 여러 머신에 작업을 분산하여 CI 실행을 최적화할 수 있는 또 다른 중요한 구성 요소입니다.
✔️ 작업 분배: 메인 CI 머신은 Nx Cloud에서 제공하는 에이전트 머신으로 작업을 전송합니다. Nx Cloud는 어떤 작업을 동시에 실행할 수 있는지 지능적으로 판단하여 에이전트에 분배하여 유휴 시간을 최소화합니다.
✔️ 종속성 관리: Nx Cloud는 종속성에 따라 작업이 올바른 순서로 실행되도록 합니다.
✔️ 결과 수집: 에이전트 머신의 결과가 메인 머신으로 다시 수집되어 모든 작업이 메인 머신에서 실행된 것처럼 보이게 합니다.
✔️ 동적 스케일링: 에이전트 머신의 수와 크기는 PR의 규모 또는 특정 프로젝트 요구 사항에 따라 동적으로 조정할 수 있습니다.
// .nx/workflows/dynamic-changesets.yaml
distribute-on:
small-changeset: 3 linux-medium-js
medium-changeset: 6 linux-medium-js
large-changeset: 10 linux-medium-js
// .github/workflows/main.yaml
...
jobs:
- job: main
displayName: Main Job
...
steps:
- checkout
- run: npx nx-cloud start-ci-run --distribute-on=".nx/workflows/dynamic-changesets.yaml" --stop-agents-after="e2e-ci"
- ...
PR의 크기를 결정하기 위해 Nx Cloud는 영향을 받는 프로젝트 수와 워크스페이스의 총 프로젝트 수 간의 관계를 계산합니다. 그런 다음 소형, 중형 또는 대형의 세 가지 범주 중 하나에 할당합니다.
이제 리포지토리의 작은 비율에 영향을 미치는 PR은 3개의 에이전트에서 실행되고, 중간 규모의 PR은 6개의 에이전트를 사용하며, 대규모 PR은 10개의 에이전트를 사용하게 됩니다. 이 기능은 대규모 PR에 필요한 고성능을 유지하면서 소규모 PR의 비용을 절감하는 데 도움이 됩니다.
https://nx.dev/ci/features/distribute-task-execution
https://nx.dev/concepts/mental-model#distributed-task-execution
✔️ 더 빠른 CI 파이프라인: 특히 대규모 프로젝트의 경우 CI 실행 시간을 크게 단축합니다.
✔️ 향상된 개발자 경험: 개발자 컴퓨터에서 더 빠르게 빌드하고 테스트할 수 있습니다.
✔️ 간소화된 설정: 작업 배포에 필요한 구성이 최소화됩니다.
✔️ 확장성: 프로젝트의 필요에 따라 리소스를 동적으로 확장할 수 있습니다.
💡 Nx Cloud는 프로젝트 종속성, 작업 실행 시간(예상 또는 과거), 사용할 수 있는 에이전트 리소스를 고려하여 작업 배포에 정교한 알고리즘을 사용합니다. 이는 총 실행 시간을 최소화하고 에이전트 간에 부하를 분산하는 것을 목표로 합니다. 구체적인 알고리즘은 다양할 수 있지만 휴리스틱과 최적화 기법을 사용하는 경우가 많습니다.
이제 우리가 배운 지식을 테스트하고 연습을 시작할 때입니다! 🚧
🔳 설치
✔️ Nx CLI는 npm install -g nx
를 사용하여 전역적으로 설치할 수 있습니다. 이렇게 하면 nx
명령을 바로 사용할 수 있습니다.
✔️ 또는 npx nx
를 사용하여 전역 설치 없이 Nx 명령을 실행할 수 있습니다.
🔳 워크스페이스 만들기
✔️ 특정 프리셋(예: 리액트, 앵귤러, 노드)이 있는 새 Nx 워크스페이스는 npx create-nx-workspace@latest
를 사용하여 생성할 수 있습니다.
✔️ npx nx init
을 실행하여 기존 프로젝트에 Nx 기능을 추가할 수 있습니다.
🔳 프로젝트 및 라이브러리 생성: 사전 설정된 구성의 프로젝트 및 라이브러리는 nx g @nx/react:app my-new-app
와 같은 생성기를 사용하여 생성할 수 있습니다.
🔳 작업 실행
✔️ Nx 작업은 기존 package.json
스크립트에서 생성하거나, 툴링 구성 파일에서 유추하거나, project.json
파일에 정의할 수 있습니다. Nx는 이 세 가지 소스를 모두 결합하여 특정 프로젝트의 작업을 식별합니다.
// https://nx.dev/reference/project-configuration
...
"targets": {
"test": {
"inputs": ["default", "^production"],
"outputs": [],
"dependsOn": ["build"],
"executor": "@nx/jest:jest",
"options": {}
},
...
✔️ 작업을 실행하기 위해 Nx는 다음 구문을 사용합니다.
https://nx.dev/features/run-tasks
✔️ run-many
명령을 사용하여 여러 프로젝트에 대한 작업을 실행할 수도 있습니다.
npx nx run-many -t build lint test
✔️ nx affected 명령은 코드 변경의 영향을 받는 프로젝트에서만 작업을 지능적으로 실행하는 데 사용할 수 있습니다.
npx nx affected -t test
🔳 프로젝트 그래프 활용하기
✔️ nx graph
를 사용하여 워크스페이스의 종속성 그래프를 시각화할 수 있습니다.
https://nx.dev/features/explore-graph
✔️ 사용할 수 있는 플러그인 및 해당 기능은 nx list
에서 확인할 수 있습니다.
🔳 인기 Nx 플러그인: Nx는 기능을 확장하기 위해 다양한 공식 및 커뮤니티 플러그인을 제공합니다.
✔️ 공식 Nx 플러그인의 전체 목록은 https://nx.dev/plugin-registry 에서 확인할 수 있습니다.
✔️ NX를 사용하기 위한 다양한 레시피를 여기에서 찾을 수 있습니다: https://github.com/nrwl/nx-recipes/tree/main.
배포 버전 구성을 진행해 보겠습니다. ☁️
연결되면 Nx는 자동으로 원격 캐싱을 사용하기 시작합니다. nx build my-app
와 같은 명령이 실행되면 Nx가 먼저 클라우드 캐시를 확인합니다. 결과가 발견되면 로컬 빌드를 건너뛰고 다운로드하여 비교한 후 사용할 수 있습니다.
🔳 CI 파이프라인에서 분산 작업 실행을 활성화합니다.
✔️ Nx 명령을 실행하기 전에 nx-cloud start-ci-run
명령을 CI 구성에 추가해야 합니다.
✔️ 사용할 에이전트의 수와 유형(예: --distribute-on=“4 linux-medium”
)을 지정해야 합니다.
✔️ 그런 다음 일반 Nx 명령(예: nx affected --target=build
)을 진행할 수 있습니다. 이러한 작업은 Nx Cloud에 의해 지정된 에이전트에 자동으로 배포됩니다.
# https://nx.dev/ci/recipes/set-up/monorepo-ci-gitlab
# ... (기타 CI 단계)
- name: Start CI run
run: npx nx-cloud start-ci-run --distribute-on="4 linux-medium"
- name: Run affected tests
run: nx affected --target=test --parallel
# ... (나머지 CI 단계)
이제 우리가 보고 공부한 모든 것에 대한 의견을 표현할 시간입니다.
🔵 Nx의 강점: 대규모 개발 지원
✔️ 효율성 향상: Nx는 작업을 간소화하고 빌드 프로세스를 최적화하며 병렬 실행을 지원하여 개발자의 생산성을 크게 향상시킵니다.
✔️ 견고한 아키텍처: DAG와 토폴로지 정렬을 사용하여 올바른 작업 순서를 보장하고 순환 종속성을 방지합니다.
✔️ 풍부한 생태계: 다양한 플러그인과 생성기를 통해 다양한 도구 및 프레임워크와의 개발과 통합을 간소화합니다.
✔️ 우수한 문서: 포괄적인 설명서를 통해 초보자도 Nx를 쉽게 배우고 사용할 수 있습니다.
✔️ Nx 클라우드의 장점: 분산 캐싱 및 작업 실행(Nx Cloud 사용)은 특히 대규모 프로젝트의 빌드 시간과 리소스 활용도를 크게 개선합니다.
✔️ 기존 도구와 통합: Nx는 기존 빌드 툴 및 프레임워크와 통합할 수 있어 원활하게 전환할 수 있습니다.
🔴 Nx의 도전 과제: 도입 시 고려 사항
✔️ 학습 곡선: Nx의 개념과 구성 옵션을 익히는 것은 도구를 처음 사용하는 사람들에게는 어려울 수 있습니다.
✔️ 블랙박스 효과: 코드 생성 기능은 경험이 적은 개발자가 프로젝트의 내부 작동을 이해하기 어렵게 만드는 양날의 검이 될 수 있습니다.
✔️ 제한된 인트로스펙션: 그래프와 영향을 받는 메커니즘을 제외하면 Nx는 빌드 프로세스에 대한 심층적인 가시성을 제공하지 않아 잠재적으로 디버깅을 방해할 수 있습니다.
✔️ 의견 수렴 구조: 프로젝트 구조 및 구성에 대한 Nx의 규칙이 기존 팀 관행이나 선호도와 충돌할 수 있습니다.
✔️ 소규모 프로젝트에는 과잉: 더 간단한 도구로 충분할 수 있는 소규모 프로젝트나 팀에게는 Nx의 모든 기능이 과도할 수 있습니다.
✔️ 두 가지 유형의 그래프 사용: 프로젝트 그래프(프로젝트 종속성을 나타내는 DAG)와 작업 그래프(역시 작업 종속성을 나타내는 DAG)를 사용합니다.
Nx의 효과에 대한 보다 폭넓은 관점을 얻기 위해 실제 프로젝트에서 어떻게 사용되고 있는지 살펴보고 NX 사용자이기도 한 커뮤니티의 인사이트를 수집해 보겠습니다.
Nx에 대한 균형 잡힌 관점을 얻기 위해 활발한 온라인 커뮤니티와 Nx GitHub 이슈 트래커를 모두 살펴봤습니다. 그 결과를 소개합니다.
1️⃣ 커뮤니티 피드백(Reddit)
🔳 Reddit 스레드 링크
🔸 “AskJS: 자바스크립트 모노레포용 Nx에 대한 경험”
🔸 “NX vs Turborepo? 어느 쪽에 베팅할지 고민 중입니다.”
🔳 피드백 요약
➕ Nx는 지능형 작업 오케스트레이션, 종속성 관리 및 코드 생성을 통해 개발을 간소화하고 생산성을 높이는 대규모 엔터프라이즈급 모노레포에서 빛을 발합니다. 특히 Angular 개발자는 Nx의 원활한 통합 및 자동화 기능을 높이 평가합니다.
➖ 하지만 사용자들은 잠재적인 단점도 지적합니다. 특히 유연성이나 더 간단한 학습 환경을 원하는 사용자에게는 Nx의 고정된 구조와 높은 학습 곡선이 장애물이 될 수 있습니다. 일부 사용자는 소규모 프로젝트에 Nx가 과하다고 생각하거나 Git 관리 및 상업적 지원에 대한 우려를 표명하기도 합니다.
2️⃣ 기술적 문제(Nx의 GitHub)
Nx의 GitHub 이슈 트래커를 분석해 보면 반복되는 문제가 드러납니다.
🔻 이슈 #26798: (확장성 제한) Nx는 매우 큰 리포지토리(250개 이상의 라이브러리)에서 프로젝트 그래프를 다시 빌드하는 데 어려움을 겪을 수 있습니다.
🔻 이슈 #26778 및 이슈 #26771: (호환성 문제) 일부 사용자가 e2e 테스트에서 사용자 지정 라이브러리를 사용하거나 Nx CLI로 Next.js 앱을 빌드하는 데 어려움을 겪고 있다고 보고합니다.
🔻 이슈 #26783: (파일 처리 및 구성) 프로젝트 이름 변경/이동 및 복잡한 구성 관리와 관련된 문제도 제기되었습니다.
이러한 인사이트는 프로젝트의 특정 요구 사항과 제약 조건을 고려하는 것이 중요하다는 점을 강조하면서 Nx를 채택하고 사용하는 데 있어 잠재적인 마찰 지점을 강조합니다.
🔵 강점
✔️ 대규모의 복잡한 모노레포에 탁월합니다.
✔️ 엔터프라이즈 환경에 이상적입니다.
✔️ 지능형 작업 오케스트레이션 및 빌드 최적화 기능을 제공합니다.
✔️ 프로젝트 간 코드 공유를 촉진합니다.
✔️ 확장성과 생산성을 위한 풍부한 기능을 제공합니다.
🔴 잠재적 단점
✔️ 고정된 구조로 인해 유연성이 제한될 수 있습니다.
✔️ 신규 사용자를 위한 학습 곡선이 가파릅니다.
✔️ 기능이 풍부하지만 소규모 프로젝트에는 과할 수 있습니다.
⚪ 다음과 같은 경우 대안을 고려하세요.
✔️ 프로젝트가 작거나 단순합니다.
✔️ 유연성이 최우선 순위입니다.
✔️ 비용 효율성이 중요합니다.
Nx 탐험은 여기서 끝났지만, 모노레포의 여정은 계속됩니다! Turborepo와 pnpm 작업 공간의 강점, 약점, 이상적인 사용 사례를 Nx와 비교하는 심층 분석을 계속 지켜봐 주세요. 곧 다시 뵙겠습니다! 😍
Nx에 대해 자세히 알아보면 강력하면서도 미묘한 차이가 있는 툴이라는 것을 알 수 있습니다. 구조화된 접근 방식, 빌드 최적화, 클라우드 기능이 개발을 크게 간소화하는 대규모의 복잡한 모노레포에서 그 강점이 빛을 발합니다.
하지만 만능 솔루션은 아닙니다. 프로젝트 규모가 작거나, 워크플로가 독특하거나, 유연성을 선호하는 팀에게는 Nx의 관습과 학습 곡선이 장애물이 될 수 있습니다.
프로젝트 그래프와 작업 오케스트레이션부터 데몬과 Nx Cloud의 역할에 이르기까지 Nx의 내부 작동을 이해하는 것은 정보에 입각한 결정을 내리는 데 매우 중요합니다. 다른 개발자들의 실제 경험을 통해 긍정적이든 부정적이든 Nx의 실질적인 장점과 잠재적인 단점을 파악할 수 있습니다.
최고의 모노레포 도구는 가장 많은 기능을 갖춘 도구가 아니라 프로젝트의 복잡성, 팀의 워크플로 및 전반적인 개발 목표에 가장 잘 부합하는 도구라는 점을 기억하세요. 여기서 얻은 지식으로 무장한 여러분은 모노레포 숙달의 길로 나아가고 있습니다!
앞으로 예정된 Turborepo 및 pnpm 워크스페이스에 대한 심층 분석도 기대해 주세요.
새로운 글과 새로운 모험으로 다시 만나 뵙겠습니다! ❤️
제 글을 읽어주셔서 감사합니다.
저와 연락하고 싶으신가요?
GitHub에서 저를 찾을 수 있습니다: https://github.com/helabenkhalfallah