쾌락코딩

람다식에서 return문 사용하기

|

이 글에서는 람다식 내부에서 return을 만났을 때 실제로 return 되는 위치를 조절하는 방법을 다룬다. 해당 포스팅에서는 다루지 않지만 람다에서 return문 자체를 사용할 수 없는 경우도 있는데, 자세히 알고 싶다면 람다 내부의 return은 언제 불가능하며 이유는 무엇일까? 포스팅을 읽어보는 것을 추천한다.

코틀린은 함수형 프로그래밍을 지원하기 때문에 익명함수, 람다식을 사용할 경우가 흔하다. map, filter, reduce, forEach등 함수를 인자로 받는 함수들(고차함수)의 매개변수로 우리만의 로직이 담긴 함수를 넘길때 람다를 자주 사용한다. 자주 사용하는 만큼 꼭 집고 넘어가야할 사항이 있다.

일반 함수를 넘겼을 때 (return에 주목하자)

일반적으로 함수 내부의 return은 그 함수만 종료시킨다. 물론 코틀린의 label을 사용해서 종료시킬 함수를 지정할 수 있지만 그렇지 않은 경우 return문을 포함하는 가장 가까운 함수를 종료시키게 된다. 우선 람다식이 아니라 일반적인 익명 함수를 넣은 예시를 보자.

fun exampleFunc() {
    var ints = listOf(0,1,2,3)
    ints.forEach(
        fun(value: Int) {
            if (value == 0) return
            print("value ")
        }
    )
}

// 1 2 3

0 1 2 3 이 담긴 배열에서 forEach를 돌며 0이 아닌 요소만 출력하는 함수다. 우선은 forEach의 인자로 이름 없는 함수를 넘겨주었다. 이 경우는 if문에서 value == 0이 걸리게 되면 우리가 넘긴 이름 없는 함수만 종료된다. 이것은 맨 처음 말했듯이 일반적인 함수로써 함수 내부의 return은 그 함수만 종료시킨다는 원칙에 맞는 현상이다. 따라서 결과값은 1 2 3 이 출력된다.

람다식을 넘겼을 때

fun exampleFunc2() {
    var ints = listOf(0,1,2,3)
    ints.forEach {
        if (it == 0) return
        print(it)
    }
}
// 아무것도 출력되지 않음

forEach의 인자로 람다식을 넘기니 결과값이 달라진다. 왜 그럴까? 람다식은 자기 자신의 block 범위를 가지지 않기 때문이다. 즉 람다식 내부의 context는 자신을 감싸고 있는 외부 block인 것이다. javascript의 화살표함수와 비슷한 성향을 가지고 있다(화살표 함수 내부의 this는 자신을 감싸는 context를 가리킴). 따라서 위의 코드의 경우, 람다식 내부의 return은 자기 자신을 종료시키는 것이 아니라 exampleFunc2를 종료시킨다.

나는 람다식에 대해서만 return 하고 싶은데?

람다식 자체만 return으로 끝내고 싶다면 방법이 있다. kotlin의 label문법을 사용하면 된다.

fun exampleFunc3() {
    var ints = listOf(0,1,2,3)
    ints.forEach label@ {
        if (it == 0) return@label
        print(it)
    }
}
// 1 2 3

람다식에서 return문을 만나게 되면, 원래는 자신의 context는 exampleFunc3이기 때문에 exampleFunc3이 종료되야 한다. 하지만 label을 return하도록 코딩했으므로 해당 라벨이 가리키는 자기 자신(람다)만 종료된다. 주의해야 할 점은, forEach가 끝이나는게 아니라 forEach로 넘겨준 lambda가 끝이 난다는 것이고, forEach로 여러번 실행시킨 lambda들 중에서 조건에 맞는 lambda만 종료된다는 것이다.

매번 label을 사용해야 해?

