[소고] 페어 프로그래밍에 관한 단상 feat. ChatGPT

@codemaru · December 01, 2023 · 19 min read

테스트 주도 프로그래밍, 스크럼, 페어 프로그래밍 같은 애자일로 분류되는 많은 방법론들이 있다 좋아하는 것들도 있고 그렇지 않은 것들도 있다. 그중에 특히 극혐하는 것은 스크럼과 페어 프로그래밍이다. 내가 만약 면접 자리에서 궁금한게 없냐는 질문을 받는다면 반드시 물어볼 것이다. 스크럼과 페어 프로그래밍을 하나요? 만약 한다면 강제 사항인가요? 강제 사항이라면 나는 그 회사를 입사하지 않을 생각이다. 그만큼 싫어한다. 일단 스크럼은 차치하고, 오늘은 페어 프로그래밍에 대해서만 알아보자.

개인적으론 페어 프로그래밍이란 메커니즘이 두 페어가 현재 수행하고 있는 과제에 대해서 비슷한 지적 능력을 가졌을 때 그 효과가 절정에 이른다고 생각한다. 그렇지 않고 차이가 일정 수준 이상 난다면 그건 페어 프로그래밍이라기 보다는 도제식 교육이나 수업에 가까워진다고 생각한다. 물론 그렇지 않다는 수많은 연구 결과가 존재한다는 것은 나도 알고있다. 하지만 연구 결과는 연구 결과고 임상은 임상이니까. 내가 현업에서 느낀 심정은 연구 결과와는 많은 차이가 있었다.

그렇다면 두 페어의 실력만 비슷하면 시너지가 날까? 그렇지도 않다. 왜냐면 우리는 사람이기 때문이다. 사람이 뭔가? 아 다르고 어 다르게 느끼는 게 사람이다. 심지어 똑같은 아도 그사람 기분이 어떤지에 따라 몹시 다른 결과를 가져오는게 사람이다. 즉, 둘이서 한 모니터를 바라보며 사이좋게 작업을 하기 위해서는 케미라는 아주 미묘한 성질의 것이 또 잘 맞아야 한다는 말이다.

그렇다면 현실 세계에서 비슷한 지적 능력을 가진 케미가 잘 맞는 파트너를 만나서 페어 프로그래밍을 할 수 있는 확률은 얼마가 될까? 자세히 계산해 보지 않아도 내가 로또에 당첨되는 게 더 빠르다는 생각이다.

이렇게 설명하면 지적 우위를 가진 사람이 꼭 손해를 보는 느낌인 것 같다. 하지만 내가 현실 세계에서 느낀 페어 프로그래밍은 루즈 루즈 게임인 경우가 많았다. 실력이 좋은 사람은 딱히 페어 프로그래밍으로 얻은 이득은 없다고 느끼고 불편했다는 말을 많이 했다. 그런데 재미있게도 실력이 낮아 보이는 프로그래머도 똑같은 얘기를 하는 경우가 많았다. 이 지점이 도제식 교육과 페어 프로그래밍의 차이인 것이다. 도제식 교육이었다면 아마 배우는 사람은 그나마 그래도 만족했을 것이다.

무튼 이런 여타의 이유로 나는 페어 프로그래밍을 싫어했다. 뭐, 지금까지는 말이다. 하지만 올해 2월을 기점으로 세상이 바뀌었다. ChatGPT가 등장한 것이다. 이 친구는 내가 앞서 서술한 페어 프로그래밍의 모든 단점을 단박에 없앤다. 지적 능력은 당연히 나보다 월등하다. 그럼에도 감정이 없고 내가 어떤 쌉소리를 하더라도 화내는 법이 없이 친절하게 알려준다. 그리고 365일 24시간 내내 내가 원할 때 언제든지 나와 함께 코딩할 준비가 돼 있다. 물론 가끔 openai 서버가 다운되긴 한다. 자주 있는 일은 아니니 그럴 땐 쉬면 되겠다.

ChatGPT와 근 9개월 가량 페어 프로그래밍을 함께 하면서 나는 앞선 단점들만 없으면 페어 프로그래밍이 확실히 효과적이라는 생각이 들 게 되었다. 그래서 이제는 입사 면접 자리에서 나에게 뭐가 궁금한지 물어보면 다음과 같이 물어볼 것 같다. 인공지능 사용이 가능한가요? ChatGPT를 지원해 주나요?

아래는 블로그의 테이블 셀 크기를 동적으로 변경하는 자바스크립트 코드를 작성하기위한 ChatGPT와의 대화 일부다. 복사해놓고 다시 보니 이건 페어 프로그래밍이라기보다는 코딩 노예를 고용한 것에 가깝지 않냐는 느낌이 약간 들긴 한다. 어쨌든 이러나 저러나 멋진 신세계다.

