main-logo

약관을 자동으로 만들어보자!

실무에 알고리즘 활용하기

profile
nocturne9no1
2023년 04월 24일 · 0 분 소요

들어가며

알고리즘은 개발자들에게 보통 대기업 입사를 위한 코딩테스트, 혹은 AI와 같은 어나더 레벨의 개발에 쓰인다고 인식되고 있습니다. 하지만 알고리즘은 개발자에게 뗄 수 없는 굉장히 중요한 요소라고 생각합니다. 그 이유는 기업이 구인 시 코딩테스트를 보는 이유에 대해 생각해 보면 나옵니다.

코딩테스트는 넓게 보면 굉장히 단순한 구조입니다. 문제를 읽는다 → 문제가 요구하는 대로 구현한다. 여기서 더 자세히 들여다보면 우선 문제의 요구 사항을 명확하게 파악해야 합니다. 어떤 범위의 입력을 받고 그것을 통해 어떤 결과물을 내야 하는지 한 치의 오차도 없이 파악해야 정확성, 효율성 모두 만족하는 결과를 낼 수 있습니다. 문제의 조건에 맞춰서 어떤 알고리즘을 써야 하는지 생각해야 하기도 합니다. 가장 중요한 점은 좀 더 난도 높은 문제에서는 현실의 문제를 끌어와서 문제 해결을 요구한다는 점입니다. 이는 실제 개발에서도 굉장히 중요하게 요구되는 능력입니다.

개발자로서 가장 중요한 것이 이 ‘문제 해결 능력’이라고 생각합니다. 어떠한 문제를 가장 정확하고 효율적으로 해결하는 것이 개발자로서 가져야 하는 가장 중요한 역량이죠.

여기서 한 걸음 더 나아가 저는 ‘문제 인식 능력’ 또한 중요하다고 말씀드리고 싶습니다. 누군가는 그저 잠깐 불편함을 느끼며 그것을 감수하며 넘어갈 상황이지만 이를 놓치지 않고 해결하려는 의식 또한 정말 중요합니다. 저희가 사용하는 수많은 라이브러리 또한 그러한 문제들을 ‘인식’하고 ‘해결’하여 저희에게 전달된 것이죠. 이러한 문제 인식과 해결 능력을 갖춘 개발자들이 현대 개발 생태계를 이끌어 간다고 말해도 과언이 아닙니다.

이번 글을 통해서 실제 개발 상황에서 제가 느꼈던 문제 상황과 그를 해결하기 위해 어떤 방법을 사용했는지에 대해 공유해 보고자 합니다.

본론

문제 인식

어느 날 저는 한 서비스의 약관 페이지를 수정해달라는 태스크를 받게 되었습니다. 약관은 한번 정해지면 서비스 종료 시점까지 가만히 있지 않고 조항이 바뀔 때마다 날짜가 갱신되며 계속 바뀝니다. 당시에는 더 좋은 방법을 생각하지 못하고 빠른 일 처리를 위해서 기존 코드에 분기 처리를 하며 있는 것과 없는 것을 바꿔주었습니다.

사실 처음 수정 때에는 조그마한 불편함만 있을 뿐이었습니다. 코드를 보면 살짝 어지러워지기도 했지만 ‘하지만 빨랐죠?‘로 스스로 합리화해 버렸습니다. 하지만 두 번, 세 번 요청이 들어오며 늘어나는 분기에 되돌아가기에는 늦었다고 생각했습니다. 결국에는 코드가 이렇게 돼버린 것이죠.

<ul>
  <li>1항 ...</li>
  <li>2항 ...</li>
  <li>3항 ...</li>
  {version !== '2020-10-20' && <li>4항 ...</li>}
  {version !== '2020-12-25' && version !== '2020-10-20' && <li>5항 ...</li>}
  {version === '2021-01-01' && <li>6항 ...</li>}
</ul>

이러한 기술적 부채를 잔뜩 껴안고 살아가다가 마침 사이트의 리뉴얼이 결정되었습니다. 문제 해결의 기회가 왔던 것이죠.

문제 해결 방안

