-
Notifications
You must be signed in to change notification settings - Fork 1
2. CS 리팩토링 결과
- 프론트엔드 성능 이슈, 백엔드, CRDT 구조적 이슈로 인해 성능 최적화가 필요한 상황이었습니다.
- 프론트엔드 성능 이슈
- innerHTML 사용으로 XSS 취약점 존재
- 페이지 크기 변경시 모든 에디터와 블록이 불필요하게 리렌더링
- 화면 밖 블록까지 모두 DOM에 렌더링되어 메모리 낭비
- 백엔드, CRDT 구조적 이슈
- 전체 워크스페이스 데이터를 인메모리에 캐싱하여 서버 메모리 과도하게 사용
- CRDT Tombstone 미적용으로 인해 동시성 충돌 문제
- innerHTML 사용으로 인한 보안 및 성능 이슈
- XSS 취약점: 사용자 입력이 HTML로 직접 파싱되어 스크립트 실행 위험이 존재했습니다.
- 불필요한 리렌더링: 텍스트 변경시 텍스트 전체 DOM을 재파싱하고 있습니다.
- 브라우저 렌더링 파이프라인
- HTML 파싱 → DOM 트리 구성
- innerHtml을 사용하게 되면 전체 DOM을 다시 파싱해 기존 DOM을 완전히 덮어쓰기 때문에 텍스트의 양이 많아질 경우 성능상 문제가 발생했습니다.
- documentFragment를 사용할 경우 DOM을 가상 메모리 내에서 수정하고, 기존 DOM과 비교 후 필요한 부분만 갱신합니다. 이를 통해 전체 DOM이 다시 파싱되지 않으므로 불필요한 연산을 제거해 DOM 업데이트 횟수를 감소시켰습니다.
- Layout 계산 → 리플로우 발생
- DOM 트리의 모든 노드를 새로 계산하기 때문에 텍스트를 입력할때마다 리플로우가 발생합니다.
- documentFragment를 사용해 기존 DOM과 새로운 DOM을 비교해 차이가 있을때만 업데이트 하도록 수정했습니다.
- Paint → 리페인트 발생
- 텍스트의 스타일이 적용되어 있을 때, 텍스트를 입력할때마다 리페인트가 발생합니다.
- documentFragment를 사용해 한 번의 페인트 작업만 수행하게 되어 불필요한 중간 렌더링 횟수를 줄였습니다.
- HTML 파싱 → DOM 트리 구성
- 기존 innerHtml문자열을 만드는 부분을 document.createDocumentFragment로 변경했습니다.
- 문자열에 span태그, 텍스트 노드를 추가하는 방식에서 fragment.appendChild를 사용하는 방식으로 변경했습니다.
- 기존 노드와 새로운 노드를 비교해 변경이 있을때만 업데이트하도록 수정했습니다.
// 직접 html '문자열'을 생성
let html = "";
let currentState: TextStyleState = {
styles: new Set<string>(),
color: "black",
backgroundColor: "transparent",
};
chars.forEach((char, index) => {
const targetState = positionStyles[index];
// 스타일, 색상, 배경색 변경 확인
const styleChanged =
!setsEqual(currentState.styles, targetState.styles) ||
currentState.color !== targetState.color ||
currentState.backgroundColor !== targetState.backgroundColor;
// 변경되었으면 현재 span 태그 닫기
if (styleChanged && spanOpen) {
html += "</span>";
spanOpen = false;
}
// 새로운 스타일 조합으로 span 태그 열기
if (styleChanged) {
const className = getClassNames(targetState);
html += `<span class="${className}" style="white-space: pre;">`;
spanOpen = true;
}
// 텍스트 추가
html += sanitizeText(char.value);
// 다음 문자로 넘어가기 전에 현재 상태 업데이트
currentState = targetState;
// 마지막 문자이고 span이 열려있으면 닫기
if (index === chars.length - 1 && spanOpen) {
html += "</span>";
spanOpen = false;
}
});
// html 문자열 대신 documentFragment를 사용
// dom api를 통해 블록을 변경
const fragment = document.createDocumentFragment();
let currentSpan: HTMLSpanElement | null = null;
let currentState: TextStyleState = {
styles: new Set<string>(),
color: "black",
backgroundColor: "transparent",
};
chars.forEach((char, index) => {
const targetState = positionStyles[index];
// 스타일, 색상 변경 확인
const hasStyles =
targetState.styles.size > 0 ||
targetState.color !== "black" ||
targetState.backgroundColor !== "transparent";
if (hasStyles) {
const styleChanged =
!setsEqual(currentState.styles, targetState.styles) ||
currentState.color !== targetState.color ||
currentState.backgroundColor !== targetState.backgroundColor;
if (styleChanged || !currentSpan) {
currentSpan = document.createElement("span");
currentSpan.className = getClassNames(targetState);
currentSpan.style.whiteSpace = "pre";
fragment.appendChild(currentSpan);
currentState = targetState;
}
const textNode = document.createTextNode(sanitizeText(char.value));
currentSpan.appendChild(textNode);
} else {
currentSpan = null;
const textNode = document.createTextNode(sanitizeText(char.value));
fragment.appendChild(textNode);
}
});
- XSS 취약점 제거
- DOM 파싱 및 빌드 비용 절감
- 페이지의 크기를 변경할 때, 페이지 내부 에디터 및 블록 모두가 리렌더링 되는 문제가 발생했습니다. 이로인해 불필요한 리렌더링 및 리플로우가 발생했습니다.
- React memo, useMemo를 학습해 페이지 리사이징 중에는 에디터 및 블록이 리렌더링 되지 않도록 변경했습니다.
- 리플로우/리페인트 최적화 방법을 학습해 오버레이 스크린이 보일 때는 transform: scale(0) 속성을 통해 페이지 컴포넌트가 보이지 않도록 변경했습니다.
- 브라우저 성능 측정 도구를 분석해 Chrome react devtool을 사용했습니다. profiler를 통해 렌더링 파이프라인을 분석해 렌더링에 걸리는 시간을 측정했습니다.
- 오버레이 스크린 도입을 통해 페이지 크기 변경시 페이지가 한번만 렌더링되도록 수정했습니다.
- 에디터, 블록을 React.memo를 통해 메모이제이션을 적용했습니다. 이를 통해 텍스트 정보가 변하지 않을 경우 리렌더링을 방지합니다.
-
Render:
11.3ms
→4.9ms
(약 56.6% 향상) -
Layout effects:
1.3ms
→0.6ms
(약 53.8% 향상) -
Passive effects:
1ms
→0.6ms
(약 40.0% 향상) -
총합:
13.6ms
→6.1ms
(약 55.1% 향상)
- 스켈레톤UI는 사용자경험(UX)에 있어서는 좋지않다고 판단했습니다. 추후 디바운싱 및 스로틀링을 통해 에디터와 블록의 width를 변경하도록 수정해 리플로우 횟수를 줄이면서 리사이징 중에도 문서 내용을 파악할 수 있도록 수정할 예정입니다.
페이지 크기를 조정할 때, 안보이는 블럭까지 모두 재렌더링이 됨을 확인했습니다. 이는 안보이는 블럭이 DOM 에는 존재하기 때문에, 페이지 크기(width)를 바꾸면 자식 요소인 블럭까지 재렌더링이 되는 문제였습니다.
안보이는 블럭은 DOM에서 제거하는 리스트 가상화에 대해 학습했습니다.
화면에 보여야할 첫번째 item을 찾기 위한 방법으로 이분탐색 알고리즘 학습했습니다.
스크롤 위치를 기반으로 화면에 보여야할 첫번째 item, 마지막 item을 구하여 정적 높이 기반의 리스트 가상화를 구현하였습니다. [참고 링크](https://www.notion.so/1559ff1b21c380a491a2c12599f57a60?pvs=21)
그러나 우리 프로젝트는 각 블럭의 높이가 동적으로 변할 수 있습니다. 이에 동적 높이를 지원하는 리스트 가상화 라이브러리를 비교하고, TanStack Virtual 라이브러리를 도입함. 내부적으로 이분탐색 알고리즘을 사용하는 것을 확인했습니다
화면에 보이는 블럭들만 DOM에 존재하도록 수정하였습니다. 사진을 보면 DOM에 실시간으로 화면에 보이는 블럭들이 추가됨을 볼 수 있습니다.
Nocta는 문서 편집 시 CRDT 알고리즘을 사용합니다. 클라이언트가 문서를 편집하면, 해당 편집 내용은 CRDT 연산 단위로 서버와 다른 클라이언트에게 전송됩니다.
이 과정에서 사용자가 문자를 입력할 때마다 연산이 매우 빈번하게 발생합니다. 만약 모든 CRDT 연산을 DB에 즉시 반영한다면, 잦은 읽기/쓰기 때문에 DB 부하가 크게 증가합니다.
이를 완화하기 위해 인메모리 캐시에 편집 내용을 모아뒀다가 일정 주기마다 한꺼번에 DB에 저장하는 방식을 도입했습니다.
그 결과 문서가 수정될 때마다 이를 DB에 바로 반영하지 않고 캐시에 모았다가 한번에 반영하기 때문에 DB 읽기/쓰기 비용을 줄였고, 클라이언트가 최신 상태를 조회할 때 캐시를 확인하기만 하면 되므로 빠른 응답이 가능합니다.
그러나 다음과 같은 문제가 발생했습니다.
- 메모리 사용량 증가: 전체 워크스페이스 데이터를 서버 메모리에 통째로 보관하다 보니, 서버 메모리 사용량이 불필요하게 커졌습니다.
- 장애 시 데이터 손실: 서버 장애가 발생하면 캐시에만 존재하던(아직 DB에 동기화되지 않은) 변경 내역이 사라집니다.
- 과거 기록 추적 어려움: 연산을 즉시 적용하다 보니, 어느 시점에서 어떤 연산이 있었는지 추적하기가 어렵습니다.
이 문제를 해결하기 위해, 워크스페이스 전체 데이터를 캐시하지 않고, 클라이언트로부터 발생하는 CRDT 연산 단위만 별도로 저장(캐싱)하는 구조로 변경했습니다.
해당 방식을 구현하기 위해 저희는 이벤트 소싱 패턴(Event Sourcing Pattern)을 도입했습니다.
이벤트 소싱 패턴은 시스템에서 발생하는 모든 변경 연산을 ‘이벤트’로 기록한 뒤, 나중에 이 이벤트들을 순서대로 재생(Replay)하여 최종 상태를 구하는 방식입니다. 구체적으로는 다음 단계를 거칩니다.
- 이벤트 스토어에 연산 누적: 서버는 클라이언트가 보낸 CRDT 연산(이벤트)을 이벤트 스토어에 쌓습니다.
- 스냅샷 생성: 일정 주기마다 이벤트 스토어에 기록된 연산을 DB 스냅샷에 재생(Replay)하여 최신 상태를 DB에 저장합니다.
- 최신 상태 제공: 클라이언트가 최신 상태를 요청하면, 서버는 DB 스냅샷에 저장된 상태를 가져온 뒤 이후에 발생한 이벤트를 추가로 재생해 최신 정보를 만듭니다.
Nocta의 CRDT는 연산 기반이므로, 클라이언트가 문서를 편집하면 그 CRDT 연산을 이벤트로 보관합니다. 서버는 받은 연산을 이벤트 스토어에 저장하고, 동시에 다른 클라이언트에게 브로드캐스트합니다. 그리고 일정 주기로 이벤트를 재생하여 DB에 새로운 스냅샷을 저장합니다.
기존에는 클라이언트로부터 연산을 받으면, 그 연산을 인메모리에 있는 워크스페이스 데이터에 즉시 적용하고, 그 결과를 다른 클라이언트에 브로드캐스트했습니다.
개선 후에는 해당 로직을 변경하여, 연산을 별도의 이벤트 스토어에 저장한 뒤(재생 없이 그대로) 브로드캐스트만 합니다. 실제 워크스페이스 상태 업데이트(스냅샷 생성)는 일정 주기마다 이벤트 스토어에 쌓인 연산을 재생하는 방식으로 바뀌었습니다.
async handleBlockUpdate(
@MessageBody() data: RemoteBlockUpdateOperation,
@ConnectedSocket() client: Socket,
batch: boolean = false,
): Promise<void> {
const clientInfo = this.clientMap.get(client.id);
try {
// Before
const { workspaceId } = client.data;
const currentPage = await this.workSpaceService.getPage(workspaceId, data.pageId);
if (!currentPage) {
throw new Error(`Page with id ${data.pageId} not found`);
}
currentPage.crdt.remoteUpdate(data.node, data.pageId);
currentPage.crdt.LinkedList.updateAllOrderedListIndices();
// After
this.workSpaceService.storeOperation(client.data.workspaceId, data);
...
this.emitOperation(client.id, data.pageId, "update/block", operation, batch);
} catch (error) {
...
}
}
일정 시간마다 이벤트 스토어에 쌓인 연산들을 재생(Replay)하여 새 스냅샷을 DB에 업데이트합니다. 재생 후에는 이벤트 스토어를 비워 초기 상태로 만들어줍니다.
async makeSnapshot(): Promise<void> {
const bulkOps = [];
const tasks = [];
this.operationStore.forEach((operations, workspaceId) => {
tasks.push(
(async () => {
// DB에서 찾기
const workspaceJSON = await this.workspaceModel.findOne({ id: workspaceId });
if (!workspaceJSON) {
throw new Error(`workspaceJson ${workspaceId} not found`);
}
const workspace = new CRDTWorkSpace();
workspace.deserialize({
id: workspaceJSON.id,
pageList: workspaceJSON.pageList,
authUser: workspaceJSON.authUser,
} as WorkSpaceSerializedProps);
await this.playAllOperations(workspace, operations);
const newWorkspace = await this.clearDeletedObject(workspace);
const serializedData = newWorkspace.serialize();
const workspaceData = {
id: workspaceId,
name: workspace.name,
pageList: serializedData.pageList,
authUser: serializedData.authUser,
updatedAt: new Date(),
};
bulkOps.push({
updateOne: {
filter: { id: workspaceId },
update: { $set: workspaceData },
upsert: true,
},
});
})(),
);
});
// 모든 작업이 끝날 때까지 대기
await Promise.all(tasks);
if (bulkOps.length > 0) {
await this.workspaceModel.bulkWrite(bulkOps, { ordered: false });
}
this.operationStore.clear();
this.logger.log(`Snapshot 저장 완료`);
}
클라이언트가 특정 페이지를 요청하면, DB 스냅샷과 이벤트 스토어에 적재된 연산을 조합해 최신 상태를 생성합니다.
async updatePage(workspaceId: string, pageId: string) {
const page = await this.getPage(workspaceId, pageId);
if (!page) {
throw new Error(`Page with id ${pageId} not found`);
}
const operations = this.operationStore.get(workspaceId) || [];
const pageOperations = operations.filter((op) => op.pageId === pageId);
for (const operation of pageOperations) {
await this.playOperationToPage(page, operation);
}
return page;
}
CRDT 연산을 이벤트로 저장하는 구조로 전환하면서, 다음과 같은 이점을 얻었습니다.
- 메모리 사용량 감소: 워크스페이스 전체 데이터를 메모리에 두지 않고, 연산만을 보관하기 때문에 메모리 부담이 크게 줄었습니다.
- 이력 추적 및 복구 용이: 이벤트가 순차적으로 기록되어 있으므로, 과거 변경 내역을 쉽게 재생해 볼 수 있습니다. 이는 향후 버전 관리나 장애 복구 로직을 추가하기에 유리합니다.
- Redis 적용 시 효율성 증가(확장성): 추후 Redis 등 외부 캐시를 도입하더라도 전체 데이터가 아닌 연산만 저장하므로, 캐시 용량을 효율적으로 활용할 수 있습니다.
CRDT 이론에서는 Tombstone 기법이 중요한 역할을 합니다. 그러나 기존 구현에서는 메모리 효율을 고려하여 Tombstone 처리를 생략(Hard Delete)했습니다. 이로 인해 다음 문제가 발생했습니다.
- CRDT 이론과 불일치: 하드 딜리트로 인해 삭제된 노드와 관련된 충돌 상태를 적절히 처리하기 어렵습니다.
-
참조 에러: 이미 삭제된 노드를 다른 사용자가 수정하려고 하면,
'node not found'
에러가 발생합니다.
Hard Delete 대신 Soft Delete 방식을 도입해 Tombstone 기법을 적용했습니다. 노드를 완전히 제거하지 않고 deleted
와 같은 플래그만 지정해두고, 실제로는 링크드리스트에서 제외시키는 식으로 수정했습니다.
아래와 같이 노드 구조체에 deleted
프로퍼티를 추가하고, 삭제 시에는 deleted
를 true
로 설정만 합니다. 그리고 별도 메서드에서 deleted
가 true
인 노드를 한꺼번에 제거합니다.
..Node.ts
export abstract class Node<T extends NodeId> {
id: T;
deleted: boolean; // 노드에 deleted 추가
value: string;
next: T | null;
prev: T | null;
style: string[];
------
..Linkedlist.ts
deleteNode(id: T["id"]): void {
const nodeToDelete = this.getNode(id);
if (!nodeToDelete) return;
nodeToDelete.deleted = true;
}
clearDeletedNode(): void {
let currentNodeId = this.head;
while (currentNodeId !== null) {
const currentNode = this.getNode(currentNodeId);
if (!currentNode) return;
if (currentNode.deleted) {
this.removeNode(currentNodeId);
}
currentNodeId = currentNode.next;
}
}
또한, 렌더링 및 연산 로직 전반에서 deleted === true
상태의 노드는 무시하도록 처리했습니다.
Tombstone(Soft Delete) 적용으로 다음과 같은 개선이 이루어졌습니다.
-
동시성 충돌 해결
- 예를 들어, 사용자 A가 노드를 삭제하는 동시에 사용자 B가 같은 노드를 수정하더라도, 더 이상
'node not found'
에러가 발생하지 않습니다. - Tombstone으로 인해 노드가 완전히 사라지지 않으므로, 수정과 삭제 연산의 순서에 상관없이 충돌을 적절히 처리할 수 있습니다.
- 예를 들어, 사용자 A가 노드를 삭제하는 동시에 사용자 B가 같은 노드를 수정하더라도, 더 이상
-
안정성 향상
- 하드 딜리트로 인한 참조 에러가 사라졌기 때문에 시스템이 더욱 안정적으로 동작합니다.
- 노드가 완전히 제거되기 전에도 일시적으로 데이터를 확인할 수 있어, 데이터 일관성 관리가 용이합니다.
-
J078_김현훈
-
테스트 코드가 얼마나 중요한지 느꼈습니다.
- 6주간 개발하면서 테스트코드를 신경쓰며 개발하면 정해진 시간 내에 구현을 못할것이라 판단하여 테스트 코드를 skip하고 개발을 진행했습니다.
- 하지만 실상 직접 E2E테스트를 하는데 소요되는 시간과 어느 메소드에서 문제가나는지 디버깅하는 시간이 굉장히 오래걸렸습니다.
- 리팩토링 과정에서 tombstone을 추가하며 linkedlist의 각 메소드별 테스트코드를 작성했는데, 이 테스트 코드 작성 후 E2E테스트를 진행하니 굉장히 안정적으로 테스트를 할 수 있었습니다.
- 물론 자세한 QA를 하지 못했지만, 개발하는 과정에서 코드만 보고 디버깅이 가능하여 생산성이 많이 올라갔습니다.
-
설계 구조를 고려한 React의 기능을 활용한 FE 최적화
- React의 최적화 방안으로 스켈레톤UI, Memo, useCallback, 리스트 가상화등의 다양한 방법이 있습니다. 우리 프로젝트에 맞는 최적화 방안을 적절히 선택하는것이 중요하다는 것을 알게 되었습니다.
- 무분별한 최적화가 아닌, 명확한 수치적 개선과 사용성 개선과같은 근거를 명확히 하여 적용하는것이 타당하다 라는걸 깨닳았습니다.
-
개선의 근거를 찾는 여정의 도착지는 결국 CS
- 개선의 근거를 단순히 수치적인 이유로 15%→50% 등으로 향상했다, 감소했다를 고민했던 적이 있습니다. 이 수치가 향상하거나, 최적화가 된 이유는 결국 CS적인 지식으로 들여다 봐야 이해가 가능한 내용이 많았습니다.
- Redis가 좋은 이유를 탐색해보니 운영체제 지식을 알게되었습니다. cache를 메모리에 올리기 때문인데, 왜 메모리에 cache가 있으면 빠른건지 그리고 아키텍처를 효율적으로 분산하는게 실제 유저 경험에 얼마나 영향(최대 부하시 페이지 로딩에 최대 13s)을 미치는지 알게 되었습니다.
- 또한 각 서버와 클라이언트가 할 수 있는 개선 여부들(api별로 서버 나누기, MSA, Redis, 이벤트소싱, 스냅샷, 연산병합(클라이언트)) 등을 검토해보며 다양한 개선점이 우리 프로젝트에 어떤식으로 적용될 수 있는지 검토해보며 이전보다 넓은 시야를 가지게 되었습니다.
-
테스트 코드가 얼마나 중요한지 느꼈습니다.
-
J098_민연규
- 리팩토링을 통해 성능 최적화를 단순한 코드 수정이 아닌, 브라우저의 동작 원리를 깊이 이해하고 접근해야 한다는 점을 깨닫게 되었습니다. 브라우저 렌더링 과정에서 발생하는 DOM 조작, 리플로우, 리페인트 과정, 불필요한 DOM 업데이트를 줄이기 위한 기존 방식 분석, 최적화 기법 및 innerHtml과 documentFragment의 차이 등 브라우저 동작 원리를 Deep dive할 수 있는 기회가 되었습니다.
- 텍스트를 입력할 때마다 에디터 전체 렌더링되는 문제를 해결하려면 에디터와 블록의 상태관리 구조를 모두 수정해야 했습니다. 초기 개발 단계에서 리액트의 상태 관리 구조를 보다 신중하게 설계하지 않으면 이후의 유지보수와 성능 최적화에 많은 비용이 발생할 수 있음을 깨달았습니다.
- 오버레이 스크린을 적용할 때는 성능상 개선에만 집중했습니다. 하지만 실제로 적용하고 난 후 UX적으로는 이전 동작방식이 더 좋을 것 같다는 피드백이 있었습니다. 실제로 리사이징 중에 문서를 볼수 없는 문제가 있어 추후 수정할 예정입니다. 이를 통해 성능을 개선하는 과정에서 UX와의 균형을 맞추는 것이 중요하다는 것과, 단순히 성능 지표를 향상시키는 것만이 아니라 사용자 경험을 고려한 접근이 필요함을 확인할 수 있었습니다.
- 리팩토링을 하면 할 수록 개선해야할 부분들이 계속 보이는 것 같습니다. 3주간의 리팩토링 과정이 끝나도 계속 개선해보고 싶다는 생각이 들었습니다.
-
J099_민정우
- 이전 프로젝트에서는 기능 구현에 집중하느라 성능적 측면을 충분히 고민하지 못했습니다. 이번 리팩토링 기간 동안 코드를 다시 살피면서, 현재 구조가 가진 문제점을 구체적으로 파악하고 해결책을 모색할 수 있어서 의미 있는 시간이 되었습니다.
- 우리가 직접 만든 CRDT 알고리즘을 기존에 통용되는 라이브러리와 비교해볼 수 있었던 점도 큰 수확이었습니다. 상용 알고리즘과 견주어 보며, 우리가 구현한 기능의 완성도를 점검하고, 이전에 해결하지 못했던 문제를 어떻게 풀 수 있는지 아이디어를 얻었습니다. 실제로 일부 기능을 적용해본 결과, CRDT 알고리즘 전반의 완성도를 한층 끌어올릴 수 있었습니다.
- 기능을 개선할 때 명확한 문제 정의와 객관적인 근거를 제시하려고 노력했지만, 결과적으로 수치를 동반한 분석을 충분히 진행하지 못했습니다. 리팩토링 초기에 테스트 코드를 작성하는 등 통계 자료를 준비하려 했으나 의미 있는 데이터를 확보하지 못했고, 이로 인해 이후 개선 작업이 다소 이론적 추측에 의존하게 되었습니다. 개선이 끝난 뒤에도 결과를 정량적으로 측정하지 못해, “개선되었을 것”이라는 추정에 머문 점이 아쉽습니다.
- 리팩토링 기간 동안 “큰 목표”를 설정하고 해당 문제를 집중적으로 해결하려 했지만, 막상 해결 과정에서 예상치 못한 문제들이 나타나 시간을 크게 소모했습니다. 결국 일부 개선 사항은 보류하고 다른 작업으로 넘어가야 했고, 그만큼 실질적인 개선에 쏟은 시간이 줄어들었습니다. 문제 해결에 필요한 공수를 초기에 제대로 파악하지 못한 것이 원인이었는데, 만약 작은 단위의 목표를 잡아 각 요소를 깊이 파고드는 방식으로 진행했다면 시간을 보다 효율적으로 운용할 수 있었을 것 같습니다.
-
J213_장서윤
- 구현에만 급급하여 성능적으로 놓친게 많다는 것을 알게 되었습니다.
- 또한 테스트코드도 구현이 끝난 뒤 생각하는게 아니라, 구현하면서 테스트코드를 짜야함을 알게 되었습니다.
- 리스트 가상화를 직접 구현하고 싶었지만, 쉽지 않다는 걸 깨달았습니다.
- 기회가 된다면 동적 높이의 리스트 가상화를 직접 구현하고, 원리를 다른 사람들에게 설명할 수 있을 정도로 학습해보고자 합니다.
- 🧑🤝🧑 그라운드 룰
- 🏃♂️ CS 리팩토링 계획
- 🤖 인공지능 리팩토링 계획
- ✅ 팀의 성장목표
- 🗒️ 프로젝트 계획
- 👨👧👧 1차 2025.01.06
- 👨👧👧 2차 2025.01.07
- 👨👧👧 3차 2025.01.08
- 👨👧👧 4차 2025.01.09
- 👨👧👧 5차 2025.01.13
- 👨👧👧 6차 2025.01.14
- 👨👧👧 7차 2025.01.15
- 👨👧👧 8차 2025.01.16
- 👨👧👧 9차 2025.01.20
- 👨👧👧 10차 2025.01.21
- 👨👧👧 11차 2025.01.22
- 👨👧👧 12차 2025.01.23
- 👨👧👧 13차 2025.02.03