[익스프레션] 중복해서 작성해야하는 코드 줄이기

한글 타이핑 자막의 완성포인트 텍스트에만 강조 박스 만들기 코드의 개선

After Effect Blender expression Premiere 모션 그래픽 템플릿 애프터이펙트 익스프레션 프리미어

앞서 만든 한글 타이핑 자막의 완성과 포인트 텍스트에만 강조 박스 만들기에서는 동일한 함수를 각 레이어마다 작성해서 사용하였습니다. 애프터 이펙트는 블록별로 익스프레션 코드를 작성해야하기 때문인데 이 부분을 어떻게 개선할 수 있는지 살펴보겠습니다.

함수의 재사용

익스프레션에서는 다른 블록에서 만든 함수를 재사용할 수는 없습니다. 즉, 같은 함수를 이용하기 위해서는 블록마다 같은 함수를 새로 만들어서 사용해야하는 것이죠. 이 부분을 어떻게 개선할 수 있을까요?

정답은 함수를 재사용할 수는 없지만 출력된 함수 값을 재사용하는 것입니다. 즉, 타이핑 자막의 완성에서 작성한 코드 기억 나시나요?

이 세 개의 레이어에 동일한 코드가 들어갔었습니다. 초성, 중성, 종성을 정의하고, 타이핑되는 함수들을 세 레이어에 공통으로 작성을 했었죠.

//RenderText에 중복해서 들어갔던 코드

let cCho = ["ㄱ", "ㄲ", "ㄴ", "ㄷ", "ㄸ", "ㄹ", "ㅁ", "ㅂ", "ㅃ", "ㅅ", "ㅆ", "ㅇ", "ㅈ", "ㅉ", "ㅊ", "ㅋ", "ㅌ", "ㅍ", "ㅎ"], 
cJung = ["ㅏ", "ㅐ", "ㅑ", "ㅒ", "ㅓ", "ㅔ", "ㅕ", "ㅖ", "ㅗ", "ㅘ", "ㅙ", "ㅚ", "ㅛ", "ㅜ", "ㅝ", "ㅞ", "ㅟ", "ㅠ", "ㅡ", "ㅢ", "ㅣ"],  
cJong = ["", "ㄱ", "ㄲ", "ㄳ", "ㄴ", "ㄵ", "ㄶ", "ㄷ", "ㄹ", "ㄺ", "ㄻ", "ㄼ", "ㄽ", "ㄾ", "ㄿ", "ㅀ", "ㅁ", "ㅂ", "ㅄ", "ㅅ", "ㅆ", "ㅇ", "ㅈ", "ㅊ", "ㅋ", "ㅌ", "ㅍ", "ㅎ"];

let toStringFromCharCode = (cho, jung, jong) =>
  String.fromCharCode(44032 + cho * 588 + jung * 28 + (jong || 0));
  
let seperateChar = (char) => {
  let cCode = char.charCodeAt() - 0xac00;
  let jong = cCode % 28;
  let jung = ((cCode - jong) / 28) % 21;
  let cho = ((cCode - jong) / 28 - jung) / 21;
  return []
    .concat(cCho[cho])
    .concat(toStringFromCharCode(cho, jung))
    .concat(cJong[jong] !== "" ? toStringFromCharCode(cho, jung, jong) : []);
};

let toKorChars = (char) => {
  let cCode = char.charCodeAt();
  return [].concat(
    (!cCode && []) ||
      (cCode === 32 && " ") ||
      ((cCode < 0xac00 || cCode > 0xd7a3) && char) ||
      seperateChar(char)
  );
};

let typing = (str) =>
  str
    .split("")
    .map((char) => toKorChars(char))
    .reduce(
      (pre, cur) =>
        pre.concat(
          cur.map((v) => (pre.length !== 0 ? pre[pre.length - 1] : "") + v)
        ),
      [""]
    );

const countCharComponents = str => {
  if (typeof str !== "string" || !str) return 0;
  return str.split("").reduce((sum, ch) => {
    return sum + toKorChars(ch).length;
  }, 0);
};

const totalText = thisComp.layer("Text").text.sourceText.valueAtTime(0);
const pointText = thisComp.layer("PointText").text.sourceText.valueAtTime(0);

let firstRenderText, beforeRenderText, renderText;