내가 인공지능에게 코딩을 배우는 날이 올거라곤 상상 못했다.

내가 인공지능에게 코딩을 배우는 날이 올거라곤 상상 못했다.

ChatGPT와의 페어 프로그래밍이라 쓰고 코딩 시키기

You
gatsby 블로그 마크다운에서 테이블 셀 크기가 3개가 있는데 처음 두개는 너무 작고, 나머지 하나가 너무 커. 적당하게 나눠서 표시되게 만드려면 어떻게 해야 해?

ChatGPT
Gatsby 블로그에서 Markdown 테이블의 셀 크기를 조정하려면 몇 가지 방법을 시도할 수 있습니다. Markdown 자체에는 셀 크기를 직접 조정하는 기능이 없으므로, CSS를 사용하여 스타일을 적용하는 방법이 가장 일반적입니다. 다음은 몇 가지 방법을 소개합니다:

... 중략 ...

  • CSS
  • Inline 스타일
  • 자바스크립트 사용

... 중략 ...

You
셀 내용에 따라 다이나믹하게 설정할 수는 없어?

ChatGPT
셀 내용에 따라 테이블 셀 크기를 동적으로 조정하려면 JavaScript를 사용하는 것이 가장 효과적입니다. CSS와 Markdown만으로는 셀의 내용에 반응하여 크기를 조정하는 것이 불가능합니다. JavaScript를 사용하면 셀 내용을 읽고, 그 내용에 따라 적절한 크기를 동적으로 할당할 수 있습니다.

... 중략 ...

document.addEventListener("DOMContentLoaded", function() {
  const table = document.querySelector('.my-table'); // 대상 테이블 선택
  const rows = table.getElementsByTagName('tr');

  for (let row of rows) {
    const cells = row.getElementsByTagName('td');
    for (let cell of cells) {
      const contentLength = cell.textContent.length;
      // 셀의 내용 길이에 따라 셀 크기 조정
      if (contentLength < 10) {
        cell.style.width = '20%';
      } else if (contentLength < 20) {
        cell.style.width = '30%';
      } else {
        cell.style.width = '50%';
      }
    }
  }
});

You
그렇게 하는 거 보다는 각 칼럼의 가장 긴 길이를 각각 구해서 해당 비율로 크기를 설정하는 것이 좋을 거 같아. 그렇게 수정해줘

ChatGPT
셀 내용에 따라 테이블 셀의 크기를 동적으로 조정하기 위해, 각 칼럼의 가장 긴 셀의 내용을 기준으로 크기를 결정하는 접근 방법을 사용할 수 있습니다. 이 방법은 JavaScript를 사용하여 구현할 수 있으며, 다음 단계로 구성됩니다:

... 중략 ...

document.addEventListener("DOMContentLoaded", function() {
  const table = document.querySelector('.my-table'); // 대상 테이블 선택
  const rows = table.getElementsByTagName('tr');
  let maxContentLengths = [];
  let totalMaxLength = 0;

  // 각 칼럼의 최대 길이 계산
  for (let row of rows) {
    const cells = row.getElementsByTagName('td');
    cells.forEach((cell, index) => {
      const contentLength = cell.textContent.trim().length;
      maxContentLengths[index] = Math.max(maxContentLengths[index] || 0, contentLength);
    });
  }

  // 전체 최대 길이 계산
  totalMaxLength = maxContentLengths.reduce((acc, length) => acc + length, 0);

  // 각 칼럼에 비율에 따른 너비 설정
  for (let row of rows) {
    const cells = row.getElementsByTagName('td');
    cells.forEach((cell, index) => {
      const widthPercentage = (maxContentLengths[index] / totalMaxLength) * 100;
      cell.style.width = `${widthPercentage}%`;
    });
  }
});

You
cells.forEach is not a function 오류가 발생해

ChatGPT
cells.forEach is not a function 오류가 발생하는 이유는 getElementsByTagName 메서드가 반환하는 HTMLCollection 객체가 Array가 아니기 때문에 forEach 메서드를 지원하지 않기 때문입니다. 이 문제를 해결하기 위해, Array.from 메서드를 사용하여 HTMLCollection을 배열로 변환하거나, 전통적인 for 루프를 사용할 수 있습니다.

... 중략 ...