이런식으로 람다를 사용할 일이 굉장히 많은데 그때마다 label을 사용해야 할까? 꼭 그렇지는 않다. 물론 완전히 안쓸수는 없지만 약간의 편법(?)이 존재한다.

암시적 label이란 것을 사용하면 label을 줄일 수 있다. 람다식을 사용할 때 암시적 label은 자동으로 람다가 사용된 함수의 이름이 된다. 예를 들어 forEach()가 람다를 사용했다면, 그 람다의 암시적 label 이름은 “forEach”인 것이다. 코드를 보자.

fun exampleFunc4() {
    var ints = listOf(0,1,2,3)
    ints.forEach {
        if (it == 0) return@forEach
        print(it)
    }
}
// 1 2 3

위 코드처럼 람다식 첫 부분에 label을 따로 명시하지 않아도 된다. 암시적 label이 forEach를 가르키기 때문이다. 물론 return문에 아직도 @라벨이 나타나긴 하지만 하나라도 더 줄인게 어디야 …

만약 ints.forEach가 아니라 ints.map을 사용할 때면 람다식 내부의 return문에 return@map 과 같이 선언해주면 된다.

람다가 아닌 forEach를 끝내는 방법

위 예제를 조금 변형시켜 리스트의 요소 중 1을 만나면 forEach를 끝내보자. 그러기 위해선 요소가 1 일 때 람다가 종료될 게 아니라 forEach가 종료되야 한다. 코틀린 공식 문서는 아래와 같은 방법의 예제 코드를 내놓았다.

fun exampleFunc5() {
    var ints = listOf(0,1,2,3)
    run loop@ {
        ints.forEach {
            if (it == 0) return@loop
            print(it)
        }
    }
}
// 0

forEach를 감싸는 함수(run)를 선언하여, 조건에 맞을 경우 감싼 함수(run)을 종료시키면 된다.

맥 OS terminal에서 intelliJ 열기

|

인텔리제이를 설치했다면, 인텔리제이로 열고 싶은 프로젝트 디렉터리에 가서

idea . 

를 입력해 인텔리제이를 실행할 수 있다. 다만 사전에 command-line launcher를 만들어 줘야 가능하다.

command-line launcher는 인텔리제이가 간단하게 만들어준다. 먼저 인텔리제이에 들어가서 아무 프로젝트나 연 다음, 상단바에 위치한 tool을 누른다. 이후 Create Command-line Launcher 버튼을 누른다. command_line

아래와 같은 화면이 뜨면 그대로 ok를 누른다. 그러면 끝이다. command_line2

이제 커맨드라인에서

idea .

위 명령어로 인텔리제이를 열 수 있다. command_line3

클린코드_경계

|

개인프로젝트를 제외하면 우리는 거의 대부분 오픈소스를 이용하거나 패키지를 사서 사용한다. 때로는 사내 다른 팀이 제공하는 컴포넌트를 사용한다. 프로젝트를 위해 내가 작성하는 코드와 타인이 작성한 코드사이의 경계, 이 경계를 깔끔하게 처리하는 방법에 대한 내용이다.

외부 코드 사용하기

패키지 제공자나 프레임워크 제공자는 적용성을 최대한 넓히려 애쓴다. 반면, 사용자는 자신의 요구에 집중하는 인터페이스를 바란다. 이런 경계의 온도차를 잘 극복해야 한다.

한 예로, java.util.Map을 살펴보자. Map은 아래와 같이 굉장히 다양한 인터페이스로 수많은 기능을 제공한다.

  • clear() void - map
  • containsKey(Object key) boolean - Map
  • containsValue(Object value) boolean - Map
  • entrySet() set - Map
  • equals(Object o) boolean - Map
  • get(Object key)Object - Map
  • getClass() Class<? extends Object> - Object
  • hashCode() int - Map
  • isEmpty() boolean - Map
  • keySet() Set - Map
  • notify() void - Object
  • notifyAll() void - Object
  • put(Object key, Object value) Object - Map
  • putAll(Map t) void - Map
  • remove(Object key) Object - Map
  • size() int - Map
  • toString() String - Object
  • values() Collection - Map
  • wait() void - Object
  • wait(long timeout) void - Object
  • wait(long timeout, int nanos) void - Object

