byminseok.com

회복탄력성 자가검사 웹앱 개발기 Building a Resilience Self-Test Web App

회복탄력성 자가검사 인트로 화면

회사 옆 팀에서는 AI 시대에 청소년들에게 어떤 교육을 해야 하는가에 대한 ‘AI 시민성’ 연구를 하고 있다. 오늘 어떤 연구를 하고 있는지에 대해 짤막하게 공유해주시는 시간이 있었는데, 이야기 중 ‘협력적 항해자’라는 개념에 대한 이야기가 있었다. 앞으로 학생들은 지식을 채우기만 하는 것이 아니라, 스스로 미래를 만드는 주체가 되어야 한다는 것. 그래서 혼자 깃발을 꽂는 것이 아니라 갈등을 조정하고 사회적 책무를 다하며 함께 항해하는 사람이 되어야 한다는 이야기가 덧붙여졌다. 이를 위해 필요한 여러 능력 중에서 메타인지와 회복탄력성이 제시되었고, 그중에서도 회복탄력성에 우선 방점이 찍혔다. 어떻게 해야 회복탄력성을 높일 수 있을까? 실패를 한다면? 그 이야기에 5년 전 회복탄력성에 관한 책을 읽고 자가진단을 했던 파일이 떠올랐다.

2020년 말에 어느 행사에 참여했다가 받아둔 엑셀 파일이었는데, 김주환 교수가 한국에 맞게 번안한 53문항짜리 자가진단이다. 9개의 하위 영역(감정조절력, 충동통제력, 원인분석력, 소통능력, 공감능력, 자아확장력, 자아낙관성, 생활만족도, 감사하기)에 걸친 문항 53개. 6개월 간격으로 두 번 해보고 네이버 블로그에 비공개로 기록을 남겨뒀던 것이 2021년 6월. 자아확장력이 6개월 만에 29에서 27로 떨어진 걸 보고 ‘확실히 사람을 덜 만난 시기였구나’라고 적어두었던 게 기억난다.

회사 동료들과 함께 해보면 좋겠다 싶어서 그 엑셀을 열었는데, 한참 수식을 들여다보다가 마음이 식었다. 시트 안에서 셀이 무겁게 움직였고, 모바일에서 열면 표가 깨졌고, 채점은 SUMIF가 가득했다. 53번을 다 답해야 결과가 나오는 도구가 이렇게 무거우면 사람들이 끝까지 안 한다. 이걸 어떻게 가볍게 만들지 고민하다가 클로드 코드한테 물어봤더니 곧장 ‘엑셀 말고 그냥 웹앱으로 만들면 어때요’라는 답이 돌아왔다. 마침 byminseok.com에 /lab이라는 실험 코너를 두고 있던 터라 그 하위에 만들기로 했다.

/lab/krq53/에 올라간 결과물은 정적 HTML 한 장이다. Jekyll 사이트 안에 들어 있지만 layout은 비워두고 단일 페이지로 동작한다. 인트로(소개 + 출처) → 섹션별 진행 화면 → 결과 화면(총점 + 등급 + 막대 + 9각 레이더 + 응답 다시보기)의 세 단계 흐름이고, 모든 상태는 메모리 안에서만 관리한다. localStorage를 의도적으로 쓰지 않았다. 한 번 풀고 지나가는 도구이고, 반복해서 풀고 싶다면 매번 새로 시작하는 편이 더 정직하다고 봤다. 결과를 그냥 한국인 평균과 비교해주는 안내 문구도 일부러 뺐는데, 절대값보다 본인의 6개월 전과 비교하는 트래킹이 훨씬 의미 있는 도구라고 생각해서다.

데이터 구조는 단순하다. 9개의 하위 영역을 배열로 두고, 각 영역에 그 영역에 속하는 문항의 인덱스를 묶어둔다.

const SUBSCALES = [
  { group: '자기조절능력', name: '감정조절력', items: [0,1,2,3,4,5] },
  { group: '자기조절능력', name: '충동통제력', items: [6,7,8,9,10,11] },
  // ...
  { group: '긍정성',       name: '감사하기',   items: [47,48,49,50,51,52] }
];

const REVERSE = new Set(
  [4,5,6,10,11,12,16,17,18,22,23,24,28,29,30,
   34,35,36,40,41,42,51,52,53].map(n => n - 1)
);

REVERSE Set에 들어 있는 인덱스의 문항은 채점할 때 (6 - 응답값)으로 뒤집는다. “나는 내 감정에 잘 휘말린다” 같이 점수가 낮을수록 회복탄력성이 높은 문항을 보정하기 위함이다. 점수 산식은 한 줄.

function score(idx) {
  const v = answers[idx];
  if (v === null) return 0;
  return REVERSE.has(idx) ? (6 - v) : v;
}

처음 만들었을 때 한 가지 문제가 있었다. 원본 시트는 각 영역의 앞 3문항이 정방향, 뒤 3문항이 역방향으로 묶여 있었다. 그대로 옮기면 사람이 한 섹션을 풀다가 ‘아 이제부터는 반대로 답해야겠구나’ 하고 패턴을 눈치챈다. 자기보고식 척도에서 가장 무서운 응답 편향이 바로 이 straight-lining인데, 정·역을 분리해두는 건 그걸 도와주는 안티패턴에 가깝다. 그래서 한 번 더 손을 봤다. 섹션 안에서만 문항 순서를 한 번 셔플하고, 그 순서를 세션 동안 고정한다. 섹션 자체의 순서(감정조절력 → 충동통제력 → …)는 건드리지 않는다.

function shuffle(arr) {
  const a = arr.slice();
  for (let i = a.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [a[i], a[j]] = [a[j], a[i]];
  }
  return a;
}
function reshuffleSections() {
  SUBSCALES.forEach(s => { s.displayItems = shuffle(s.items); });
}

채점은 여전히 원본 인덱스 기준이라 셔플이 점수에 영향을 주지 않는다. 화면에 보여줄 때만 displayItems를 쓰고, 결과 화면의 ‘응답 다시보기’에서는 다시 원본 1~53번 순서로 정렬해서 보여준다. 검사 시작과 다시 검사하기 버튼 두 군데에서 셔플을 다시 돌리도록 묶어뒀다.

결과 화면에서는 9개 하위 영역의 점수를 막대 그래프와 레이더 차트 두 가지로 보여준다. 레이더 쪽이 시각적으로는 가장 강력한데, 9각형 위에 본인 점수와 평균 점수를 두 겹의 영역으로 겹쳐 그린다. 한쪽 영역이 푹 들어간 곳이 보이면 거기가 본인이 약한 차원이고, 6개월 뒤 같은 도구를 다시 풀어 두 레이더를 비교하면 그 자체가 트래킹이 된다. 5년 전의 나는 자아확장력이 29에서 27로 떨어진 걸 막대그래프 하나로만 봤는데, 레이더로 봤다면 그 변화가 더 일찍 와닿았을 것 같다.

만들고 나서 5년 만에 나도 다시 한 번 풀어봤다.

나의 회복탄력성 지수와 9개 하위영역 점수

9개 하위영역을 한국인 평균과 겹쳐 그린 레이더 차트