document.addEventListener("DOMContentLoaded", function() {
  const table = document.querySelector('.my-table');
  const rows = Array.from(table.getElementsByTagName('tr'));
  let maxContentLengths = [];
  let totalMaxLength = 0;

  // 각 칼럼의 최대 길이 계산
  rows.forEach(row => {
    const cells = Array.from(row.getElementsByTagName('td'));
    cells.forEach((cell, index) => {
      const contentLength = cell.textContent.trim().length;
      maxContentLengths[index] = Math.max(maxContentLengths[index] || 0, contentLength);
    });
  });

  // 전체 최대 길이 계산
  totalMaxLength = maxContentLengths.reduce((acc, length) => acc + length, 0);

  // 각 칼럼에 비율에 따른 너비 설정
  rows.forEach(row => {
    const cells = Array.from(row.getElementsByTagName('td'));
    cells.forEach((cell, index) => {
      const widthPercentage = (maxContentLengths[index] / totalMaxLength) * 100;
      cell.style.width = `${widthPercentage}%`;
    });
  });
});

You
table 이 하나도 없는 경우에 예외 처리해줘

ChatGPT
테이블이 없는 경우에 대한 예외 처리를 추가하려면, 먼저 테이블 요소의 존재 여부를 확인하는 로직을 추가해야 합니다. 이를 위해 document.querySelector로 선택한 테이블 요소가 null인지 확인하는 간단한 조건문을 사용할 수 있습니다. 테이블이 존재하지 않는 경우, 함수의 실행을 중단하거나 적절한 메시지를 표시하도록 할 수 있습니다.

... 중략 ...

You
table에 tr이나 td가 하나도 없는 경우도 예외 처리가 필요할 거 같아.

ChatGPT
테이블에 tr이나 td 요소가 하나도 없는 경우에 대한 예외 처리를 추가하는 것은 좋은 접근입니다. 이를 위해서는 tr 요소의 배열이 비어있는지, 그리고 각 tr 내의 td 요소가 존재하는지를 확인하는 조건문을 추가해야 합니다. tr이나 td 요소가 없는 경우에는 함수의 실행을 중단하거나 적절한 경고 메시지를 표시할 수 있습니다.

... 중략 ...

You
Array.from(null);은 정의돼 있어?

ChatGPT
Array.from(null)은 정의되어 있지 않습니다. Array.from 함수는 유사 배열 객체나 반복 가능한 객체를 새로운 배열 인스턴스로 만드는 메서드입니다. 그러나 null은 반복 가능한 객체가 아니기 때문에 Array.from(null)을 사용하려고 하면 오류가 발생합니다.

따라서 Array.from을 사용하기 전에 반드시 해당 객체가 null이 아닌지 확인해야 합니다. 예를 들어, document.getElementsByTagName나 document.querySelectorAll과 같은 DOM 메서드가 비어있는 NodeList를 반환할 경우, 이를 배열로 변환하기 전에 null 여부를 검사해야 합니다.

You
그렇게 수정해서 함수 새로 작성해줘

ChatGPT
앞서 논의한 내용을 바탕으로, 테이블에 tr이나 td가 없는 경우에 대한 예외 처리를 포함한 새로운 JavaScript 함수를 작성하겠습니다. 이 함수는 테이블이 존재하고, 적어도 하나의 tr과 td가 있는 경우에만 셀 크기 조정 로직을 실행합니다.

... 중략 ...

You
비율이 너무 낮은 셀이 나오면 문제가 될 거 같아서 최소 15% 이상은 보장되도록 코드를 수정해줘

ChatGPT
셀의 너비 비율이 너무 낮아지지 않도록 최소 15% 이상의 너비를 보장하는 로직을 추가하겠습니다. 이를 위해 각 셀의 계산된 비율을 검사하고, 만약 그 비율이 15% 미만인 경우 15%로 조정하는 로직을 추가합니다. 또한, 이러한 조정으로 인해 총 너비 합계가 100%를 초과하지 않도록 전체 너비의 합을 다시 계산해야 합니다.

다음은 수정된 코드입니다:

document.addEventListener("DOMContentLoaded", function() {
  const table = document.querySelector('.my-table');

  if (!table) {
    console.log('테이블이 존재하지 않습니다.');
    return;
  }

  const rows = Array.from(table.getElementsByTagName('tr'));

  if (rows.length === 0) {
    console.log('테이블에 행이 없습니다.');
    return;
  }

  let maxContentLengths = [];
  let totalMaxLength = 0;
  let isCellPresent = false;

  rows.forEach(row => {
    const cells = Array.from(row.getElementsByTagName('td'));

    if (cells.length > 0) {
      isCellPresent = true;
    }

    cells.forEach((cell, index) => {
      const contentLength = cell.textContent.trim().length;
      maxContentLengths[index] = Math.max(maxContentLengths[index] || 0, contentLength);
    });
  });

  if (!isCellPresent) {
    console.log('테이블의 행에 셀이 없습니다.');
    return;
  }

  totalMaxLength = maxContentLengths.reduce((acc, length) => acc + length, 0);

  let widthPercentages = maxContentLengths.map(length => (length / totalMaxLength) * 100);
  let adjustedPercentages = widthPercentages.map(width => Math.max(width, 15));
  let adjustedTotal = adjustedPercentages.reduce((acc, width) => acc + width, 0);

  rows.forEach(row => {
    const cells = Array.from(row.getElementsByTagName('td'));
    cells.forEach((cell, index) => {
      const adjustedWidth = (adjustedPercentages[index] / adjustedTotal) * 100;
      cell.style.width = `${adjustedWidth}%`;
    });
  });
});