기능이 많다고 무조건 좋은 것은 아니다. 예를 들어 우리 팀에서는 Map의 모든 데이터를 지울수 있는 claer메서드를 불필요하고 위험하다고 여긴다. 그러나 Map은 누구에나 clear메서드를 제공한다.

조금 더 살펴보자.

Map<String, Sensor> sensors = new HashMap<String, Sensor>();
Sensor s = sensors.get(sensorId); 

위 코드는 제네릭을 사용함으로써 깔끔해보이지만 사용자에게 필요하지 않은 기능까지 제공한다. 또한 프로그램에서 Map<String,Sensor>인스턴스를 여기저기로 넘긴다면, Map인터페이스가 변할 경우, 수정할 코드가 상당히 많아진다. 인터페이스가 변할 가능성이 거의 없다고 여길지도 모르지만, 자바 5가 제네릭스를 지원하면서 Map 인터페이스가 변했다는 사실을 명심해야한다.

아래는 Map을 좀 더 깔끔하게 사용한 코드다.

public class Sensors {
    private Map sensors = new Hashmap();

    public Sensor getById(string id) {
        return (sensor) sensors.get(id);
    }
}

경계 인터페이스인 MapSensors 안으로 숨긴다. 따라서 Map 인터페이스가 변하더라도 나머지 프로그램에는 영향을 미치지 않는다. Sensors 클래스만 적절히 대응해주면 된다.

Sensors를 사용하는 입장에서는 제네릭스가 사용되었는지 여부에 신경 쓸 필요가 없다. 제네릭스를 사용할지 말지는 Sensors에서 결정하면 될 일이다.

또한 Sensors 클래스는 프로그램에 필요한 인터페이스만 제공한다. 위에서 고민했던 clear메서드는 Sensors에서는 찾아볼 수 없다.

**Map 클래스를 사용할 때마다 위와 같이 캡슐화 하라는 소리가 아니다. Map을 여기저기 넘기지 말라는 말이다. 즉, 이를 이용하는 클래스나 클래스 계열 밖으로 노출되지 않도록 하자.

경계 살피고 익히기

외부 코드를 처음 사용하려고 할때 어떻게 시작해야하는지, 어떻게 익히는게 좋은지 알아보자.

외부 패키지 테스트가 우리 책임은 아니다. 하지만 우리 자신을 위해 우리가 사용할 코드를 테스트하는 편이 바람직하다.

학습 테스트 : 외부 코드에 대한 문서를 읽으며 사용법을 결정한 후, 곧바로 우리쪽 코드를 작성해 외부 코드를 호출하는 대신 먼저 간단한 테스트 케이스를 작성해 외부 코드를 익히는 것.

학습 테스트는 프로그램에서 사용하려는 방식대로 외부 API를 호출한다. 통제된 환경에서 API를 제대로 이해하는지를 확인하는 셈이다. 학습 테스트는 API를 사용하려는 목적에 초점을 둔다.

적용 예시

로깅 기능을 직접 구현하는 대신 아파치의 log4j 패키지를 사용하려 한다고 가정하자. 먼저 문서를 열어 자세히 읽기 전에 첫 번째 테스트 케이스를 작성한다.

@Test
public void testLogCreate() {
    Logger logger = Logger.getLogger("MyLogger");
    logger.info("hello");
}

테스트 케이스를 돌렸더니 Appender라는 것이 필요하다는 오류가 발생한다. 문서를 더 읽어본다. 더 읽어보니 ConsoleAppender라는 클래스가 있다. 그래서 Console Appender를 생성한 후 테스트 케이스를 다시 돌린다.