if (!pointText || totalText.indexOf(pointText) === -1) {
	firstRenderText = totalText;
	beforeRenderText= "";
	renderText = "";
} else {
	firstRenderText = totalText.split(pointText)[0].replace(/\s+$/, "");
	beforeRenderText= pointText.replace(/\s+$/, "");
	renderText = totalText.split(pointText)[1];
}

const p = thisComp.layer("RenderText_Control").effect("Speed")("Slider").valueAtTime(0);

const startTime = (firstRenderText && beforeRenderText) ? (countCharComponents(firstRenderText)+countCharComponents(beforeRenderText))/p : 0;

let strList = typing(renderText);

if (time > startTime) {
	const adjustedTime = time - startTime;
	const strListIndex = Math.round(adjustedTime*p);
	thisComp.layer("Text").text.sourceText.getStyleAt(0,0).setText([strList[strList.length > strListIndex ? strListIndex : strList.length-1]]);
} else {
	[]
}

이 코드를 세 레이어에 똑같이 작성하지 않고, 하나의 레이어에서 작성한 뒤 출력하는 함수 값을 배열로 만들어서 각 레이어에 배열의 원소들을 각각 연결시켜 주면 딱 한번만 이 코드를 작성하면 됩니다.

텍스트 레이어의 함정

앞서 말한 것처럼 타이핑 함수로 출력되는 값을 배열로 만들어서 출력하려고 합니다. 여기서 출력되는 값은 모두 텍스트. 즉, 문자열이죠. 따라서 텍스트 레이어를 하나 만들어서 배열로 출력하려고 합니다.

그런데 여기서 함정이 있습니다. 함정은 바로 텍스트 레이어에서 출력하는 값은 배열이 아니라 텍스트라는 것입니다.

지금 보시는 것과 같이 텍스트 레이어에서 출력되는 순간 문자열로 바뀌게 됩니다. 텍스트 레이어에 값을 [[1,2,3],[4,5,6],[7,8,9]]로 출력하도록 넣었는데도 출력되는 텍스트는 1,2,3,4,5,6,7,8,9입니다. 쉼표까지도 텍스트로 출력해주게 됩니다.

텍스트 레이어에 1을 출력하고 다른 텍스트 레이어에서 덧셈 계산을 해도 텍스트+숫자 또는 텍스트+텍스트 형태로 계산을 합니다. 1+1=2가 아니라 11로 계산을 해주는 것이죠.

블록 안에서는 숫자로 계산을 해주던 값도 출력되는 순간 문자열로 바뀌게 됩니다. 이 부분을 잘 기억해두셔야 합니다.

재사용할 수 있는 함수값 형태로 만들기

텍스트 레이어에서 출력된 값을 재사용하기 위해 구분자를 사용합니다. 배열로 만들 수 없기 때문에 배열의 원소로 구분해서 재사용하기 위해 원소로 사용할 문자열 중간에 구분자를 넣어서 구분해주는 것이죠.

a = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
a[0]+"$$"+a[1]+"$$"+a[2];
//1,2,3,$$,4,5,6,$$,7,8,9 가 출력됩니다.

이렇게 구분자를 넣어서 출력하면 다른 텍스트 레이어에서 구분자를 기준으로 나눠서 배열로 만들어 재사용할 수 있습니다.

thisComp.layer("사용하고자하는 레이어 이름").text.sourceText.split("$$");
//["1,2,3","4,5,6","7,8,9"]로 만들어집니다.

thisComp.layer("사용하고자하는 레이어 이름").text.sourceText.split("$$")[0];
//"1,2,3"을 출력합니다.

thisComp.layer("사용하고자하는 레이어 이름").text.sourceText.split("$$")[1];
//"4,5,6"을 출력합니다.

thisComp.layer("사용하고자하는 레이어 이름").text.sourceText.split("$$")[2];
//"7,8,9"을 출력합니다.

이렇게 값을 불러와서 구분자를 기준으로 나눠서 배열로 만들고 원하는 배열의 원소를 출력하는 식으로 값을 재사용할 수 있습니다.

한글 타이핑 자막 텍스트 레이어 수정하기

지난 시간에 만들었던 한글 타이핑 자막을 간단하게 수정하겠습니다. 먼저 기존에 있던 레이어에서 텍스트 레이어를 하나 더 추가해줍니다.

