Flow는 Facebook이 오픈소스로 만든 자바스크립트를 위한 정적 타입 검사기이다. Flow는 자바스크립트의 많은 약점들을 해결해 주며, 더 낫고 이해하기 쉬운 코드를 작성할 수 있도록 해 준다.
Flow의 홈페이지에 따르면:
Flow는 아래와 같은 자바스크립트의 일반적인 버그들을 프로그램을 실행하기 전에 잡아낼 수 있다.
- 암묵적 타입 변환
- Null 참조
- 그리고 무시무시한 'undefined is not a function'
또한
Flow는 코드에 점진적으로 타입 선언을 추가할 수 있도록 해준다.
즉, Flow는 많은 일반적인 자바스크립트 문제에 대한 해결책이며, 코드 베이스에 점진적으로 도입할 수도 있다. 좋구나!
타입
Flow에 대해 살펴보기 전에, 먼저 타입이 무엇인지를 명확하게 알아보자. 위키피디아의 데이터 타입에 대한 글을 살펴보자.
타입은 실수, 정수, 불린 등과 같이 여러 종류의 데이터 중 하나를 식별해 내는 분류로서,
더 나아가 해당 타입에 사용 가능한 값, 해당 타입에 사용할 수 있는 명령들, 데이터의 의미,
해당 타입의 값을 저장하는 방식을 결정한다.
내 나름대로 정리해 보자면, 타입은 프로그램 안에서의 데이터에 대한 규칙이며, 컴퓨터로 하여금 당신이 그 데이터로 할 수 있는 일과 할 수 없는 일을 결정하도록 해 주며, 이는 당신이 의도치 않게 이들 규칙을 깨려고 할 때 많은 도움이 될 것이다.
여러가지 언어로 코드를 작성해 보았다면, 타입이 사용되는 방식이 언어에 따라 다르다는 것을 알 것이다. 명시적으로 반드시 필요할 수도 있고, 선택적으로 사용될 수도 있으며, 거의 존재하지 않을 수도 있다. 일반적으로 프로그래밍 언어의 타입 시스템은 두 개의 카테고리로 구분된다. 강한(Strong) vs 약한(Weak) 그리고 정적(Static) vs 동적(Dynamic) 이다.
강한 타입 vs 약한 타입
위키피디아에 아주 잘 설명이 되어 있다. 둘에 대한 보편적인 구분은 약간 모호한데, 이는 강한 타입 vs 약한 타입에 대한 합의된 정의가 없기 때문이다. 나는 위의 위키피디아 문서에서 제목으로 쓰인 다음의 정의를 사용하도록 하겠다.
암묵적 타입 변환과 "타입 퍼닝(punning)"
파이썬과 같은 강한 타입 언어에서는 호환되지 않는 두 가지 값을 결합할 때 타입 에러가 발생한다. 타입 에러를 피하기 위한 유일한 방법은 두 값이 어울릴 수 있도록 명시적으로 변환시켜 주는 것이다.
x = 5
print x + "" # 정수형에 문자열을 더할 수 없음
는 다음의 에러를 발생시킨다.
TypeError: unsupported operand type(s) for +: 'int' and 'str'
하지만 다음 코드는 문제없다.
x = 5
print str(x) + "" # x를 문자열로 변환했기 때문에 문제없음
자바스크립트와 같은 약한 타입 언어에서는 변수들이 사용될 때 암묵적으로 자신의 타입을 변환하기 때문에 아무런 문제가 없다. 문자열을 객체에 더하거나, 배열에 객체를, 숫자형에 Null을 더할 수도 있다. 심지어 실수로 잘못된 값을 사용한 것에 대해서 에러조차 발생하지 않는다.
console.log({} + {}) // NaN
console.log({} + []) // 0
console.log([] + []) // ''
console.log({} + 2) // [object Object]2
console.log({} + 'hello') // [object Object]hello
이처럼 어떤 에러도 발생하지 않는 상황으로 인해 어떤 문제들이 발생할 지 아마 상상할 수 있을 것이다.
정적 타입 vs 동적 타입
정적 타입 vs 동적 타입은 강한 타입 vs 약한 타입에 비해 좀 더 논란의 여지가 있다. 나는 어떤 특정한 타입이 다른 타입보다 더 낫다고 말하거나, 각각의 장점에 대해 심도깊은 분석을 하지는 않을 것이다. 대신 각각에 대해 짧게 소개를 해볼까 한다. 만약 어떤 타입이 더 나은지에 대한 논쟁을 좀 더 살펴보고 싶다면 다음의 링크를 확인해보기 바란다.
정적 타입
내가 알고 있는 한, 정적 타입을 사용하는 대부분의 언어들은 강한 타입을 사용한다. 뿐만 아니라 정적 타입을 사용하는 언어들에서는, 명시적으로 변수의 타입을 지정한다. 대부분의 사람들이 알고 있듯이, 정적 타입을 사용하는 언어인 자바에서는 int
나 String
와 같이 변수의 타입을 지정하고, int add(int a, int b)
와 같이 함수의 리턴값이나 파라미터의 타입들을 지정한다.
public class Hello {
public static void main(String[] args) {
int x = 5;
int y = 10;
String s = "1.23131";
System.out.println(add(x, y)); // 15
System.out.println(add(x, s)); // Incompatible types: String cannot be converted to int
}
public static int add(int a, int b) {
return a + b;
}
}
이 코드를 컴파일 하면 8번째 라인에서 에러가 발생하는데, 왜냐하면 String
타입을 int
타입에 더할 수 없기 때문이다.
다음의 사항에 주목하자.
- 에러는 실행되는 시점(run-time)이 아닌 아닌 컴파일하는 시점(compile-time)에 발생하며, 이는 에러를 고치지 않으면 코드를 실행할 수 없다는 의미이다.
- 만약 IDE를 사용한다면,
add(x, s)
가 불가능하다는 메시지를 보게 될 것이다. 변수의 타입을 미리 지정해 두었기 때문에 당신의 코드는 상위 레벨에서 분석될 수 있으며, 실수를 찾기 위해 컴파일할 필요가 없다. - 만약
add
함수가sfjkasjf
라는 이름을 갖고 있어도, 당신은 여전히 이 함수가 두 개의 정수를 받아서 하나의 정수를 반환한다는 것을 알 수 있으며, 이는 유용한 정보이다.
정적 타입 언어에서의 타입 추론(Type inference)
앞서 언급했던 ‘정적 타입의 언어들은 명시적으로 타입을 지정해야 한다’는 말은 사실 100% 진실이 아니다. 자바처럼 타입 추론이 없는 언어에서는 사실이지만, 타입 추론을 지원하는 언어에서는 당신이 사용하는 타입을 컴퓨터가 알아내도록 내버려둘 수 있다. 아래의 예제는 위의 예제와 같은 코드를 하스켈로 작성한 것인데, 하스켈은 강력한 타입 시스템으로 유명하면서도 let x = 1
과 let add' = (+)
를 같이 작성하면 타입을 명시적으로 지정하지 않아도 스스로 타입을 추론해 낸다.
Haskell
main :: IO()
main = do
let x = 1
let y = 2
let s = ""
-- Type inference
let add' = (+)
print (add x y) -- 3
print (add' x y) -- 3
print (add x s) -- throws error
-- With Explicit Types
add :: Int -> Int -> Int
add = (+)
타입 추론은 Flow의 타입 시스템을 비롯한 다른 많은 타입 시스템에서 지원한다. 하지만 일반적인 견해에서 타입 추론이 코드량을 줄여주어 삶을 편하게 해 주는 것은 맞지만, 모든 것을 타입 추론에 의존할 수는 없으며, 그래서도 안된다.
동적 타입
동적 타입의 언어에서는 오직 코드내의 값(value)에 의해서만 타입이 정해진다. 타입을 직접 지정할 일은 전혀 없다. 이 방식의 주된 장점은 코드가 더 깔끔해진다는 점과, 프로그래밍을 할 때 타입에 대해 신경쓸 일이 없다는 것이며, 이로 인해 단기적으로는 생상성이 향상될 수 있다. 위의 코드를 파이썬으로 작성하면 다음과 같을 것이다.
def main():
x = 5
y = 10
s = "1.23131"
print add(x, y) # 15
print add(x, s) # TypeError: unsupported operand type(s) for +: 'int' and 'str'
def add(a, b):
return a + b
이 코드를 실행하면 라인 7에서 에러가 발생하는데, 이는 string
타입을 int
타입에 더할 수 없기 때문이다. 이것이 에러인 이유는 파이썬이 강한 타입 언어이기 때문임을 기억하기 바란다. 약한 타입 언어인 자바스크립트에서는 add(5, "1.23123213")
가 100% 유효한 코드일 것이다.
다음의 사항에 주목하자.
- 코드가 더 간결하다.
- 타입 추론이 없다. 동적 타입 언어에서 변수들은 단지 값을 담고 있는 컨테이너일 뿐이며, 다른 어떠한 특성도 없다.
add(x, s)
가 실패하는 이유는 실행되는 시점에int
와string
을 더하려고 하기 때문이지x
와s
가 서로 같이 연산될 수 없음을 인터프리터가 미리 알아냈기 때문이 아니다. a
와b
의 타입이 실제로 무엇인지 말할 수 없다.int
,string
,float
혹은 어떤것이든 모두 가능하다.- 에러를 발생시킨다는 것은 같지만, 컴파일 시점이 아닌 실행 시점이라는 것이 큰 차이이다. 이는 동적 타입 언어에서 테스트가 더욱 중요하다는 것을 의미하는데, 왜냐하면 코드가 타입 에러를 포함하고 있어도 일단 문제없이 실행될 것이기 때문이다.
자바스크립트와 Flow
이제 타입에 대해서 더 잘 알게 되었으니, 다시 원래의 문제로 돌아와서 자바스크립트 코드에서 어떻게 실수를 줄일 수 있을지를 알아보자.
자바스크립트는 약한 타입이면서 동적 타입인 언어인데, 이는 유연하지만 에러를 극도로 유발하는 조합이다. 위에서 살펴보았듯이, 암묵적 형변환 때문에 다른 타입간의 모든 연산은 이들 연산이 유효한지 아닌지와 무관하게 에러없이 실행되며 (약한 타입), 타입을 직접 지정하는 일도 결코 없다. (동적 타입)
다음의 예제에서 살펴볼 수 있듯이 이러한 약한 타입과 동적 타입의 혼합은 아주 큰 불행이며, 수없이 많은 사람들이 이러한 특징을 비판하고 있다.
이런 대부분의 문제들에 대한 해결책은 Flow이다. Flow는 정적 타입, 타입 추론을 통해 위에서 본 것과 같은 자바스크립트의 많은 문제들을 처리한다.
이 글은 튜토리얼이 아니므로, 만약 더 자세한 내용을 알고 싶다면 Flow 시작하기 가이드를 참고하기 바란다.
이제 좀 더 나아가 보자. 처음에 ‘약한 타입’ 절에서 보았던 자바스크립트 예제로 돌아가서 기존 코드에 Flow를 적용해 보겠다.
타입 기능을 사용하기 위해 첫번째 줄에 // @flow
를 추가하고, 커맨드라인 도구인 flow
를 실행해서 코드를 검사한다. (물론 IDE에서 지원한다면 그걸 사용해도 된다) :
// @flow
// ^^^^^ that's necessary to activate flow
// flow is opt-in to allow you to gradually add types
console.log({} + {}) // NaN
console.log({} + []) // 0
console.log([] + []) // ''
console.log({} + 2) // [object Object]2
console.log({} + 'hello') // [object Object]hello
즉시 모든 각각의 라인이 아래와 비슷한 타입 에러를 발생시킬 것이다.
index.js:3
3: console.log({} + {}) // NaN
^^ object literal. This type cannot be added to
3: console.log({} + {}) // NaN
^^^^^^^ string
타입 어노테이션과 같은 추가적인 작업이 전혀 없이도, Flow는 이미 뭔가가 잘못되었다는 것을 알려준다. 위의 wat
영상은 더이상 적용되지 않는다.
타입 어노테이션의 이점
Flow가 위의 예제에서처럼 에러를 잡는 것을 도와주긴 하지만, 진정한 이점을 누리기 위해서는 직접 자신만의 타입 어노테이션을 작성해야 한다. 즉, 값에 특정한 타입을 지정하기 위해 Flow의 내장된 타입인 number
, string
, null
, boolean
, etc.
등을 사용하거나 다음과 같이 자신만의 타입 별칭을 만들어내야 한다.
type Person = {
age: number,
name: string,
gender: 'male' | 'female'
}
다음과 같은 함수를
function xyz(x, y, z) {
return x + y + z
}
이렇게 변경할 수 있다.
// @flow
function xyz(x: number, y: number, z: number): number {
return x + y + z
}
이 경우에 우리는 xyz가 3개의 숫자형을 받아서 하나의 숫자형을 반환한다는 것을 알 수 있다. 만약 xyz({}, '2', [])
를 시도한다면 자바스크립트에서는 100% 유효한 코드이지만(lol), Flow가 에러를 발생시킬 것이다! 이런 식의 코드를 점점 더 작성하기 시작하면, Flow는 당신이 작성한 코드에 대해 좀 더 알 수 있게 되고 당신이 만들어낸 실수에 대해 좀 더 나은 조언을 해 줄 것이다.
몇가지 예제
함수에 전달하는 파라미터 개수 오류를 잡아낸다
코드:
// @flow
function xyz(x: number, y: number, z: number): number {
return x + y + z
}
xyz(1, 2)
에러:
index.js:7
7: xyz(1, 2)
^^^^^^^^^ function call
7: xyz(1, 2)
^^^^^^^^^ undefined (too few arguments, expected default/rest parameters). This type is incompatible with
3: function xyz(x: number, y: number, z: number): number {
^^^^^^ number
잘못된 파라미터 타입 오류를 잡아낸다
// @flow
function xyz(x: number, y: number, z: number): number {
return x + y + z
}
xyz(1, 2, '')
에러:
index.js:7
7: xyz(1, 2, '')
^^^^^^^^^^^^^ function call
7: xyz(1, 2, '')
^^ string. This type is incompatible with
3: function xyz(x: number, y: number, z: number): number {
^^^^^^ number
Null 체크를 잊지 않도록 해준다
코드:
// @flow
function xyz(x: number, y: number, z: number): ?number {
return Math.random() < 0.5 ? x + y + z : null
}
function printNumber(x: number): void {
console.log(x)
}
printNumber(xyz(1, 2, 3))
에러:
index.js:11
11: printNumber(xyz(1, 2, 3))
^^^^^^^^^^^^^^^^^^^^^^^^^ function call
11: printNumber(xyz(1, 2, 3))
^^^^^^^^^^^^ null. This type is incompatible with
7: function printNumber(x: number): void {
^^^^^^ number
올바른 타입을 반환하도록 해준다
코드:
// @flow
function xyz(x: number, y: number, z: number): number {
return Math.random() < 0.5
? x + y + z
: null
}
에러:
index.js:6
6: : null
^^^^ null. This type is incompatible with the expected return type of
3: function xyz(x: number, y: number, z: number): number {
^^^^^^ number
객체가 필요한 모든 프라퍼티들을 갖도록 만든다
코드:
// @flow
type Person = {
age: number,
name: string,
gender: 'male' | 'female'
}
const person: Person = { name: 'joe', age: 10, gender: 'male' }
console.log(person.job)
에러:
index.js:9
9: console.log(person.job)
^^^ property `job`. Property not found in
9: console.log(person.job)
^^^^^^ object type
더 깊게 들어가기
아마도 내가 빼먹은 몇가지 이점들이 있을 테지만, 위의 예제들은 대부분의 보편적인 이점들을 다루고 있다. 만약 ‘이게 다야?’ 라고 생각한다면, 더 깊게 파고들수록 훨씬 많은 것들이 있다는 것을 알아두자. 이는 단순히 변수나 반환값의 타입에 관한 것이 아닌, 타입으로 개념화할 수 있는 모든 것들에 대한 것이다.
Giulio Canti가 쓴 몇가지 아티클에서 Flow로 할 수 있는 좀 더 많은 것들에 대해 다루고 있다. 읽어보면 Flow를 사용해서 당신이 작성한 코드의 모든 부분이 의도한 대로 동작하도록 만들 수 있을 것이다.
- Flow와 유령 타입(Phantom Type): 타입을 사용해 유저의 입력값이 유효한지를 체크할 수 있다.
- Flow와 정제 타입(Refinement type): 유효성 체크가 내장된 타입을 만들어낼 수 있다.
- Flow와 상위 종류 타입(Higher Kinded type): 상위 종류 타입을 만들어낼 수 있다.
- Flow에서 Eff 모나드 구현하기: 타입으로 코드 내의 부작용(side effect)을 표현할 수 있다.
그는 정말 인상깊은 flow-static-land의 저자이기도 하다.
결론
- 자바스크립트는 약한 타입과 동적 타입을 사용하는데, 이는 에러를 유발하기 쉬우며 언어가 나쁜 평가를 받는 데에 한몫 하고 있다.
- 약간의 비용을 미리 들여서 천천히 적용해 본다면, Flow는 자바스크립트에 타입 시스템을 추가하여 위의 두가지 문제를 해결해줄 것이다.