
25.3.24 ~ 25.4.
개발리뷰회고를 모두 합하여 있었던 일에 대해 회고하며 어려웠던 점이나 재미있었던 점을 기록해본다.
outliner 토글 구현
소요 시간: ~ 9시간
토글 기능은 하위 목록을 숨기고, 펼치는 기능이니 현재 요소를 부모 요소로 지정하여 children 속성을 만들어 하위 요소를 배열 구조로 넣는다. 하위 요소는 level로 구분하여, 판단한다. 현재 요소보다 level이 크다면, 하위 요소인 것으로 정하고, 같거나 작다면 넘어간다. 이렇게 선별한 children을 배열 구조로 저장하여 map으로 출력한다. 이렇게 하면, 어느 요소나 children 항목을 가지고 있기 때문에 자식 요소가 없다면, 렌더링을 하지 않고, 있다면, 렌더링을 하게 만들면 된다고 생각했다.
그러나 위의 방법은 꽤 복잡했다. 우선, 데이터 구조가 플랫하다. 하위 요소를 위해 children을 만들게 되면, 현재 로직 특성상 기존 items에 새로운 item이 계속 추가된다. 이를 삭제하고 하위 요소로 보내는 것이 비효율적이다. 또, 토글에서 벗어나면, 하위 요소를 삭제하고 상위 요소로 보내야 하는데, 흐름상 구체적으로 보일 수는 있어도 jsx 구조도 변경해야 하고, 로직도 크게 손을 봐야 할 것 같았다.
따라서, 원래 구상했던 계획을 접고, 모든 item 요소에 isOpen을 추가한다. isOpen은 현재 화면에 보이는지, 안보이는지의 상태며, 토글과 동기화된다. 토글이 되는 주체는 상태를 toggleOpen/toggleClose로 판단하며, close가 될 시 isHide도 true가 될 것이다.
컴포넌트 렌더링에서 isHide이 true라면 hidden 처리하게 두어 토글 시 일부 목록은 보이지 않게 설정한다.
추가 구현은 별도로 지정되어 있지 않지만, https://workflowy.com/basics/ 에서 참고하면 새로운 아이디어가 생길지도 모른다는 내용이었다. 나는 새로운 걸 구상하는 능력이 떨어지기 때문에 사이트에서 참고하여 메모 기능을 간략하게 따라 만들어보았다.
토글을 어떤 식으로 만들어야 할 지 고민을 해봤다. 가장 처음 생각이 든 건, 하위 목록을 만드는 것이었다. 토글이라는 것이 하위 요소에도 존재하니, children이라는 목록을 만들어 계속 이어지도록 만들고, 하위목록이 없다면 [] 빈 배열로 두도록 구상했다. (어쩌면 리액트의 렌더링과 유사한)
데이터를 확인하니, 별도의 하위 목록은 없었고, 기존 코드에는 상태관리툴인 Zustand를 이용하고 있었다. 그렇다면, 데이터에 하위 목록을 새로 만들어주자 생각했다. 그리고 현재 렌더링하고 있는 Item을 기준으로 level을 활용하여 들여쓰기한 목록을 찾아내자. 그걸 하위 목록에 넣어주면 되겠다. 그렇게 넣는 것까지는 성공했으나, 하위 목록을 어떻게 렌더링할 지 고민이 됐다. list 컴포넌트에서 items 컴포넌트를 map으로 출력한다. list 컴포넌트에서 하위 목록을 출력하면, 별도의 list가 생성될테니 불가하고, items 컴포넌트에서 자체적으로 하위 목록을 생성해야 한다. 그렇지만 이 방법은 모든 Items에 영향이 간다. 계속 고민하다가 결국, 하위 목록을 생성하는 것 말고 다른 방법으로 우회하여 들어갔다.
지금 다시 생각해보면, 현재 item에 하위목록이 있는지 확인하고 있다면, 출력. 없다면, 출력하지 않는 것으로 토글을 다룰 수 있었을 것 같다.
텍스트를 드래그하여, 드래그한 텍스트 분량만큼만 스타일을 주는 것도 구현하고 싶었는데, 그러려면 기존의 input을 수정하여 div로 만들고, contentEditable 속성을 추가해 사용해야 한다. div로 수정한 뒤, textContent로 input에서 받던 text 값을 받아오는 것까지는 문제가 되지 않지만, 커서 위치(값을 수정하려고 하면, 커서 위치가 맨앞으로 이동함)를 수정해야 한다. 또한, 스타일을 줄 수 있는 툴 박스와 동작을 구현한다고 생각하면, 꽤 리소스가 많이 드는 작업이다. 수정 도중 오류를 만나면, 그것도 수정해줘야 해서 그만뒀다. 다시 시도해봐도 좋을 거 같다.
contentEditable를 사용하여 다시 시도해보았다. 검색과 AI에게 질문하여, 커서 위치를 수정하는 데까지는 성공했지만, 스페이스바(띄어쓰기 공백)가 새로운 문제로 떠올랐다. 또, contentEditable를 사용하는 이유는 텍스트에 스타일을 주는 것이 목표인데, 글씨 강조, 배경색, 텍스트 컬러 등 버튼도 여러개를 만들어야 하고, 상태를 어떻게 관리할 건지도 생각해야 하니, 다음에 하기로 결심했다…