그냥 Text 레이어에서 수정을 해도 괜찮은데 모션 그래픽 템플릿에서 폰트 컬러를 지정해주기 위해 익스프레션 코드를 넣어놓았기 때문에 새로운 텍스트 레이어를 추가했습니다.

그리고 TotalText 레이어의 소스텍스트에 다음과 같이 코드를 넣습니다.

//TotalText 레이어의 소스텍스트

let cCho = ["ㄱ", "ㄲ", "ㄴ", "ㄷ", "ㄸ", "ㄹ", "ㅁ", "ㅂ", "ㅃ", "ㅅ", "ㅆ", "ㅇ", "ㅈ", "ㅉ", "ㅊ", "ㅋ", "ㅌ", "ㅍ", "ㅎ"], 
cJung = ["ㅏ", "ㅐ", "ㅑ", "ㅒ", "ㅓ", "ㅔ", "ㅕ", "ㅖ", "ㅗ", "ㅘ", "ㅙ", "ㅚ", "ㅛ", "ㅜ", "ㅝ", "ㅞ", "ㅟ", "ㅠ", "ㅡ", "ㅢ", "ㅣ"],  
cJong = ["", "ㄱ", "ㄲ", "ㄳ", "ㄴ", "ㄵ", "ㄶ", "ㄷ", "ㄹ", "ㄺ", "ㄻ", "ㄼ", "ㄽ", "ㄾ", "ㄿ", "ㅀ", "ㅁ", "ㅂ", "ㅄ", "ㅅ", "ㅆ", "ㅇ", "ㅈ", "ㅊ", "ㅋ", "ㅌ", "ㅍ", "ㅎ"];

let toStringFromCharCode = (cho, jung, jong) =>
  String.fromCharCode(44032 + cho * 588 + jung * 28 + (jong || 0));
  
let seperateChar = (char) => {
  let cCode = char.charCodeAt() - 0xac00;
  let jong = cCode % 28;
  let jung = ((cCode - jong) / 28) % 21;
  let cho = ((cCode - jong) / 28 - jung) / 21;
  return []
    .concat(cCho[cho])
    .concat(toStringFromCharCode(cho, jung))
    .concat(cJong[jong] !== "" ? toStringFromCharCode(cho, jung, jong) : []);
};

let toKorChars = (char) => {
  let cCode = char.charCodeAt();
  return [].concat(
    (!cCode && []) ||
      (cCode === 32 && " ") ||
      ((cCode < 0xac00 || cCode > 0xd7a3) && char) ||
      seperateChar(char)
  );
};

let typing = (str) =>
  str
    .split("")
    .map((char) => toKorChars(char))
    .reduce(
      (pre, cur) =>
        pre.concat(
          cur.map((v) => (pre.length !== 0 ? pre[pre.length - 1] : "") + v)
        ),
      [""]
    );

const totalText = thisComp.layer("Text").text.sourceText.valueAtTime(0);
const pointText = thisComp.layer("PointText").text.sourceText.valueAtTime(0);

let firstRenderText, beforeRenderText, renderText;

if (!pointText || totalText.indexOf(pointText) === -1) {
	firstRenderText = totalText;
	beforeRenderText = "";
	renderText = "";
} else {
	firstRenderText = totalText.split(pointText)[0].replace(/\s+$/, "");
	beforeRenderText = pointText.replace(/\s+$/, "");
	renderText = totalText.split(pointText)[1];
}

const p = thisComp.layer("RenderText_Control").effect("Speed")("Slider").valueAtTime(0);

let strList = typing(firstRenderText+"$"+beforeRenderText+"$"+renderText);

if (time > 0) {
	const adjustedTime = time;
	const strListIndex = Math.round(adjustedTime*p);
	[strList[strList.length > strListIndex ? strListIndex : strList.length-1]];
} else {
	[]
}

코드가 많이 간결해졌습니다. totalText를 pointText로 구분해서 firstRenderText, beforeRenderText, renderTextf를 만들고, firstRenderText+”$”+beforeRenderText+”$”+renderText를 타이핑하게 만들었습니다. 어차피 순차적으로 타이핑되기 때문에 타이핑 시간 계산을 할 필요가 없어졌습니다.

//시간 계산을 위한 함수 삭제
const countCharComponents = str => {
  if (typeof str !== "string" || !str) return 0;
  return str.split("").reduce((sum, ch) => {
    return sum + toKorChars(ch).length;
  }, 0);
};