@Test
public void testLogAddAppender() {
    Logger logger = Logger.getLogger("MyLogger");
    ConsoleAppender appender = new ConsoleAppender();
    logger.addAppender(appender);
    logger.info("hello");
}

또 한번 에러와 마주친다. Appender에 출력 스트림이 없다는 에러다. 그래서 구글링을 한 후 다음과 같이 시도한다.

@Test
public void testLogAddAppender() {
    Logger logger = Logger.getLogger("MyLogger");
    logger.removeAllAppenders();
    logger.addAppender(new ConsoleAppender(
        new PatternLayout("%p %t %m%n"),
        ConsoleAppender.SYSTEM_OUT));
    logger.info("hello");
}

이제 잘 돌아간다(책에서는 이 코드에서 수상한 점을 해결해 나가는 방법도 소개하지만, 그 부분은 위의 단계를 하며 익힐 수 있는 단계라 넘어가겠다).

이 과정을 거치며 log4j가 돌아가는 방식을 상당히 많이 이해했으며 여기서 얻은 지식을 바탕으로 단위 테스트 케이스 몇개를 작성한다.

public class LogTest {
    private Logger logger;

    @Before
    public void initialize() {
        logger = Logger.getLogger("logger");
        logger.removeAllAppenders();
        Logger.getRootLogger().removeAllAppenders();
    }

    @Test
    public void basicLogger() {
        BasicConfigurator.configure();
        logger.info("basicLogger");
    }

    @Test
    public void addAppenderWithStream() {
        logger.addAppender(new ConsoleAppender(
            new PatternLayout("%p %t %m%n"),
            ConsoleAppender.SYSTEM_OUT));
        logger.info("addAppenderWithStream");
    }

    @Test
    public void addAppenderWithoutStream() {
        logger.addAppender(new ConsoleAppender(
            new PatternLayout("%p %t %m%n")));
        logger.info("addAppenderWithoutStream");
    }
}

모든 지식을 독자적인 로거 클래스로 캡슐화 한다. 그러면 나머지 프로그램은 log4j가 제공하는 인터페이스를 몰라도 된다(log4j 인터페이스가 변한다면 독자적으로 만든 클래스만 수정하자).

결론

  • 외부 API를 사용할 때 해당 부분 TEST CODE를 작성하며 익히고 사용하자.
  • 통제가 불가능한 외부 패키지에 의존하는 대신 통제가 가능한 우리 코드에 의존하게 하자(외부 패키지를 캡슐화해서 우리 패키지인 것 처럼 사용하게 하자).

Search Box와 검색어 추천 만들기

|

HTML, CSS 그리고 JAVASCRIPT로 Search Box 디자인을 해보자. 우리의 목표는 여기 링크에서 볼 수 있고 아래 이미지로도 볼 수 있다.

searchBox1 searchBox3

해보자!

HTML

<div class="frame">
  <div class="center">
    <input type="text" placeholder="Start typing ..."/>
    <div id="recommend" class="invisible">
        <div class="item"><span class="text"></span></div>
        <div class="item">Veritatis et <span class="text"></span></div>
        <div class="item">ut aliquid ex <span class="text"></span> vero eos</div>
    </div>
  </div>
</div>

실제로 검색어를 추천하는게 아니라, 검색어를 추천할 때 생길 디자인을 하는 것이 목적이므로 검색어들을 하드코딩했다.

.frame은 박스 파란 박스부분이고, .center는 우리가 앞으로 넣을 것들을 .frame의 중앙에 놓기위한 박스라고 생각하면 된다. input은 검색어를 입력하는 곳이고 #recommend는 추천 검색어들이 뜨는 공간이다. div로 했기 때문에 요소들이 아래쪽으로 쭉쭉 나열될 것이다.

CSS

먼저 .frame을 구현하자.

.frame {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 400px;
  height: 400px;
  margin-top: -200px;
  margin-left: -200px;
  border-radius: 2px;
  box-shadow: 4px 8px 16px 0 rgba(0,0,0,0.1);
  background: #5CA4EA;
}