문제 해결의 핵심 논제는 ‘마크업 쪽, 렌더링 부분은 건드리지 않도록 하자’였습니다. 약관의 내용만 집어넣으면 곧바로 렌더링 시켜주는 시스템을 만들자는 것이었죠. 평소 마크다운 에디터를 만들어 보려고 생각했던 부분에서 착안하여 ‘약관 parse’를 만들기로 하였습니다.

이제 문제는 parser를 구현하는 방법에 대한 것이었습니다. 약관은 국문과 영문 두 가지 언어의 JSON 파일로 제공하고 있었습니다. 따라서 key value의 쌍을 가지고 있었죠. 여기서 key를 적극 활용하기로 하였습니다. 서비스에 사용되는 약관은 1) 2) 3) 과 같은 숫자 리스트 a) b) c) 가) 나) 다) 와 같은 문자 리스트로 나뉘어져 있었고. 약관의 depth에 따라 숫자/문자도 다른 형태를 띠고 있었습니다.

우선 key를 알맞게 통일시켜야 했습니다. 숫자/문자 리스트 모두 depth에 상관없이 ‘numList’, ‘charList’와 같은 형식으로 맞춰주었습니다. 이를 통해 parser 내부에서는 이 두 개만 체크하면 어떤 것을 렌더링 시킬지 결정할 수 있게 됩니다.

그다음은 depth에 대한 문제입니다. JSON 을 코드가 죽 읽으면서 ‘내가 지금 얼마나 들여 쓰기 해야 하지?’ 하는 문제가 남아있었습니다. 이를 위해 깊이 우선 탐색(DFS) 알고리즘을 차용하기로 했습니다. 함수가 타고 들어갔던 만큼의 depth를 줌으로써 들여 쓰기의 양을 결정할 수 있게 됩니다.

구현

따라서 JSON 파일은 다음과 같은 형식을 띠게 됩니다.

"numList": [
  "1항",
  "2항",
  "3항",
  "numList": {
    "3-1항",
    "3-2항",
    "3-3항"
  }
]

코드는 아래와 같은 형식입니다. 많은 case가 생략된 구현의 의도를 파악하기 위한 코드입니다.

function parse(nowKey, depth, element, lang) {
  if (typeof element === 'object') {
    return Object.keys(element)
      .map((el) => {
        const now = element[el];
        // 뎁스 끝까지 재귀
        const nextKey = nowKey === '' ? el : nowKey + '.' + el;
        return `${parse(nextKey, depth + 1, now, lang)}`;
      })
      .join('');
  } else {
    return `<span class="terms-item">${element}</span>`;
  }
}
  1. 처음 들어온 요소가 객체면 렌더링 할 수 없으므로 순회하며 다음 depth로 넘어갑니다.

    1-1. 넘어가기 전에 현재 JSON key를 읽기 위해 키 값을 붙이며 넘어갑니다.

    1-2. depth를 1 더해준 뒤 넘어가 들여 쓰기 양을 결정합니다.

  2. 만약 object 타입이 아니라면 렌더링 시켜 줍니다.

아무래도 짧게 설명하려다 보니 많은 부분이 생략된 코드입니다. 위 코드에서 리스트를 위한 렌더링, 테이블 파싱 및 렌더링, 링크 렌더링 등등이 생략되어 있습니다. 이를 분기하여 렌더링하기 위해 어떤 코드가 추가되어야 할지 여러분들도 재미있게 고민해 보는 건 어떨까요??

마치며

지금까지 알고리즘을 실무에 적용한 방법에 대해 나눠보았습니다. 작업을 하면서도 ‘굳이 이렇게 할 필요가 있을까?’ 스스로 생각해 본 적도 있습니다. DFS 자체만 두고 보면 효율이 좋은 알고리즘도 아니거니와 선형으로 짧게 갈 수 있는 방법도 있을 거란 생각이 지금도 듭니다. 하지만 이렇게 끊임없이 더 나아지기 위해 고민하는 것 또한 더 좋은 개발자가 되기 위해서 필요한 과정이라고 생각합니다! 여러분들도 실무에 알고리즘 지식을 활용해 보는 건 어떨까요??