You
그런데 저렇게 작성했다고 하면 예를 들어 칼럼이 세개고 비율이 5, 5, 90이었는데 조정을 해서 15, 15, 90이 되었다고 한다면 최종적으로 첫번째 칼럼은 15 / 120이 되서 12.5%가 되니까 원래 요구했던 15%보다 작게 설정되는 거 아냐?

ChatGPT
맞습니다. 코드에서 조정된 비율이 15%, 15%, 90%로 변경된 경우, 총합이 120%가 되어 각 비율을 다시 조정할 때, 원래 의도했던 최소 15% 비율이 보장되지 않을 수 있습니다. 이 문제를 해결하기 위해, 조정된 비율의 합이 100%를 초과할 경우에 대한 추가적인 처리가 필요합니다.

비율을 조정하는 과정에서, 각 칼럼의 비율이 최소 15%가 되도록 하면서도 전체 합이 100%를 넘지 않도록 조정하는 방법은 다음과 같습니다:

먼저, 모든 칼럼의 비율을 계산합니다. 15% 미만인 칼럼의 비율을 15%로 조정합니다. 조정 후 총합이 100%를 넘는 경우, 15% 이상인 칼럼들의 비율을 적절히 감소시켜 총합이 100%가 되도록 합니다. 이 과정은 비교적 복잡할 수 있으며, 특히 칼럼의 수나 비율에 따라 다양한 시나리오를 고려해야 할 수 있습니다. 간단한 해결책으로, 모든 칼럼이 최소한 15%를 차지할 수 있도록 고정된 비율을 설정하거나, 동적으로 비율을 조정하는 더 복잡한 알고리즘을 구현할 수 있습니다.

You
말로 하지 말고 그렇게 코드를 작성해줘

ChatGPT

... 중략 ...

document.addEventListener("DOMContentLoaded", function() {
  const table = document.querySelector('.my-table');

  if (!table) {
    console.log('테이블이 존재하지 않습니다.');
    return;
  }

  const rows = Array.from(table.getElementsByTagName('tr'));

  if (rows.length === 0) {
    console.log('테이블에 행이 없습니다.');
    return;
  }

  let maxContentLengths = [];
  let totalMaxLength = 0;
  let isCellPresent = false;

  rows.forEach(row => {
    const cells = Array.from(row.getElementsByTagName('td'));

    if (cells.length > 0) {
      isCellPresent = true;
    }

    cells.forEach((cell, index) => {
      const contentLength = cell.textContent.trim().length;
      maxContentLengths[index] = Math.max(maxContentLengths[index] || 0, contentLength);
    });
  });

  if (!isCellPresent) {
    console.log('테이블의 행에 셀이 없습니다.');
    return;
  }

  totalMaxLength = maxContentLengths.reduce((acc, length) => acc + length, 0);

  let widthPercentages = maxContentLengths.map(length => (length / totalMaxLength) * 100);
  let adjustedPercentages = widthPercentages.map(width => Math.max(width, 15));
  let adjustedTotal = adjustedPercentages.reduce((acc, width) => acc + width, 0);

  // 총합이 100%를 초과하는 경우, 초과분을 줄이는 로직
  if (adjustedTotal > 100) {
    let excess = adjustedTotal - 100;
    adjustedPercentages = adjustedPercentages.map(width => {
      if (width > 15 && excess > 0) {
        let decrease = Math.min(width - 15, excess);
        excess -= decrease;
        return width - decrease;
      }
      return width;
    });
  }

  rows.forEach(row => {
    const cells = Array.from(row.getElementsByTagName('td'));
    cells.forEach((cell, index) => {
      cell.style.width = `${adjustedPercentages[index]}%`;
    });
  });
});
@codemaru
돌아보니 좋은 날도 있었고, 나쁜 날도 있었다. 그런 나의 모든 소소한 일상과 배움을 기록한다. 여기에 기록된 모든 내용은 한 개인의 관점이고 의견이다. 내가 속한 조직과는 1도 상관이 없다.
(C) 2001 YoungJin Shin, 0일째 운영 중