단순히 파란색 박스를 body의 정 중앙에 놓고 그림자나 배경색을 더한 코드일 뿐이다.

.center를 구현하자.

.center {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%,-50%);
}

.center도 역시 중앙에 위치해야 함으로 position: absolute로 지정해 주었다.

input을 구현하자.

input {
  height: 1.8em;
  width: 220px;
  padding: 0 10px;
  box-shadow: 3px 3px 10px #566270;
  outline: none;
  border: none;
  color: #4F86C6;
}

::placeholder {
	color: #84B1ED;
}

높이와 넓이를 적당하게 지정해주고, 텍스트가 너무 인풋박스에 밀착되어지지 않도록 padding을 넣어주었다.

일반적으로 input 박스에 마우스를 클릭해놓으면 테두리가 파란색으로 변하게 되는데 outline: none을 하면 파란색 테두리가 사라진다.

input의 color를 바꾸어줬는데, 여기서 color란 사용자가 입력한 텍스트만 적용이 된다. 즉 placeholder에는 적용되지 않기 때문에 따로 ::placeholder지정자로 설정해 줘야 한다. color 뿐만 아니라 font-size같은 일반 디자인도 가능하다.

이제 #recommend를 구현하자. 사용자가 텍스트를 입력하면 input박스 밑에 나타날 영역이다. searchBox5

#recommend {
	margin-top: 1px;
	position: absolute;
	background: white;
	padding: 0 10px;
}

.item {
	height: 1.8em;
	width: 220px;
	outline: none;
}

.item:hover {
	color: #9baec8;
}
.text {
	font-weight: bold;
}

주목할 점은 #recommendposition: absolute이다. abolute는 뷰포트에 상대적으로 위치가 지정되는 것이 아니라 가장 가까운 곳에 위치한 조상 엘리먼트에 상대적으로 위치가 지정된다.

HTML 코드 일부를 다시 보자.

<div class="center">
    <input type="text" placeholder="Start typing ..."/>
    <div id="recommend" class="invisible">
        <div class="item"><span class="text"></span></div>
        <div class="item">Veritatis et <span class="text"></span></div>
        <div class="item">ut aliquid ex <span class="text"></span> vero eos</div>
    </div>
  </div>

#recommend의 가장 가까운 곳에 위치한 조상 엘리먼트는 .center이고, 바로 윗 이웃 요소로 input이 있기 때문에 그 바로 아래 영역에 절대적 위치로 잡히게 된다. 만약 #recommendabsolute가 아니라 relative 또는 static이었다면 절대적인 자리가 잡히지 않기 때문에 바로 윗 이웃 요소인 input과 함께 가운데 자리를 차지하려고 침범할 것이다. 우리는 absolute를 사용하여 이웃 요소의 자리를 침범하지 않고 해결할 수 있다.

거의 다 왔다. 마지막으로 #recommed영역은 최초에 보이지 않아야 하는 영역이므로 이를 처리해주자.

.invisible {
	display: none;
}

여기 까지 했다면, 아래와 같은 이미지가 된다. searchBox4

검색 박스에 텍스트를 넣어봐도 아무 변화가 없다. 이제 간단한 javascript 코딩으로 텍스트가 입력되면 추천 검색어 영역이 표시되고, 동시에 입력한 텍스트가 추천 검색어에도 뜨게 하자.

JAVASCRIPT

const inputBox = document.querySelector("input");
const recommendBox = document.querySelector("#recommend");
const texts = document.querySelectorAll(".text");

inputBox.addEventListener("keyup", (e) => {
	if (e.target.value.length > 0) {
		recommendBox.classList.remove('invisible');
		texts.forEach((textEl) => {
			textEl.textContent = e.target.value;
		})
	} else {
		recommendBox.classList.add('invisible');
	}
})

