Upgrade to Pro — share decks privately, control downloads, hide ads and more …

섬세한 ISFP의 코드 가독성 개선 경험

kakao
December 08, 2022

섬세한 ISFP의 코드 가독성 개선 경험

#코드가독성 #React

협업에서는 코드 가독성은 중요한데요,
섬세한 성격의 프론트엔드 개발자는 어떠한 점을 중점을 두고 가독성을 개선해보려고 노력했는지 사례를 살펴보겠습니다.

발표자 : coze.nutmott
카카오 엔터테인먼트의 FE개발팀 coze입니다. 저의 MBTI는 섬세한 성격인 ISFP입니다.

kakao

December 08, 2022
Tweet

More Decks by kakao

Other Decks in Programming

Transcript

  1. 서종만 Coze.Nutmott 카카오 엔터테인먼트 Copyright 2022. Kakao Corp. All rights

    reserved. Redistribution or public display is not permitted without written permission from Kakao. 섬세한 ISFP의 코드 가독성 개선 경험 if(kakao)2022
  2. 타인이 자신이 작성한 코드에 느낄 WTF에 몹시 괴로워한다. 사람들에게 친절하다.

    의견 충돌을 피한다. 타인의 감정에 지나치게 민감하다. WTF WTF Works That Frustrate ISFP의 특징
  3. 긴 서브루틴 여러가지를 수행하는 함수 올바르지 않은 네이밍 일반적인 리팩토링

    중복 코드 전역 변수 고정된 크기의 스타일 부작용을 유발 사용하지 않는 코드
  4. 긴 서브루틴 여러가지를 수행하는 함수 올바르지 않은 네이밍 ISFP의 가독성

    개선 중복 코드 전역 변수 고정된 크기의 스타일 부작용을 유발 사용하지 않는 코드 ISFP의 가독성 개선 유지보수 향상을 위한 노력 일반적인 리팩토링 이 부분이 남들에게 쉽게 이해가 안되면 어떡하지?
  5. 긴 서브루틴 여러가지를 수행하는 함수 올바르지 않은 네이밍 ISFP의 가독성

    개선 중복 코드 전역 변수 고정된 크기의 스타일 부작용을 유발 사용하지 않는 코드 ISFP의 가독성 개선 유지보수 향상을 위한 노력 일반적인 리팩토링 이 부분이 남들에게 쉽게 이해가 안되면 어떡하지?
  6. 서론 정확한 단어 고르기 다른 뜻을 가진 단어와 구분하기 보다

    구체적인 단어로 바꾸기 정확하지 않아도 좋은 경우 잘 보이는 형태로 작성해보기 정리 A = A’
  7. 다음 중 데이터가 없을 때 최초 한 번만 바뀌는 변수는?

    const { isFetching } = useQuery(['todos'], fetchTodos); const { isLoading } = useQuery(['todos'], fetchTodos);
  8. const { isFetching } = useQuery(['todos'], fetchTodos); const { isLoading

    } = useQuery(['todos'], fetchTodos); isLoading isFetching isFetching isFetching isFetching 불러옴 다시 불러옴 다시 불러옴 다시 불러옴 다음 중 데이터가 없을 때 최초 한 번만 바뀌는 변수는?
  9. const { isFetching } = useQuery(['todos'], fetchTodos); const { isLoading

    } = useQuery(['todos'], fetchTodos); isLoading isFetching isFetching isFetching isFetching 불러옴 다시 불러옴 다시 불러옴 다시 불러옴 다음 중 데이터가 없을 때 최초 한 번만 바뀌는 변수는?
  10. get? query? test('should show “Username”’, () => { render(<Login />);

    expect(screen.getByText('Username')).toBeInTheDocument(); }); test('should show “Username”’, () => { render(<Login />); expect(screen.queryByText('Username')).toBeInTheDocument(); }); Username: _
  11. get? query? test('should show “Username”’, () => { render(<Login />);

    screen.getByText('Username'); }); test('should show “Username”’, () => { render(<Login />); screen.queryByText('Username'); }); no text: _ Error: No instances found = null
  12. get? query? test('should show “Username”’, () => { render(<Login />);

    screen.getByText('Username'); }); test('should show “Username”’, () => { render(<Login />); screen.queryByText('Username'); }); no text: _ Error: No instances found = null 가져오다: 결과를 당연히 가져올 것으로 기대 질문하다: 결과는 없을 수도 있음
  13. query: 결과는 없을 수도 있지만 확인만 해볼 때 test('should show

    “Username”’, () => { render(<Login />); expect(screen.getByText('Username')).toBeInTheDocument(); }); test('should show “Username”’, () => { render(<Login />); expect(screen.queryByText('Username')).toBeInTheDocument(); });
  14. get: 가져올 대상은 필히 존재한다고 가정 test('should show “Username”’, ()

    => { render(<Login />); expect(screen.getByText('Username')).toBeInTheDocument(); }); test('should show login form', async () => { render(<Login />); const userNameField = screen.getByText('Username'); await user.type(userNameField, ‘Coze'); expect(userNameField).toHaveValue(‘Coze'); }); 가져온 뒤 대상을 활용하기를 기대하지만 추가 행동이 없음 가져온 뒤 대상을 활용 Username: Coze_
  15. get: 가져올 대상은 필히 존재한다고 가정 test('should show “Username”’, ()

    => { render(<Login />); expect(screen.getByText('Username')).toBeInTheDocument(); }); test('should show login form', async () => { render(<Login />); const userNameField = screen.getByText('Username'); await user.type(userNameField, ‘Coze'); expect(userNameField).toHaveValue(‘Coze'); }); 가져온 뒤 대상을 활용하기를 기대하지만 추가 행동이 없음 가져온 뒤 대상을 활용 Username: Coze_
  16. const FruitBox = fruit => ( <div style={{border: '1px solid

    purple'}}> <span>{fruit.name}</span> <img src={fruit.img} /> <textarea>…</textarea> <div/> ); 사과
  17. const FruitBox = fruit => ( <div style={{border: '1px solid

    purple'}}> <span>{fruit.name}</span> <img src={fruit.img} /> <textarea>…</textarea> <div/> ); 사과
  18. 컴포넌트 작성 - UI 명칭 이해하기 App Bar Card Box

    Global Navigation Bar Local Navigation Bar
  19. 컴포넌트 작성 - UI 명칭 이해하기 App Bar Card Box

    Global Navigation Bar Local Navigation Bar
  20. const FruitCard = fruit => ( <div style={{border: '1px solid

    purple'}}> <span>{fruit.name}</span> <img src={fruit.img} /> <div/> ); const FruitBox = fruit => ( <div style={{border: '1px solid purple'}}> <span>{fruit.name}</span> <img src={fruit.img} /> <div/> ); 액자처럼 감싸는 요소만을 기대 사과
  21. const FruitBox = fruit => ( <div style={{border: '1px solid

    purple'}}> <span>{fruit.name}</span> <img src={fruit.img} /> <div/> ); 사과 const FruitBox = children => ( <div style={{border: '1px solid purple'}}> {children} <div/> );
  22. import { Select } from ui/Select'; <Select size="small" value={novel} …

    import { Search } from ui/Search'; <Search size="small" value={novel} …
  23. 서론 정확한 단어 고르기 다른 뜻을 가진 단어와 구분하기 보다

    구체적인 단어로 바꾸기 정확하지 않아도 좋은 경우 잘 보이는 형태로 작성해보기 정리
  24. get (가져오다) extract (추출하다), parse (분해하다), aggregate (합치다) number (숫자)

    limit (제한이 되는 수), count (총계) change (변경하다) convert (변환하다), fi lter (거르다), override (덮어쓰다) changed (바뀐) dirty (더러운 = 수정이 이루어진) 대체 단어
  25. get (가져오다) extract (추출하다), parse (분해하다), aggregate (합치다) number (숫자)

    limit (제한이 되는 수), count (총계) change (변경하다) convert (변환하다), fi lter (거르다), override (덮어쓰다) changed (바뀐) dirty (더러운 = 수정이 이루어진) 대체 단어
  26. 서론 정확한 단어 고르기 다른 뜻을 가진 단어와 구분하기 보다

    구체적인 단어로 바꾸기 정확하지 않아도 좋은 경우 잘 보이는 형태로 작성해보기 정리
  27. 항상 정확해야 할까? test('should convert seconds to days', () =>

    { const MIN_TO_SEC = 60; const HOUR_TO_SEC = MIN_TO_SEC * 60; const DAY_TO_SEC = HOUR_TO_SEC * 24; convertSecondToText(3 * DAY_TO_SEC + 12 * HOUR_TO_SEC + 30 * MIN_TO_SEC).toEqual('3.5 days'); });
  28. 항상 정확해야 할까? test('should convert seconds to days', () =>

    { const MIN_TO_SEC = 60; const HOUR_TO_SEC = MIN_TO_SEC * 60; const DAY_TO_SEC = HOUR_TO_SEC * 24; convertSecondToText(3 * DAY_TO_SEC + 12 * HOUR_TO_SEC + 30 * MIN_TO_SEC).toEqual('3.5 days'); }); test('should convert seconds to days', () => { const MIN = 60; const HOUR = MIN * 60; const DAY = HOUR * 24; convertSecondToText(3 * DAY + 12 * HOUR + 30 * MIN).toEqual('3.5 days'); }); 초를 분으로 환산하기 위한 승수 (multiplier) 초를 분으로 환산하기 위한 승수 (multiplier)
  29. 항상 정확해야 할까? test('should convert seconds to days', () =>

    { const MIN_TO_SEC = 60; const HOUR_TO_SEC = MIN_TO_SEC * 60; const DAY_TO_SEC = HOUR_TO_SEC * 24; convertSecondToText(3 * DAY_TO_SEC + 12 * HOUR_TO_SEC + 30 * MIN_TO_SEC).toEqual('3.5 days'); }); test('should convert seconds to days', () => { const MIN = 60; const HOUR = MIN * 60; const DAY = HOUR * 24; convertSecondToText(3 * DAY + 12 * HOUR + 30 * MIN).toEqual('3.5 days'); }); 초를 분으로 환산하기 위한 승수 (multiplier) 초를 분으로 환산하기 위한 승수 (multiplier)
  30. 항상 정확해야 할까? test('should convert seconds to days', () =>

    { const MIN_TO_SEC = 60; const HOUR_TO_SEC = MIN_TO_SEC * 60; const DAY_TO_SEC = HOUR_TO_SEC * 24; convertSecondToText(3 * DAY_TO_SEC + 12 * HOUR_TO_SEC + 30 * MIN_TO_SEC).toEqual('3.5 days'); }); test('should convert seconds to days', () => { const MIN = 60; const HOUR = MIN * 60; const DAY = HOUR * 24; convertSecondToText(3 * DAY + 12 * HOUR + 30 * MIN).toEqual('3.5 days'); }); 초를 분으로 환산하기 위한 승수 (multiplier) 초를 분으로 환산하기 위한 승수 (multiplier)
  31. 서론 정확한 단어 고르기 다른 뜻을 가진 단어와 구분하기 보다

    구체적인 단어로 바꾸기 정확하지 않아도 좋은 경우 잘 보이는 형태로 작성해보기 정리
  32. 한 눈에 잘 들어오게 작성하려면? 이 발표는 섬세한 ISFP의 코드

    가독성 개선 경험이라는 제목으로 작성되었습니다. 작성자는 MBTI가 ISFP인 카카 오 엔터테인먼트의 FE개발팀 소속 coze.nutmott입니다. 이 발표는 서론에서 ISFP의 특징을 언급하며 가독성 개 선에 특히 주목하는 부분을 사례를 들어 설명합니다. 크게 두 가지 부분에 주목을 하고 있는데, 하나는 정확한 단어가 무엇인지 고민해 본 부분이고 다른 하나는 잘 보이는 형태가 무엇인지 고민해 본 부분입니다. 정확한 단어를 고를 때 다른 뜻을 가진 단어와 구분해 본 사례나 보다 구체적인 단어로 바꿔본 사례를 함수명이나 UI 컴포넌트를 예시를 들 며 소개하고 있습니다…
  33. 모델을 사용해보자 용어 정리 표 목차 페이지 하위 단원 제목

    서론 5 0 서론 본론1 10 3 정확한 단어 고르기 본론2 35 3 잘 보이는 형태로 작성 해보기 정리 70 0 정리 FE개발팀: front - end 개발팀 우시아월드: 북미 웹소설 서비스. Coze: 카카오 엔터테인먼트에서 사용하는 영문 호칭 “저는 카카오 엔터테인먼트의 FE개발팀에서 우시아월드 프 로젝트를 맡고 있는 Coze입니다.” <목차> 서론 —————————————— p9 정확한 단어 고르기————————— p20 잘 보이는 형태로 작성하기 —————— p31 결론 —————————————— p55
  34. 조건문 const type = exception ? undefined : condA ?

    'A' : condB ? condC ? 'BC' : 'BD' : 'A';
  35. 플로우차트 const type = exception ? undefined : condA ?

    'A' : condB ? condC ? 'BC' : 'BD' : 'A'; exception condA condB condC ‘A’ 네 네 네 아니오 아니오 아니오 unde fi ned 네 ‘BC’ ‘BD’ 아니오 ‘A’ द੘
  36. exception condA condB condC type TRUE unde fi ned TRUE

    ‘A’ TRUE TRUE ‘BC’ TRUE FALSE ’BD’ ‘A’
  37. let type = ‘A'; if (exception) type = undefined; if

    (condA) type = ‘A'; if (condB) { if (condC) type = 'BC'; else type = 'BD'; } const type = exception ? undefined : condA ? 'A' : condB ? condC ? 'BC' : 'BD' : 'A'; 플로우차트 -> 표
  38. const type = (function () { if (exception) return undefined;

    if (condA) return 'A'; if (condB && condC) return 'BC'; if (condB && !condC) return 'BD'; return 'A'; })(); exception condA condB condC type TRUE unde fi ned TRUE ‘A’ TRUE TRUE ‘BC’ TRUE FALSE ’BD’ ‘A’ 즉시 실행 함수와 early return의 활용
  39. let str = ''; switch (type) { case 'apple': str

    = 'ࢎҗ'; break; case 'banana': str = '߄աա'; break; default: str = 'ನب'; }
  40. const FRUIT_MAP = { apple: 'ࢎҗ', banana: '߄աա', DEFAULT: 'ನب',

    } const str = FRUIT_MAP[type] || FRUIT_MAP.DEFAULT; apple ‘사과’ banana ‘바나나’ ‘포도’ 맵 let str = ''; switch (type) { case 'apple': str = 'ࢎҗ'; break; case 'banana': str = '߄աա'; break; default: str = 'ನب'; } 대응 관계가 일직선 상에 가깝게 위치
  41. 목차 모델을 코드에 적용해 본 사례 <목차> 서론 —————————————— p9

    정확한 단어 고르기————————— p20 잘 보이는 형태로 작성하기 —————— p31 결론 —————————————— p55
  42. z - index 순서 꼬임 사례 <> <div style={{ zIndex:

    900 }} … <div style={{ zIndex: 1000 }} … </> <> <div style={{ zIndex: 3000 }} … <div style={{ zIndex: 1000 }} … </> 해당 요소의 z - index를 높여주세요 900 1000 1000 3000
  43. z - index 순서 꼬임 사례 <> <div style={{ zIndex:

    900 }} … <div style={{ zIndex: 1000 }} … </> <div style={{ zIndex: 2000 }} 모달이 가려지게 됨 900 1000 1000 2000 모달 2000 모달 3000 <> <div style={{ zIndex: 3000 }} … <div style={{ zIndex: 1000 }} … </> <div style={{ zIndex: 2000 }}
  44. 목차 작성을 응용한다면? <> <div style={{ zIndex: 900 }} …

    <div style={{ zIndex: 1000 }} … </> <div style={{ zIndex: 2000 }} … <div style={{ zIndex: 9999 }} … <목차> 서론 —————————————— p900 본론 —————————————— p1000 결론 —————————————— p2000 부록 —————————————— p9999
  45. 목차 작성을 응용한다면? <> <div style={{ zIndex: ZINDEX_USAGES.HEADER_DROPDOWN}} … <div

    style={{ zIndex: ZINDEX_USAGES.HEADER }} … </> <div style={{ zIndex: ZINDEX_USAGES.MODAL }} … <div style={{ zIndex: ZINDEX_USAGES.ALERT_SNACKBAR }} … export const ZINDEX_USAGES = { HEADER_DROPDOWN: 900, HEADER: 1000, … MODAL: 3000, ALERT_SNACKBAR: 9999, };
  46. 용어 정리를 코드에 적용해 본 사례 FE개발팀: front - end

    개발팀 우시아월드: 북미 웹소설 서비스. 세계 최대 무협 위주의 아시아 판타지 웹소설 플랫폼. Coze: 카카오 엔터테인먼트에서 사용하는 영문 호칭 “저는 카카오 엔터테인먼트의 FE개발팀에서 우시아월드 프로젝트를 맡고 있는 Coze입니다.” <용어>
  47. if (accessType === 'kakao') { return Array.from(data) .filter(item => !(item.sugar

    > 5000)) .sort((a, b) => a.energy - b.energy); } 의도를 드러내기
  48. const shouldDisplay = accessType === 'kakao'; if (shouldDisplay) { const

    foods = Array.from(data); const healthyFoods = foods.filter(menu => { const isUnhealthy = food.sugar > 5000; return !isUnhealthy; }) const calorieOrderedFoods = healthyFoods.sort((a, b) => a.energy - b.energy); return calorieOrderedFoods; } 용어 정리: 표식 삽입 if (accessType === 'kakao') { return Array.from(data) .filter(item => !(item.sugar > 5000)) .sort((a, b) => a.energy - b.energy); }
  49. 각주 모델을 코드에 적용해본 사례 나는 칠레산 양상추와 경상북도 포항에

    위치한 양계장의 닭이 낳은 계란을 버무린 샐러드를 아침 식사로 먹었습니다. 나는 서울특별시 강동구 아리수로 93가길에 위치한 학교에 갑니다.
  50. 나는 아침 식사1) 를 먹었습니다. 나는 학교2) 에 갑니다. 2)

    서울특별시 강동구 아리수로 93가길에 위치 1) 칠레산 양상추와 경상북도 포항에 위치한 양계장의 닭이 낳은 계란을 버무린 샐러드 각주 모델을 코드에 적용해본 사례
  51. const handleNovelClick = () => { if (novel) { sendLog(Events.NovelClick)({

    novel }); } }; const handleChapterClick = () => { if (novel && chapter) { sendLog(Events.ChapterClick)({ novel, chapter }); } }; … <article onClick={handleNovelClick}> ࣗࢸ {novel.name} <section onClick={handleChapterClick}> ୀఠ {chapter.name} </section> </article> 각주를 활용하여 로그 전송 코드를 개선해본 사례 소설 A 챕터 a
  52. const handleNovelClick = () => { if (novel) { sendLog(Events.NovelClick)({

    novel }); } }; const handleChapterClick = () => { if (novel && chapter) { sendLog(Events.ChapterClick)({ novel, chapter }); } }; … <article onClick={handleNovelClick}> ࣗࢸ {novel.name} <section onClick={handleChapterClick}> ୀఠ {chapter.name} </section> </article> 소설 A 챕터 a 각주를 활용하여 로그 전송 코드를 개선해본 사례
  53. const handleNovelClick = () => { if (novel) { sendLog(Events.NovelClick)({

    novel }); } }; const handleChapterClick = () => { if (novel && chapter) { sendLog(Events.ChapterClick)({ novel, chapter }); } }; … <article onClick={handleNovelClick}> ࣗࢸ {novel.name} <section onClick={handleChapterClick}> ୀఠ {chapter.name} </section> </article> 소설 A 챕터 a 각주를 활용하여 로그 전송 코드를 개선해본 사례 각주만 남기는 형태로 바꿔본다면?
  54. export default function LogReport(Component) { const Observer = (args) =>

    { return ( <div onClick={e => { const target = e.target.closest( '[data-click-log]' ); if (!target) return; const event = target?.getAttribute('data-click-log'); handler(event, target); }} > <Component {...args} /> </div> ); }; return Observer; } 클릭 이벤트를 모아서 처리하는 HOC 작성 소설 A 챕터 a 챕터 b 소설 B 챕터 a 챕터 b 소설 Z 챕터 a 챕터 b … LogReport
  55. const handleNovelClick = () => { if (novel) { sendLog(Events.NovelClick)({

    novel }); } }; const handleChapterClick = () => { if (novel && chapter) { sendLog(Events.ChapterClick)({ novel, chapter }); } }; … <article onClick={handleNovelClick}> ࣗࢸ {novel.name} <section onClick={handleChapterClick}> ୀఠ {chapter.name} </section> </article> 각주를 활용하여 로그 전송 코드를 개선해본 사례 <article data-click-log={Events.NovelClick}> ࣗࢸ {novel.name} <section data-click-log={Events.ChapterClick}> ୀఠ {chapter.name} </section> </article> HOC의 handler에서 처리
  56. const handleNovelClick = () => { if (novel) { sendLog(Events.NovelClick)({

    novel }); } }; const handleChapterClick = () => { if (novel && chapter) { sendLog(Events.ChapterClick)({ novel, chapter }); } }; … <article onClick={handleNovelClick}> ࣗࢸ {novel.name} <section onClick={handleChapterClick}> ୀఠ {chapter.name} </section> </article> 파라미터 전송은 어떻게? <article data-click-log={Events.NovelClick}> ࣗࢸ {novel.name} <section data-click-log={Events.ChapterClick}> ୀఠ {chapter.name} </section> </article> novel, chapter 파라미터 전송이 누락됨
  57. <article data-click-log={Events.NovelClick}> {novel.name} <section data-click-log={Events.ChapterClick} > {chapter.name} </section> </article> <article

    data-click-param={novel} data-click-log={Events.NovelClick}> {novel.name} <section data-click-param={chapter} data-click-log={Events.ChapterClick}> {chapter.name} </section> </article> 파라미터 전송: 각주에 정보를 더 기입하기 ChapterClick 로그 전송 novel, chapter 파라미터 취합
  58. export const extractParams = (el: HTMLElement) => { if (!el)

    return {}; let paramEl = el; const params = {}; for (let i = 0; i < 3; i++) { const paramsEl = paramEl.closest(‘[data-click-param]'); if (!paramsEl || !paramsEl.parentElement) break; const params = paramsEl.getAttribute('data-click-param'); Object.assign(params, JSON.parse(params ?? '{}')); paramEl = paramsEl.parentElement; } return params; }; 파라미터를 DOM의 data - attribute에서 취합
  59. const Novel = ({ novel, chapter }) => ( <article

    onClick={() => { sendLog(Events.NovelClick)({ novel }); }} > {novel.name} <Chapter novel={novel} chapter={chapter} /> </article> ); const Chapter = ({ novel, chapter }) => ( <section onClick={() => { sendLog(Events.ChapterClick)({ novel, chapter }); }} > {chapter.name} </section> ); 기존의 Props Drilling 문제 chapter에서는 로그 전송을 위해 novel의 정보도 props를 통해 전달받고 있었음
  60. const Novel = ({ novel, chapter }) => ( <article

    onClick={() => { sendLog(Events.NovelClick)({ novel }); }} > {novel.name} <Chapter novel={novel} chapter={chapter} /> </article> ); const Chapter = ({ novel, chapter }) => ( <section onClick={() => { sendLog(Events.ChapterClick)({ novel, chapter }); }} > {chapter.name} </section> ); 기존의 Props Drilling 문제 해결 const Novel = ({ novel, chapter }) => ( <article data-click-param={novel} data-click-log={Events.NovelClick}> {novel.name} <Chapter chapter={chapter} /> </article> ); const Chapter = ({ chapter }) => ( <section data-click-param={chapter} data-click-log={Events.ChapterClick}> {chapter.name} </section> ); handler가 DOM에서 필요한 정보를 취합하기 때문에 상위 요소의 값인 novel을 넘겨줄 필요가 없음
  61. ISFP의 가독성 개선 사례 정리 더 잘 보이는 형태를 고려해본

    사례 좀 더 정확한 단어를 고려해본 사례 목차 각주 용어 표 ࠺त೧ࠁ੉૑݅ ׮ܲ ੄޷ܳ ыח ױয ҳ࠙ ੌ߈੸ੋ ױযܳ ҳ୓੸ੋ ױয۽ ؀୓ ࠗ੿ഛೞ؊ۄب оةࢿ੉ જই૑ݶ ೲਊ
  62. ISFP의 가독성 개선 사례 정리 더 잘 보이는 형태를 고려해본

    사례 좀 더 정확한 단어를 고려해본 사례 ࠺त೧ࠁ੉૑݅ ׮ܲ ੄޷ܳ ыח ױয ҳ࠙ ੌ߈੸ੋ ױযܳ ҳ୓੸ੋ ױয۽ ؀୓ ࠗ੿ഛೞ؊ۄب оةࢿ੉ જই૑ݶ ೲਊ 목차 각주 용어 표
  63. ISFP의 가독성 개선 사례 정리 더 잘 보이는 형태를 고려해본

    사례 좀 더 정확한 단어를 고려해본 사례 목차 각주 용어 표 ࠺त೧ࠁ੉૑݅ ׮ܲ ੄޷ܳ ыח ױয ҳ࠙ ੌ߈੸ੋ ױযܳ ҳ୓੸ੋ ױয۽ ؀୓ ࠗ੿ഛೞ؊ۄب оةࢿ੉ જই૑ݶ ೲਊ
  64. Q&A