그래서 countCharComponents 함수를 삭제하게 되었습니다.

//RenderText_Left
thisComp.layer("Text").text.sourceText.getStyleAt(0,0).setText(thisComp.layer("TotalText").text.sourceText.split("$")[0]);
//RenderText_Center
thisComp.layer("PointText").text.sourceText.getStyleAt(0,0).setText(thisComp.layer("TotalText").text.sourceText.split("$")[1]);
//RenderText_Right
thisComp.layer("Text").text.sourceText.getStyleAt(0,0).setText(thisComp.layer("TotalText").text.sourceText.split("$")[2]);

이제 RenderText 의 소스텍스트를 간단하게 수정할 수 있습니다.

포인트 텍스트에만 강조 박스 만들기 코드에 적용하기

포인트 텍스트에만 강조 박스 만들기 컴포지션에도 똑같이 적용해보겠습니다. 먼저 텍스트 레이어를 하나 추가해서 TotalText로 이름을 바꿔주겠습니다.

그 다음 코드를 이렇게 작성해주세요.

//TotalText 레이어

posterizeTime(0);

totalText = thisComp.layer("Text").text.sourceText.valueAtTime(0);
pointText = thisComp.layer("PointText").text.sourceText.valueAtTime(0);

let firstText, secondText, thirdText;

if (pointText.length === 0 || totalText.indexOf(pointText) === -1) {
	firstText = totalText;
	secondText = [""]
	thirdText = [""];
} else {
	firstText = totalText.split(pointText)[0].replace(/\s+$/, "");
	secondText = pointText.replace(/\s+$/, "");
	thirdText = totalText.split(pointText)[1];
}

firstText+"$$"+secondText+"$$"+thirdText;

그리고 나머지 RenderText 레이어에 코드를 이렇게 작성합니다.

//RenderText_Left

posterizeTime(0);
totalText = thisComp.layer("Text").text.sourceText;
renderText = thisComp.layer("TotalText").text.sourceText.split("$$")[0];
totalText.getStyleAt(0,0).setText(renderText);
//RenderText_Center

posterizeTime(0);
totalText = thisComp.layer("PointText").text.sourceText;
renderText = thisComp.layer("TotalText").text.sourceText.split("$$")[1];
totalText.getStyleAt(0,0).setText(renderText);
//RenderText_Right

posterizeTime(0);
totalText = thisComp.layer("Text").text.sourceText;
renderText = thisComp.layer("TotalText").text.sourceText.split("$$")[2];
totalText.getStyleAt(0,0).setText(renderText);

이제 완성되었습니다.

어떤가요? 조금 더 효율적인 코드가 된 것 같지 않나요?

이렇게 애프터 이펙트에서는 다른 블록에 있는 변수나 함수 들을 재사용할 수는 없지만 대신 해당 블록 안에서 결과 값을 배열로 만들고, 구분자를 이용해서 텍스트로 출력한 뒤 다른 블록에서는 출력한 텍스트를 다시 배열로 만들어서 원하는 데이터만 취하는 방식으로 작업할 수 있습니다.

아무래도 여러 번 반복해서 복사 붙여넣기 할 필요가 없다는 장점과, 실제 렌더링 시 모든 레이어마다 같은 연산을 반복하지 않는다는 장점은 있지만, 코드를 짤 때 조금 더 신경써야하는 부분이 있을 수 있습니다.

그리고 이 방법으로 타이핑 자막을 만들 때 오히려 타이핑을 시작하는 시간을 계산할 필요가 없어졌기 때문에 좀 더 효율적인 코드로 재탄생할 수 있었습니다.

이번 시간에 만든 타이핑 자막과 포인트 텍스트 밑줄 자막 최종본입니다. 두 자막 다 posterizeTime()까지 사용해서 최적화 하였습니다.
필요하신 분들은 다운 받아 보세요.

그럼 이번 시간은 여기까지 하겠습니다.

P.S 포스팅을 하면서 템플릿들을 하나씩 정리하며 업로드 하는 사이트를 만들고 있습니다. 다운 받으시는 분들이 늘어나고 반응이 괜찮으면 회원 가입을 받고 서로의 자료를 공유하는 식으로 발전시키면 어떨까 생각됩니다. 편하게 구경해보시고 템플릿 다운받아 가세요.

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다