순수 자바스크립트 코드다. 이미 자바스크립트를 조금 다룰줄 안다면 너무 쉬운 난이도라서 딱히 설명할 부분도 없다.

여기까지 했다면 이 링크와 동일한 결과물을 얻을 수 있다. 전체 코드도 이 링크에 있다.

읕!

흔들리는 종 만들기

|

HTML, CSS만으로 흔들리는 종을 구현해보자! 아래 이미지 말고 생생한 현장을 보고싶다면 Codepen.io로 오면 된다. 직접 수정해 볼 수도 있다. GIF로 만들지 못한점은 정말 죄ㅅ…

ring1 ring2

HTML

먼저 HTML 코드부터 작성하자. HTML은 사실 너무 간단해서 한번에 다 올리겠다.

<head><link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.1/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous"></head>

<div class="box">
    <div class="bellWrapper">
      <i class="fas fa-bell my-bell"></i>
    </div>
    
    <div class="circle first"></div>
    <div class="circle second"></div>
    <div class="circle third"></div>
</div>

Html에서 주목할 점은 font-awesome을 가져오는 링크 뿐이다. cdn으로 font-awesome을 가져와 종모양 아이콘을 사용할 것이다(종소리까지 만들기 너무 힘들 것 같아서가 아니다. 절대).

덧붙이자면 붉은 박스 부분은 .box로, 그 아래에 종모양을 감싸는 .bellWrapper와 소리가 퍼져나가는 것을 표현할 circle을 나란히 작성했다.

ring3 HTML만 작성했을 경우 위의 사진처럼 화면 좌측 상단에 검은색 종모향 하나만 딸랑 존재한다. 이제 CSS를 입혀 본격적으로 작업 해보자.

CSS

가장 먼저 분홍색 박스를 감싸고 있는 body태그를 건드릴 것이다.

.body {
    height: 100vh;
    width: 100vw;
    display: flex;
    align-items: center;
    justify-content: center;
    color: white;
}

heightwidthbody에 적용하였는데, 만약 적용하지 않는다면 아래와 같이 된다(사실 아직 박스에 색을 입히지 않아서 모든게 흰색으로 보이기 때문에 임의로 box에 칼라를 입혀서 보여준 것이다. 만약 따라하고 있는 중이라면 아무것도 보이지 않는게 정상이다). 즉 body의 크기가 자식 요소의 크기에 맞춰지게 되어 flex를 적용하였음에도 불구하고 화면의 딱 중간에 놓이지 않는다.

ring4 그러니 body의 크기를 스크린 전체 크기로 잡아서 상하좌우 중앙정렬하자.

중앙정렬은 flex를 사용한 후 아래처럼 아주 간단히 할 수 있다.

    align-items: center;
    justify-content: center;

flex를 모른다면 이 링크에서 배우자. 게임형식으로 쉽게 배울 수 있다.

box

이제 분홍색 box를 꾸며보자.