이번 과제에서 주안점으로 둔 것은 지난번과 동일하게 액션, 계산, 렌더링 이다.
이미 구현된 gyul store에서 새로운 기능을 추가해주었다. toggle 기능을 담당하는데, 클릭했을 때, 하위 목록을 찾고, 하위 목록의 visible을 반대값으로 설정한다. (toggle 답게 온/오프 할 수 있게)
toggleItems: (item: Item, isHide: boolean) => {
const index = getIndexById(item.id);
set((state) => {
const length = state.ids.length;
const ids = state.ids;
const updateItems = { ...state.items };
for (let i = index; i < length; i++) {
const nextId = ids[i + 1];
const nextItem = updateItems[nextId];
if (!nextItem || nextItem.level === undefined) continue;
if (item.level < 0) return state;
if (item.level < nextItem.level) {
nextItem.isHide = !isHide;
} else if (item.level >= nextItem.level) {
break;
}
}
return { ...state, items: updateItems};
});
},
id를 받으면, item 요소의 메모 input을 visible/invisible 해준다. 마찬가지로 toggle 기능이기 때문에 이전 상태값의 반대값을 할당해준다.
toggleNotes: (itemId: string) => {
const id = itemId;
set((state) => {
return {
...state,
items: {
...state.items,
[id]: {
...state.items[id],
isNote: !state.items[id].isNote,
},
},
};
});
},
const handleToggle = () => {
if (isToggle) {
updateItem({ ...item, toggle: 'toggleOpen' });
toggleItems(item, false);
} else if (!isToggle) {
updateItem({ ...item, toggle: 'toggleClose' });
toggleItems(item, true);
}
};
[vite] Internal server error: Headers is not defined
커맨드 npm run dev를 실행하니 위와 같은 에러가 떴었다. 그때의 node 버전이 17이라 그랬던 것 같다. github readme.md에도 노드 버전 20으로 적혀있어서 버전을 변경하니 잘 작동했다.
TypeError: Cannot read properties of undefined (reading 'level')
gyul store가 리액트의 렌더링보다 느려서, undefined이 뜨는 것 같았다. undefined에서 level을 찾으려고 하니 에러가 나는 거겠지 싶어, if문 조건을 걸어 해결했다.
if (!nextItem || nextItem.level === undefined) continue;
if (item.level < 0) return state;
토글 기능에서의 오류 동작: 자신의 하위 목록이 아닌 목록도 숨겨짐. (전체 하위 목록이 숨겨짐)

toggleItems에서 for 문에 if문을 걸면 해결될 것 같았다. 전체 리스트를 돌다가, 다음 요소의 level이 item(토글 부모 요소)의 level 과 같거나 크다면(작지 않다면) 하위 요소가 아닌 것으로 판단하고 for 문을 break 걸어 빠져나온다.
for (let i = index; i < length; i++) {
const nextId = ids[i + 1];
const nextItem = updateItems[nextId];
if (!nextItem || nextItem.level === undefined) continue;
if (item.level < 0) return state;
if (item.level < nextItem.level) {
nextItem.isHide = !isHide;
} else if (item.level >= nextItem.level) {
break;
}
}