.box {
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 10px;
  background: linear-gradient(to bottom,#DD3C54 0%,#ff686b 100%);
  padding: 150px;
}

여기까지 하면 바로 위에서 본 사진처럼 나올 것이다. box 내부에서도 상하좌우 중앙 정렬을 위해 flex를 사용했다.

종 꾸미기

.bellWrapper {
  font-size: 50px;
}

.my-bell {
  transform-origin: top;
  animation: bell 2s infinite linear;
}

@keyframes bell{
  0%, 50%{
    transform: rotate(0deg);
	}
  5%, 15%, 25%, 35%, 45% {
    transform: rotate(13deg);
  }
  10%, 20%, 30%, 40% {
    transform: rotate(-13deg);
  }
}

먼저 주목할 점은 .bellWrapper에서 font-size:50px를 해준 것이다. 이는 종 아이콘의 사이즈를 결정하는 부분인데, 어떻게 font-size로 아이콘 크기를 수정할 수 있는 것일까?

그것은 font-awesome이 텍스트 방식으로 아이콘을 만들 수 있게 해주는 솔루션이기 때문이다. 컴퓨터에서 기본적으로 제공하고 있는 아이콘을 출력하는 방식이 아니라, 특수하게 개발된 Font Awesome 아이콘 전용 폰트 파일을 이용하여 아이콘을 출력한다.

.my-bell

transform-origin: top; 부분을 보자. .my-belltransform애니메이션(짧은주기의 화전을 좌우로 반복)을 부여할 것인데 이때 기준(origin)이 되는 위치를 지정하는 코드다. top으로 지정했으므로 transform의 중심, 기준은 윗부분이 되어 윗부분은 움직이지 않을 것이다.

애니메이션을 50% 까지 잡은 이유

50%로 한 이유는, Codepen.io을 들어가봤다면 알겠지만 뒷부분 50% -> 100%는 종이 흔들리지 않고 쉬게끔 하기 위해서다. infinite로 설정해주었기 때문에 0 ~ 100%를 반복할 것이다. 따라서 0 ~ 50%의 시간은 종이 흔들리고, 50 ~ 100%는 잠쉬 쉬고 다시 0%로 돌아가서 흔들림을 반복한다.

여기까지는 종이 흔들리긴 하지만, 주변에 아무 효과없이 단지 종만 흔들리고 있어서 심심하다. 울림 효과를 적용해보자.

울림 표현하기

.circle {
  width: 100px;
  height: 80px;
  position: absolute;
  border: 2px solid white;
  border-radius: 70%;
  border-color: transparent white;
  animation: ring 2s infinite linear both;
}

.second {
  animation-delay: .3s;
}

.third {
  animation-delay: .7s;
}

@keyframes ring{
  0%, 100% {
    opacity: 0;
  }
  
  1% {
    opacity: 1;
  }

  50% {
    width: 250px;
    height: 250px;
    opacity: 0;
  }
}

circle은 말 그대로 원인데, 결과물을 보면 원은 찾아볼 수 없다. 단지 흰 곡선들이 양옆으로 퍼지는 효과만을 볼 수 있는데, 사실은 퍼지고 있는 곡선은 원의 일부분이다. 즉 원의 위, 아랫부분은 투명색인 것이고 좌우 부분은 흰색인 것이다. 그것은 border-color: transparent white; 이 코드로 가능하다. margin이나 padding을 적용할 때 처럼 short-hand가 동일하게 적용된다. 즉 transparent는 상,하에 적용되고 white부분은 좌,우에 적용이 된다.

position: absolute;을 해줌으로써 원을 종소리와 동일하게 정 중앙에 위치시킬 수 있었다. 만약 abolute적용을 해주지 않았다면 아래 사진처럼 정말 다이나믹한 일이 벌어진다. 흰 원이 종소리를 기준으로 오른쪽에 추가가 되는데, 원 세개가 모두 겹쳐져 있지 않고 자기마음대로 애니메이션을 작동한다.

ring5

아무튼 우리는 absolute를 적용해 주었기 때문에 3개의 원이 모두 겹쳐있는 상태이다(애니메이션 효과를 지우고 확인해 보라).

ring6

종이 흔들리지 않을 때는 울림표시가 나타나면 안되므로 애니메이션의 시작부분(0%)에서는 opacity를 0으로 해주었다. 또한 겹쳐진 3개의 원이 차례대로 애니메이트되도록 delay를 다르게 부여했다.

애니메이션 100%에서 또다시 opacity : 0을 해준 이유는, 50%에서 opacity가 0이었다가 100%로 향하면 다시 opacity가 1로 변하기 때문이다. 기존의 opacity가 기본값인 1이기 때문에 돌아가는 것이다. 뿐만아니라 기존의 widthheight도 원래 값인 width: 100px;, height: 80px;으로 돌아간다. 그렇게 되면 처음 종이 흔들릴때 울림이 벽에 맞고 다시 종으로 모이는 꼴이 된다.

읕!