동작이 잘 되는 것을 확인할 수 있다.
상위 목록을 접었다 펼치면, 모든 하위 목록이 열리는 동작 오류 (의도한 동작 아님)

for (let i = index; i < length; i++) {
const nextId = ids[i + 1];
const nextItem = updateItems[nextId];
if (!nextItem || nextItem.level === undefined) continue;
if (item.level < 0) return state;
if (nextItem.level <= currentLevel) break;
if (nextItem.level === currentLevel + 1) {
nextItem.isHide = !isHide;
}
}
현재 item의 level + 1의 값이 다음 요소의 level과 같다면, 상위 목록이라 판단하고, 다음 요소의 목록을 !isHide 처리 해준다. 펼쳤을 때, 자식 요소의 상태를 건드리지 않아 그대로 노출되지만, 아래의 문제가 발생했다.
아래와 같은 구조에서 상위 목록을 접으면 자식 목록이 노출되는 동작 오류
- 상위 목록
- 하위 목록 1
- 자식 목록
- 자식 목록
- 하위 목록 2
- 자식 목록
- 자식 목록

문제가 된 코드
if (nextItem.level === currentLevel + 1) { nextItem.isHide = !isHide; }현재 item의 level에서 +1 만 더해주고 있기 때문에 하위 목록까지는 도달하지만, 자식 목록에는 도달할 수 없다. 따라서 자식 목록의 숨김 처리는 해주지 못하고 있는 것이다.
for (let i = index; i < length; i++) {
const nextId = ids[i + 1];
const nextItem = updateItems[nextId];
if (!nextItem || nextItem.level === undefined) continue;
if (item.level < 0) return state;
if (nextItem.level <= currentLevel) break;
if (isHide) {
nextItem.isHide = true;
nextItem.toggle = 'toggleClose';
} else {
nextItem.isHide =
nextItem.level === currentLevel + 1 ? false : true;
}
}
토글을 숨겼을 때, 하위 목록들은 전부 닫힘 처리를 하면 된다. (의도한 동작)
따라서 현재 item의 isHide가 true(토글 닫힘)이라면, 모든 하위 목록들의 isHide 상태값을 true로 설정한다. 또한, toggle 상태값을 “toggleClose”로 업데이트하여 아이콘을 변경해준다.

잘 동작하는 것을 볼 수 있다!
기존 코드에 이미 상태관리툴을 이용하여 스토어가 구현되어 있었다. 많은 코드를 본 난 그만… 그렇지만 포기할 수도 없으니 하나하나 살펴보았다. 우선, 컴포넌트에서 사용하고 있는 기능들부터 시작해서, 스토어의 state 타입, 값 등을 차례로 분석했다. 들어오는 값은 어떤 것이 있는지 살펴보고 어떻게 사용하고 있는지까지 확인하니 대략적인 흐름이 머리에 들어와 이해할 수 있었다.
그러나 확실히 사용자 경험을 많이 추구할 수록 요구사항이 복잡해지고 동작 오류가 많아졌다. 자잘한 곳에서 발견하지 못한 동작 오류가 쏟아져나오니 어디까지 수습해야 할 지도 난관이다… (적당히 수습해야겠지만)
지금까지 했던 과제들 중 가장 많은 동작 오류를 보여줬고, 그만큼 생소한 과제였다. 이미 작성된 코드 안에서, 기존 코드와 어울리게 구현해야했기 때문에 데이터 구조를 변경하거나 무언가 새로운 것을 추가하지 않았다. (그러고 싶지도 않았다) 그러다보니 꽤 어려웠던 것 같다… (삽질도 하고)
특히, 자식 요소를 결정할 때, children 방식으로 만들어야겠지? 싶었으나 데이터 구조는 플랫 구조인 것에 당황했다… … 어쨌든 해냈으니 된 거 아닐까? 그래도 해냄.
이번 과제에서는 다른 개발자가 작성한 코드에서 많은 수정을 거치지 않고 어울리도록 코드를 구현하는 것이 목적인 만큼 그것을 제대로 수행한 것 같다.